阿里云首页 相册与网盘服务

JWT应用接入

本文档主要描述基于 JWT 应用如何获得相册与网盘服务(Photo And Drive Service, 后面简称PDS)授权访问凭证access_token。

JWT 应用介绍

本文档中的JWT应用是指使用 JWT(JSON Web Token)机制 进行身份认证的自定义应用

JWT应用可以在服务端通过私钥对数据进行签名得到一个JWT字符串,该JWT字符串作为访问已经配置了公钥的服务端的凭证。

vv

适用场景

  1. 企业已有内部的软件系统,包含独立的账号体系,想通过内部的登录页面登录,然后使用PDS的功能。

  2. 企业已有独立的账号体系和登录入口,想要使用已有的登录入口结合 PDS 搭建一套已有独立账号的云存储系统。

接入步骤概览

  1. 在 PDS 控制台创建自定义域和JWT应用。

  2. 利用RSA算法创建一对公私钥,将公钥保存到PDS服务端,私钥保存到JWT应用服务端。

  3. JWT应用服务端将数据进行编码并用私钥进行签名生成JWT字符串,然后发送给PDS服务端。

  4. PDS服务端使用公钥验证JWT字符串合法后,返回access_token给JWT应用服务端,JWT应用服务端可以通过access_token来调用PDS服务端提供的API。

详细步骤

1 配置秘钥

1.1 创建或选择域

a1

1.2 创建或选择应用

进入域详情,在应用列表界面,创建(选择)一个应用:

k1

1.3 设置公钥

创建(选择)应用后,点击”设置公钥”:

k3

生成公私钥:k5

生成公私钥后,记得复制私钥,自己保存。然后点确定即可。

k4

2 获取ACCESS_TOKEN

2.1 应用服务端计算JWT字符串

将待签名的数据进行编码,并使用私钥通过指定的加密算法对其进行签名,生成JWT字符串。下面是Node.js 的参考代码:

  1. const JWT = require('jsonwebtoken');
  2. const privateKeyPEM = '';
  3. var current_time_sec = Math.floor(Date.now()/1000);
  4. var opt = {
  5. iss:'',
  6. sub:'',
  7. sub_type:'',
  8. aud:'',
  9. jti: '',
  10. exp: current_time_sec + 60,
  11. iat: current_time_sec ,
  12. //nbf: '',
  13. auto_create: true,
  14. };
  15. var token = JWT.sign(opt, privateKeyPEM, {
  16. algorithm: 'RS256'
  17. });

opt 参数说明:

字段名 是否必选 类型 描述
iss 必选 String App ID
sub 必选 String User ID、Domain ID
sub_type(扩展字段) 必选 String 账号类型,目前支持填 user、service,此处填user,则sub为userID,签发普通用户accessToken。 此处填service,则sub为domainID,签发domain服务账号accessToken(超级管理员权限)
aud 必选 String Domain ID
jti 必选 String 应用生成JWT的唯一标识,长度16-128位,推荐使用uuid即可
exp 必选 Integer JWT过期时间, Unix Time,单位秒,最多在当前时间基础上加60秒
iat 可选 Integer 签发时间,Unix Time,单位秒,在此时间之前无法使用,如:1577682075
nbf 可选 Integer 生效时间,Unix Time,单位秒,不指定则默认生效
auto_create(扩展字段) 可选 Boolean 如果用户不存在,则自动创建,默认不创建用户。

更多关于JWT的三方库和计算方法请参考JWT官网

2.2 通过JWT字符串换取的access_token

调用OAuth API换取access_token:

  1. POST /v2/oauth/token
  2. Content-Type: application/x-www-form-urlencoded
  3. grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=${APP_ID}&assertion=xxxxxxxxxx

注意:要设置请求的 content-type 为 application/x-www-form-urlencoded

注意:请求参数要放在body里

请求参数说明:

