本文档主要描述基于 JWT 应用如何获得网盘与相册服务PDS授权访问凭证access_token。
JWT 应用介绍
本文档中的JWT应用是指使用 JWT(JSON Web Token)机制进行身份认证的自定义应用。
JWT应用可以在服务端通过私钥对数据进行签名得到一个JWT字符串,该JWT字符串作为访问已经配置了公钥的服务端的凭证。
适用场景
企业已有内部的软件系统,包含独立的账号体系,想通过内部的登录页面登录,然后使用PDS的功能。
企业已有独立的账号体系和登录入口,想要使用已有的登录入口结合 PDS 搭建一套已有独立账号的云存储系统。
接入步骤概览
在 PDS 控制台创建自定义域和JWT应用。
利用RSA算法创建一对公私钥,将公钥保存到PDS服务端,私钥保存到JWT应用服务端。
JWT应用服务端将数据进行编码并用私钥进行签名生成JWT Assertion字符串,然后发送给PDS服务端。
PDS服务端使用公钥验证 JWT Assertion 字符串合法后,返回 AccessToken 给JWT应用服务端,JWT应用服务端可以通过 AccessToken 来调用PDS服务端提供的API。
详细步骤
1 配置密钥
1.1 创建或选择域
1.2 创建或选择应用
进入域详情,在应用列表界面,创建(选择)一个应用:
1.3 设置公钥
创建(选择)应用后,点击”设置公钥”:
生成公私钥:
生成公私钥后,记得复制私钥,自己保存。然后点确定即可。
2 获取ACCESS_TOKEN
2.1 应用服务端计算JWT字符串
将待签名的数据进行编码,并使用私钥通过指定的加密算法对其进行签名,生成JWT字符串。下面是Node.js 的参考代码:
const JWT = require('jsonwebtoken');
function signAssertion({ domain_id, client_id, user_id, privateKeyPEM }) {
var now_sec = parseInt(Date.now() / 1000);
var opt = {
iss: client_id,
sub: user_id,
sub_type: "user",
aud: domain_id,
jti: Math.random().toString(36).substring(2),
exp: now_sec + 60,
// iat: now_sec,
// nbf: '',
auto_create: false,
};
return JWT.sign(opt, privateKeyPEM, {
algorithm: "RS256",
});
}
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,单位秒,生效时间和过期时间不能超过15分钟。为防止客户端和服务器时间不一致,此时间建议设置为当前时间加5分钟。 |
iat | 可选 | Integer | 签发时间,Unix Time,单位秒,在此时间之前无法使用,如:1577682075 |
nbf | 可选 | Integer | 生效时间,Unix Time,单位秒,不指定则默认为当前时间。生效时间和过期时间不能超过15分钟。 为防止客户端和服务器时间不一致,此时间建议设置为当前时间减5分钟,或者不设置。 |
auto_create(扩展字段) | 可选 | Boolean | 如果用户不存在,则自动创建,默认不创建用户。 |
更多关于JWT的三方库和计算方法请参考JWT官网。
2.2 通过JWT字符串换取的access_token
调用Authorize - OAuth请求授权换取access_token:
POST /v2/oauth/token
Content-Type: application/x-www-form-urlencoded
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 | 申请授权的类型,此处应为字符串常量: |
client_id | 必选 | String | 应用ID |
assertion | 必选 | String | 上一步骤计算出来的JWT |
返回 token json 样例:
{
"access_token": "eyJh****eQdnUTsEk4",
"refresh_token": "kL***Lt",
"expires_in": 7200,
"token_type": "Bearer"
}
应用服务端拿到 access_token 后返回给应用Web端,调用PDS API的时候带上 access_token 就可以访问用户在PDS 上的资源。
2.3 更新access_token
通过JWT方式获取的access_token的有效期只有2小时,超过2小时后access_token将过期,过期后可以再次执行步骤2.1和2.2的方法来获取一个新的access_token。 还有一种方法是在7天内可以调用PDS API通过过期的access_token来获取新的access_token,7天后需要重新按照步骤2.1和2.2获取access_token。
调用Authorize - OAuth请求授权换取access_token的请求内容如下:
POST /v2/oauth/token
Content-Type: application/x-www-form-urlencoded
client_id=${APPID}&refresh_token=${access_token}&grant_type=refresh_token&redirect_uri=${REDIRECT_URI}
字段名 | 是否必选 | 类型 | 描述 |
client_id | 必选 | String | 应用ID |
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过去即可。
示例代码:
const endpoint = `https://${domain_id}.apps.aliyunpds.com`
const url = `${endpoint}/accesstoken?origin=${location.origin}`
var win = window.open(url)
window.addEventListener('message', onMessage, false)
async function onMessage(e) {
if (e.data.code == 'token' && e.data.message == 'ready') {
var result = await getToken();// 从服务端获取 AccessToken
//result = {"access_token": ...}
win.postMessage({
code: 'token',
message: result
}, endpoint || '*')
window.removeEventListener('message', onMessage)
}
}
方法2:
使用 iframe 嵌入 basic ui,postMessage 传递 AccessToken 过去即可。
示例代码:
const endpoint = `https://${domain_id}.apps.aliyunpds.com`
//iframe嵌入URL构成:
const iframeURL = `${endponit}/accesstoken?origin=${location.origin}`
html代码:
//注意替换变量iframeURL
<iframe id="ifr" src="iframeURL"></iframe>
window.addEventListener('message', onMessage)
async function onMessage(e) {
if (e.data.code == 'token' && e.data.message == 'ready') {
var result = await getToken();// 从服务端获取 AccessToken
//result = {"access_token": ......}
document.getElementById('ifr').contentWindow.postMessage({
code: 'token',
message: result
}, endpoint || '*')
window.removeEventListener('message', onMessage)
}
}
注意:使用方法2,还需要在basic ui中配置这个安全设置,把宿主页的origin配置上
假设宿主页为 https://example.com/a.html
, origin为 https://example.com
, 这里配置example.com
即可。
方法3:
BasicUI 通过 iframe 嵌入自定义登录页面。
在系统配置中,配置自定义登录页面的 url,和 jwt 的APPID(让 BasicUI 自动刷新token):
用户登录时,不在打开BasicUI的默认登录页面,而是iframe嵌入的打开自定义登录页面。
登录成功后, 通过postMessage 向宿主页传递 token:
if(parent!=self){
let origin = ''
parent.postMessage({
code: 'token',
message: {
access_token: 'xxxx',
refresh_token: 'xxxx',
...
}
}, endpoint || "*")
}
附录1:Node.js 代码实现
JWT应用获取 access_token 以及刷新 access_token 示例代码:
const fs = require('fs')
const JWT = require('jsonwebtoken');
const axios = require('axios')
const DOMAIN_ID = '' // 域ID
const APP_ID = '' // 应用ID
const USER_ID = '' // 用户UID
const PRIVATE_KEY_PEM = '' // 私钥,步骤1.3配置的私钥
const PRE = `https://${domain_id}.api.aliyunpds.com`
async function init() {
try {
//这几个变量需要根据实际情况填写
var params = {
domain_id: DOMAIN_ID,
client_id: APP_ID,
user_id: USER_ID,
privateKeyPEM: PRIVATE_KEY_PEM,
};
var assertion = signAssertion(params)
var obj = await getToken(assertion)
return obj.data
} catch (e) {
if (e.response) {
console.log(e.response.status)
console.log(e.response.headers)
console.log(e.response.data)
} else {
console.error(e)
}
}
}
function signAssertion({ domain_id, client_id, user_id, privateKeyPEM }) {
var now_sec = parseInt(Date.now()/1000)
var opt = {
iss: client_id,
sub: user_id,
sub_type: 'user',
aud: domain_id,
jti: Math.random().toString(36).substring(2),
exp: now_sec + 300,
// iat: now_sec,
// nbf: '',
auto_create: true,
};
return JWT.sign(opt, privateKeyPEM, {
algorithm: 'RS256'
});
}
async function getToken(assertion) {
return await axios({
method: 'post',
url: PRE + '/v2/oauth/token',
//注意:要设置请求的 content-type 为 application/x-www-form-urlencoded
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
//注意:请求参数要放在body里
data: params({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
client_id: APP_ID,
assertion
})
})
}
async function refreshToken(refresh_token) {
return await axios({
method: 'post',
url: PRE + '/v2/oauth/token',
//注意:要设置请求的 content-type 为 application/x-www-form-urlencoded
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
//注意:请求参数要放在body里
data: params({
grant_type: 'refresh_token',
client_id: APP_ID,
refresh_token,
})
})
}
function params(m){
const params = new URLSearchParams();
for(var k in m){
params.append(k, m[k]);
}
return params;
}
//调用测试
;(async ()=>{
let result = await init()
console.log(result) // 返回token对象{access_token:...},对象结构参考附录2
// access_token 失效后
refreshToken(result.refreshToken) // 返回一个新的token对象{access_token:...},对象结构考附录2
})();
附录2:token对象结构
示例数据
{
access_token: 'eyJhbG.....g7M0p28',
refresh_token: '62f1acc.......9b781f3',
expires_in: 7200,
token_type: 'Bearer',
......
}
参数说明请参考Token - 获取访问令牌。