本文檔主要描述基於 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 介面換取 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 介面換取 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 - 擷取存取權杖。