字段名 是否必选 类型 描述
grant_type 必选 String 申请授权的类型,此处应为字符串常量: urn:ietf:params:oauth:grant-type:jwt-bearer
client_id 必选 String 应用ID
assertion 必选 String 上一步骤计算出来的JWT

返回 token json 样例

  1. {
  2. access_token: 'eyJh****eQdnUTsEk4',
  3. refresh_token: 'kL***Lt',
  4. expires_in: 7200,
  5. token_type: 'Bearer'
  6. }

应用服务端拿到 access_token 后返回给应用web端,调用PDS API的时候带上 access_token 就可以访问用户在PDS 上的资源。

2.3 更新access_token

通过JWT方式获取的access_token的有效期只有2小时,超过2小时后access_token将过期。7天内可以调用PDS API通过过期的access_token来获取新的access_token,7天后需要重新按照步骤2.1和2.2获取access_token。

调用OAuth API换取access_token的请求内容如下:

  1. POST /v2/oauth/token
  2. Content-Type: application/x-www-form-urlencoded
  3. client_id=${APPID}&client_secret=${APPSecret}&refresh_token=${access_token}&grant_type=refresh_token&redirect_uri=${REDIRECT_URI}

注意:要设置请求的 content-type 为 application/x-www-form-urlencoded请求参数要放在payload里

字段名 是否必选 类型 描述
client_id 必选 String 应用ID
client_secret 必选 String 应用 Secret
refresh_token 必选 String 已过期的access_token
grant_type 必选 String 申请授权的类型,此处应为字符串常量”refresh_token”
redirect_uri 必选 String 创建App时填写的回调地址

3 使用 Basic UI (可选)

如果您不想自己开发UI,而我们官方提供的Basic UI可以满足您的要求,可以直接使用Basic UI。

方法1:

使用window.open 打开 basic ui,postMessage传递AccessToken过去即可。

示例代码:

  1. const ENDPOINT = `https://${domain_id}.apps.aliyunpds.com`
  2. var win = window.open(ENDPOINT + '/accesstoken')
  3. window.addEventListener('message', onMessage, false)
  4. async function onMessage(e) {
  5. if (e.data.code == 'token' && e.data.message == 'ready') {
  6. var result = await getToken();// 从服务端获取 AccessToken
  7. //result = {"access_token": ...}
  8. win.postMessage({
  9. code: 'token',
  10. message: result
  11. }, '*')
  12. window.removeEventListener('message', onMess)
  13. }
  14. }

方法2:

使用 iframe 嵌入 basic ui,postMessage 传递 AccessToken 过去即可。

示例代码:

  1. //iframe嵌入URL构成:
  2. const iframeURL = `https://${domain_id}.apps.aliyunpds.com/accesstoken?origin=${location.origin}`

html代码:

  1. //注意替换变量iframeURL
  2. <iframe id="ifr" src="iframeURL"></iframe>
  1. window.addEventListener('message', onMess)
  2. async function onMessage(e) {
  3. if (e.data.code == 'token' && e.data.message == 'ready') {
  4. var result = await getToken();// 从服务端获取 AccessToken
  5. //result = {"access_token": ......}
  6. document.getElementById('ifr').contentWindow.postMessage({
  7. code: 'token',
  8. message: result
  9. }, '*')
  10. window.removeEventListener('message', onMess)
  11. }
  12. }

注意: 使用方法2,还需要在basic ui中配置这个安全设置,把宿主页的origin配置上

假设宿主页为 http://demopds.com/a.html, origin为 http://demopds.com, 这里配置 demopds.com 即可。

v2

方法3:

BasicUI 通过 iframe 嵌入自定义登录页面。

在系统配置中,配置自定义登录页面的 url,和 jwt 的APPID(让 BasicUI 自动刷新token):

pp2

用户登录时,不在打开BasicUI的默认登录页面,而是iframe嵌入的打开自定义登录页面。

登录成功后, 通过postMessage 向宿主页传递 token:

