JWT应用接入

本文档主要描述基于 JWT 应用如何获得网盘与相册服务PDS授权访问凭证access_token。

JWT 应用介绍

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

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

image

适用场景

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

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

接入步骤概览

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

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

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

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

详细步骤

1 配置密钥

1.1 创建或选择域

a1

1.2 创建或选择应用

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

k1

1.3 设置公钥

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

k3

生成公私钥:k5

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

k4

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

申请授权的类型,此处应为字符串常量:urn:ietf:params:oauth:grant-type:jwt-bearer

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 即可。

image

方法3:

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

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

image

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

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

image
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 - 获取访问令牌