pp3

  1. if(parent!=self){
  2. let origin = ''
  3. parent.postMessage({
  4. code: 'token',
  5. message: {
  6. access_token: 'xxxx',
  7. refresh_token: 'xxxx',
  8. ...
  9. }
  10. }, "*")
  11. }

附录1:Node.js 代码实现

JWT应用获取 access_token 以及刷新 access_token 示例代码:

  1. const fs = require('fs')
  2. const JWT = require('jsonwebtoken');
  3. const axios = require('axios')
  4. const DOMAIN_ID = '' // 域ID
  5. const APP_ID = '' // 应用ID
  6. const USER_ID = '' // 用户UID
  7. const PRIVATE_KEY_PEM = '' // 私钥,步骤1.3配置的私钥
  8. const PRE = `https://${domain_id}.auth.aliyunpds.com`
  9. async function init() {
  10. try {
  11. //这几个变量需要根据实际情况填写
  12. var params = {
  13. domain_id: DOMAIN_ID,
  14. client_id: APP_ID,
  15. user_id: USER_ID,
  16. privateKeyPEM: PRIVATE_KEY_PEM,
  17. };
  18. var assertion = signAssertion(params)
  19. var obj = await getToken(assertion)
  20. return obj.data
  21. } catch (e) {
  22. if (e.response) {
  23. console.log(e.response.status)
  24. console.log(e.response.headers)
  25. console.log(e.response.data)
  26. } else {
  27. console.error(e)
  28. }
  29. }
  30. }
  31. function signAssertion({ domain_id, client_id, user_id, privateKeyPEM }) {
  32. var now_sec = parseInt(Date.now()/1000)
  33. var opt = {
  34. iss: client_id,
  35. sub: user_id,
  36. sub_type: 'user',
  37. aud: domain_id,
  38. jti: Math.random().toString(36).substring(2),
  39. exp: now_sec + 60,
  40. // iat: now_sec,
  41. // nbf: '',
  42. auto_create: true,
  43. };
  44. return JWT.sign(opt, privateKeyPEM, {
  45. algorithm: 'RS256'
  46. });
  47. }
  48. async function getToken(assertion) {
  49. return await axios({
  50. method: 'post',
  51. url: PRE + '/v2/oauth/token',
  52. //注意:要设置请求的 content-type 为 application/x-www-form-urlencoded
  53. headers: {
  54. 'Content-Type': 'application/x-www-form-urlencoded'
  55. },
  56. //注意:请求参数要放在body里
  57. data: params({
  58. grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
  59. client_id: APP_ID,
  60. assertion
  61. })
  62. })
  63. }
  64. async function refreshToken(refresh_token) {
  65. return await axios({
  66. method: 'post',
  67. url: PRE + '/v2/oauth/token',
  68. //注意:要设置请求的 content-type 为 application/x-www-form-urlencoded
  69. headers: {
  70. 'Content-Type': 'application/x-www-form-urlencoded'
  71. },
  72. //注意:请求参数要放在body里
  73. data: params({
  74. grant_type: 'refresh_token',
  75. client_id: APP_ID,
  76. refresh_token,
  77. })
  78. })
  79. }
  80. function params(m){
  81. const params = new URLSearchParams();
  82. for(var k in m){
  83. params.append(k, m[k]);
  84. }
  85. return params;
  86. }
  87. //调用测试
  88. init().then(aysnc function(result) {
  89. console.log(result) // 返回token对象{access_token:...},对象结构参考附录2
  90. // access_token 失效后
  91. refreshToken(result.refreshToken) // 返回一个新的token对象{access_token:...},对象结构考附录2
  92. })

附录2:token对象结构

示例数据

  1. {
  2. access_token: 'eyJhbG.....g7M0p28',
  3. refresh_token: '62f1acc.......9b781f3',
  4. expires_in: 7200,
  5. token_type: 'Bearer',
  6. ......
  7. }

参数说明请参考OAuth API 说明