跳到正文

授权 ​

在前面的部分我们分别介绍了认证 和权限管理,认证是识别请求方身份的过程,权限管理则是决定谁具备哪些操作权限的过程,在确定了用户身份以及该用户具备的权限之后,接下来我们要做的就是安全地授予用户权限。

授权的含义 ​

在通用领域内,授权是领导者通过为员工和下属提供更多的自主权,以达到组织目标的过程。

在计算机领域内,授权是由信息系统指定批准机构授予某实体处理、存储或传送信息的权力。

而在身份认证领域内,授权是指当客户端经过身份认证后,能够有限的访问服务端资源的一种机制。

为什么要进行「授权」? ​

在已经构建起的用户系统中,当你的 API 需要判断当前访问用户是否能访问当前资源时,就需要你构建自己的权限系统了。授权是权限系统中一个很重要的概念,是指判断用户具备哪些权限的过程,这与认证完全不同。

对于企业来说,授权能够明确组织成员之间的关系,使职责和边界变得更加清晰,方便公司管理;同时,授权能够保障数据安全、防控风险,不同的权限准许不同的操作,可防止用户人为破坏、数据泄漏、误操作等事故的发生;授权能够提高决策的效率,优秀的授权和权限管理使系统更易操作,使员工的工作效率得到提升。

而从产品角度出发,授权可以保障产品系统的使用安全和数据安全,防止违规操作和数据泄漏;授权也可以提高系统的可操作性,提升用户体验;此外,好的授权功能会提升产品价值,使其在市场上更具有竞争力。

授权模式 ​

授权模式主要为两种,分别是通过基于 OIDC 流程中的授权码模式,以及通过 API 接口到授权中心对用户授权进行集中验证。

基于 OIDC 框架的授权模式 ​

OIDC 框架是一种安全、轻量、标准的授权体系,用于帮助资源方、调用方、资源所有者之间的完成授权流程。如果授权过程中不涉及到资源所有者,可以使用 client_credentials 模式。这种模式一般用于后端服务器的 M2M 模式。你可以在应用详情页获取创建编程访问账号,获取一对 AK 和 SK。你需要将其交给调用方。

你可以使用 OIDC 的 client_credentials 模式请求具备特定 scope 权限的 access_token:

shell
curl --request POST \
  --url https://${YOUR_AUTHING_DOMAIN}/oidc/token \
  --header 'accept: application/json' \
  --header 'cache-control: no-cache' \
  --header 'content-type: application/x-www-form-urlencoded' \
  --data 'grant_type=client_credentials&scope=customScope&client_id=AK&client_secret=SK'

GenAuth 会根据调用方请求的资源和上下文环境,动态的决定颁发具备哪些权限的 AccessToken。并返回被拒绝的 scope:

json
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3599,
  "scope": "user",
  "scope_rejected": "xxx yyy"
}

其中 scope 为该 access_token 具备的权限列表,用空格分割。你可以在后端通过 scope 判断用户具备哪些权限。

当授权流程中涉及到需要资源所有者参与授权时,可以使用 OIDC 框架中的授权码模式。你需要将权限项目放在发起授权的链接的 scope 参数中,例如:

sh
https://${YOUR_AUTHING_DOMAIN}/oidc/auth?client_id={编程访问账号 AK}&scope=openid book:read book:delete&redirect_uri={你的业务回调地址}&state={随机字符串}&response_type=code

需要让资源所有者点击链接,之后会转到登录页面,资源持有者认证自己的身份,并将资源授权授权给调用方。 完成认证授权后,浏览器将跳转到业务回调地址,并通过 URL 传递 code 授权码。调用者可以使用这个 code 授权码到 GenAuth 换取一个具备权限的 AccessToken,用于获取资源方的资源。

Code 换取 Token 的代码如下:

sh
curl --request POST \
  --url https://${YOUR_AUTHING_DOMAIN}/oidc/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data client_id={编程访问账号 AK} \
  --data client_secret={编程访问账号 SK} \
  --data grant_type=authorization_code \
  --data redirect_uri={回调地址} \
  --data code={授权码}

同样,GenAuth 会根据调用方请求的资源和上下文环境,动态的决定颁发具备哪些权限的 AccessToken。被拒绝的权限不会出现在 scope 中:

json
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3599,
  "scope": "openid book:read",
}

当然,资源方必须在返回资源前,验证调用者是否携带了具备权限的 AccessToken,当一切检验通过,就可以安全地返回资源。

使用权限 API ​

除了使用 OIDC 的 client_credentials 模式,还可以使用通用的权限 API,通过权限 API 创建角色、给角色授权角色、判断用户是否具备某个权限等。我们支持 Node.js、Python、Java、PHP、C# 等语言的 SDK,点此查看详情。

用户许可的资源授权 ​

假如你的公司是一家做社交通讯业务的公司,现在有另外一家公司想通过调用你的业务 API 开发一个聊天记录整理导出的工具,并且已经和你的公司签约合作。现在你想要安全地将用户信息授权给这家公司,你期望:

  1. API 的调用只开放给合作伙伴公司。
  2. 不同的合作伙伴拥有的访问权限不同,能够访问的业务 API 也不同。
  3. 合作伙伴公司从业务 API 获取自己公司的用户数据之前,必须先征得用户的同意。
  4. 如果将来终止合作,或者发生变化,希望能够收回某些数据的权限或者完全禁用。

权限管理与分配 ​

首先在 GenAuth 创建两个用户。分别为 user1@123.com 和 user2@123.com。

在 GenAuth 创建一个应用,假设我们的社交软件叫做「蒸汽聊天」,那么应用名字就叫做「蒸汽聊天」。

在应用详情,点击访问授权选项卡,切换到数据资源 tab,然后点击添加。

API 资源、数据资源、UI 资源在本质上没有区别,类型仅用于管理层面上的区分,创建良好的资源分类能够方便管理员快速聚焦不同的资源。

我们创建一个聊天数据资源,定义增删改查几个操作,最后点击保存。

然后在资源授权中添加授权规则。

被授权主体选择 user1@123.com 和 user2@123.com,资源类型选择聊天数据,然后点击确定。

到此管理员进行权限管理的操作就全部结束了。

然后我们创建一个编程访问账号,将来会交给调用方。

如果将编程访问账号删除,调用方将会失去获取用户授权的能力。

AccessToken 过期时间 ​

当你创建编程访问账号时,需要指定 AccessToken 过期时间。GenAuth 在颁发 AccessToken 时使用 RS256 签名算法进行签名,以确保 AccessToken 不会被篡改。

Token 签名是 JWT 中的一部分,更多内容请参考 JWT 释义及使用。

RS256 是一种非对称签名算法,GenAuth 持有私钥对 Token 进行签名,JWT 的消费者使用公钥来验证签名。RS256 签名算法,有以下好处:

  1. 任何人都可以使用应用公钥验证签名,签名方一定是 GenAuth。
  2. 无私钥泄露风险,如果你使用 HS256 但泄露了应用密钥,需要刷新密钥并重新部署所有 API。 关于签名问题更多内容请参考验证 Token。

展示确权页面 ​

当 业务软件 和 身份数据 不是被同一方掌握的时候,作为身份提供商,你需要向用户展示 确权 页面。在本例中,你的公司掌握身份数据,你的合作伙伴公司掌握业务软件,所以在他们获取用户数据时,你作为身份提供商有义务告知终端用户其他公司需要获取他们的哪些用户数据(例如手机号、邮箱)以及哪些资源权限。

你可以在控制台 > 应用 > 应用详情 >「高级配置」选项卡,开启应用授权页面。

获取具备权限的 AccessToken ​

调用方需要通过 OIDC 授权码模式从资源方获得资源授权。资源方的用户会参与到授权过程,经过用户的授权后,GenAuth 会签发具备权限 scope 且主体为资源持有者的 AccessToken。 首先需要拼接授权链接:

http
https://{应用域名}.authing.cn/oidc/auth?client_id={应用ID}&response_type=code&scope=openid email message&redirect_uri={调用方业务地址}&state={随机字符串}

其中的 scope 参数中可以填写上面步骤中定义的资源以及相应操作,具体格式如下。

Scope 权限项目规范 ​

GenAuth 的 scope 权限项目以空格分隔,每一项的格式是资源标识符:资源操作。

以下是 GenAuth 支持的所有 scope 格式:

book:1:read 含义为编号为 1 的书籍资源的读取权限

book:*:read 含义为所有书籍资源的读取权限

book:read 含义为所有书籍资源的读取权限

book:*:* 含义为所有书籍资源的所有操作权限

book:* 含义为所有书籍资源的所有操作权限

book 含义为所有书籍资源的所有操作权限

*:*:* 含义为所有资源的所有操作权限

*:* 含义为所有资源的所有操作权限

* 含义为所有资源的所有操作权限

例如上面定义了 message 资源和 message 资源的 create 操作,这里的 scope 中可以填写 message:create 内容。

调用方应该引导用户点击此链接。用户点击后会跳转到认证页面。

用户完成登录后,会跳转到调用方的业务地址。并在 URL 中携带授权码 code 参数。

接下来需要使用授权码 code 和编程访问账号的 Key 和 Secret,换取用户的 AccessToken 和 IdToken。有关 OIDC 授权码模式的更多信息请查看文档。

可以看到用户的 AccessToken 中具备 message 权限 scope。token 的受众(aud)是编程访问账号 Key。AccessToken 的含义是:调用方 aud 具备资源所有者 sub 的 scope 权限,颁发者是 iss。资源方可以根据 AccessToken 中的信息进行权限校验。

添加 API 鉴权拦截器 ​

在 GenAuth 定义了 API 之后,你需要在你的实际业务 API 接口增加 API 鉴权拦截器,对于受保护的资源,只放行携带了合法的 AccessToken 且具备所需权限的来访者。 代码示例如下:

javascript
var express = require('express');
var app = express();
var jwt = require('express-jwt');
var jwks = require('jwks-rsa');
var port = process.env.PORT || 8080;
var jwtCheck = jwt({
  secret: jwks.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'https://{应用域名}.authing.cn/oidc/.well-known/jwks.json',
  }),
  audience: '{编程访问账号 ID}',
  issuer: 'https://{应用域名}.authing.cn/oidc',
  algorithms: ['RS256'],
});
// 检验 AccessToken 合法性
app.use(jwtCheck);

app.post('/article', function(req, res) {
  // 检验 AccessToken 是否具备所需要的权限项目
  if (!req.user.scope.split(' ').incldues('write:article')) {
    return res.status(401).json({ code: 401, message: 'Unauthorized' });
  }
  res.send('Secured Resource');
});

app.listen(port);

有关 Token 检验的其他内容请参考验证 Token。

常见问题 ​

用户拒绝授权意味着什么? ​

如果你开启了授权页面,而用户在授权页面点击了「拒绝」。那么认证授权请求的发起方(在本例是你的合作伙伴公司)无法获取到该用户的任何用户信息以及 Token。

用户拒绝授权之后,我应该怎么做? ​

如果你是应用软件的开发者,你应该引导用户点击「允许」,并且告知用户不会滥用他的的数据和权限。如果用户点击了「拒绝」,GenAuth 会将浏览器重定向到应用的业务回调地址(和成功时的是同一个),通过 URL query 携带 error 和 error_description 参数。如果你的后端接收到 error 参数,你可以给用户展示一个登录失败的友好页面,并引导用户重新发起认证授权。

处理用户拒绝授权的示例代码如下:

js
app.get('/oidc/callback', async (req, res) => {
  if (req.query.error) {
      // 你可以记录日志
      console.log('用户取消授权,登录失败');
      // 你可以渲染失败页面,告知用户点击允许授权按钮,引导用户重新发起登录
      res.render('login-error', { error: req.query.error, error_description: req.query.error_description });
  } else {
      // ...正常登录逻辑...
  }
});

如果你是身份提供商,负责管理用户数据,当用户拒绝授权后,你无需做任何事情。

M2M 授权 ​

M2M(Machine to Machine)授权是 无用户参与 的应用间授权。当你想要将自己的业务 API 部分地开放给其他人(例如你的外包商),外包商需要先进行 M2M 授权,然后才能访问你的业务 API。假如你的公司希望开发一些数据的大屏展示,并有几个外包商参与其中。你希望将某些非核心数据的 API 访问权限授权给外包商,让外包商完成这部分的非核心开发。此时需要 M2M 授权,因为这个过程中不需要用户参与,我们只需要确定来访者是哪个外包商,以及他有哪些接口的访问权限。

以下是该场景的架构图,外包商先到 GenAuth 获取 Access Token,然后携带 Access Token 访问公司服务的 API 接口:

权限管理与分配 ​

在 GenAuth 创建一个应用,叫做「大屏展示」。

在「大屏展示」应用下(访问授权->API资源)定义一些资源,每个资源对应「大屏展示」应用中实际的资源。这里我们添加一些资源,包括用户增长(user-growth)、客户(customer)、公告(announce)、营收(revenue)。这些资源的名称就是 API scope。

定义完资源和操作之后,接下来为应用添加 编程访问账号,编程访问账号 就是当前应用 API 接口的 调用方。编程访问账号 有一对 AK 和 SK,用于交给外包商调用「大屏展示」应用接口。我们可以将具备不同权限的 AK、SK 交给不同的外包商,这样他们就有不同的权限,能够访问不同的 API。

创建两个编程访问账号,填写 AccessToken 过期时间和备注信息,点击确定。

如果将编程访问账号删除,调用方将会失去获取用户授权的能力。

AccessToken 过期时间 ​

当你创建编程访问账号时,需要指定 AccessToken 过期时间。GenAuth 在颁发 AccessToken 时使用 RS256 签名算法进行签名,以确保 AccessToken 不会被篡改。

Token 签名是 JWT 中的一部分,更多内容请参考 JWT 释义及使用。

RS256 是一种非对称签名算法,GenAuth 持有私钥对 Token 进行签名,JWT 的消费者使用公钥来验证签名。RS256 签名算法,有以下好处:

  1. 任何人都可以使用应用公钥验证签名,签名方一定是 GenAuth。
  2. 无私钥泄露风险,如果你使用 HS256 但泄露了应用密钥,需要刷新密钥并重新部署所有 API。

关于签名问题更多内容请参考验证 Token。

我们刚刚创建了两个编程访问账号,将来需要交给外包商。

下面我们需要赋予他们资源权限。在资源授权选项卡,点击添加。

被授权主体类型选择编程访问账号,然后选择甲外包公司的编程访问账号账号。

授权规则中,资源类型选择公告信息,资源标识符填写 * 代表授权所有公告资源,操作选择特定操作,然后选择 announce:read 操作。最后点击确定。这条规则的作用是:将所有公告信息资源的读取权限授权给甲外包公司。

接下来我们为乙外包商添加授权,首先选择乙外包商的编程访问账号。

接下来,我们要添加三个规则:

  1. 将 2019 年的用户增长数据所有操作权限授权给乙外包商。点击右上方的添加授权规则可以添加多条规则。

  1. 将所有营收记录的创建、读取、修改权限授权给乙外包商。

  1. 将所有客户记录的读取权限授权给乙外包商。

到此管理员进行权限管理的操作就全部结束了,下面我们从调用方和资源方的角度进行 M2M 授权最佳实践。

获取具备权限的 AccessToken ​

OIDC 授权框架提供了许多种授权模式。在本场景中,获取用户的增长信息属于 M2M(机器对机器)授权,没有用户的参与,调用方以自己的身份去访问资源服务器的 API 接口,这里需要使用 OIDC ClientCredentials 模式。

通过 OIDC ClientCredentials 授权模式,调用方需要向 GenAuth 提供他的 ClientCredentials(也就是编程访问账号的 Key 和 Secret)和需要请求的权限 scope(也就是资源标识符)来直接获得一个具有该 API 权限的 AccessToken。

  1. 调用方发送编程访问账号的 Key、Secret 和需要请求的权限项目 scope 到 GenAuth。
  2. GenAuth 验证编程访问账号 Key 和 Secret。
  3. GenAuth 根据管理员配置的权限规则校验 scope 权限项目,签发一个具备访问资源权限的 AccessToken,被拒绝的权限 scope 不会出现在 AccessToken 里。
  4. 调用方携带 AccessToken 访问资源服务器。
  5. 资源服务器返回受保护资源。

调用方为了能够访问受保护的 API 接口,必须先获取一个具备权限的 AccessToken。为此,调用方需要向以下地址发送 POST 请求。

请求地址:https://{应用域名}.authing.cn/oidc/token

参数说明:

参数名描述
grant_type填写 client_credentials。
client_id编程访问账号 Key。
client_secret编程访问账号 Secret。
scope请求的权限项目,每个权限项目的格式为 资源标识符:操作 以空格分隔。

响应结果:

json
{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjF6aXlIVG15M184MDRDOU1jUENHVERmYWJCNThBNENlZG9Wa3VweXdVeU0ifQ.eyJqdGkiOiJ2S1ZGV3FKemltTm5MSTlYZy0zam0iLCJpYXQiOjE2MTI1MDA2OTgsImV4cCI6MTYxMjUwNDI5OCwic2NvcGUiOiJib29rIiwiaXNzIjoiaHR0cHM6Ly9zdGVhbS10YWxrLmF1dGhpbmcuY24vb2lkYyIsImF1ZCI6IjYwMWJmMzVhY2E1ZDM4NzVjNDY3NDgyYyIsImF6cCI6IjYwMTkzYzYxMGY5MTE3ZTdjYjA0OTE1OSJ9.DS0l6zdlr_bGLqmDQRxvHUL4fmyLS5je6bqUCSSo06OIWSfcDZMZAqH5aYXP7Hzm4SiT6sfOCP_IiPSOxJPgFPYAmQTPSvJ5e6zs9jNeZyep_O6NWjlOGbDirskZE1pSZO_16ceiFr3jprSp13ff6O6Fa9YkY-8b_L3ouDqKhtb_4051pWZif-VzgXSkmvflTmqauJul9b5PzaeGWL-PKOrHrUiHjJwf9wqtR-3C8voFmi9pmxrUJYGSJoxwcxxSEceUY3d9oJU3v7e6FOnT_EMxfQCrAgzXR21bOitsAutOVXg1N9H0QJiNBESorCcj6yi1fVePTeDI5nY6xj9oDw",
  "expires_in": 3600,
  "token_type": "Bearer",
  "scope": "book",
  "rejected_scope": "message table"
}

示例代码:

js
const axios = require('axios').default;
const options = {
  method: 'POST',
  url: 'https://{应用域名}.authing.cn/oidc/token',
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
  data: {
    grant_type: 'client_credentials',
    client_id: '{编程访问账号 Key}',
    client_secret: '{编程访问账号 Secret}',
    scope: '{权限项目,空格分隔}',
  },
};

axios
  .request(options)
  .then(function(response) {
    console.log(response.data);
  })
  .catch(function(error) {
    console.error(error);
  });

我们只将公告信息的读取权限授权给了 A 外包公司,如果 A 外包公司请求授权时,携带了其他 scope,例如:announce:read announce:update revenue:read customer user-growth:read 。GenAuth 会拒绝掉除了 announce:read 的所有权限。以下是 A 外包公司请求授权时的返回结果。被拒绝的权限在 rejected_scope 中。

AccessToken 的信息中包含权限 scope:

我们再来看 B 外包商的授权,如果 B 外包商想请求以下 scope:user-growth:2020:read user-growth:2019:* user-growth:2019:read revenue:create revenue:*:read customer:read

GenAuth 的返回结果如下:

需要注意的是,管理员只授权了 2019 年用户增长数据的所有权限给 B 外包公司,所以当请求 2020 年的用户增长数据的 scope 被拒绝。

Scope 权限项目规范 ​

GenAuth 的 scope 权限项目以空格分隔,每一项的格式是资源标识符:资源操作。

以下是 GenAuth 支持的所有 scope 格式:

book:1:read 含义为编号为 1 的书籍资源的读取权限

book:*:read 含义为所有书籍资源的读取权限

book:read 含义为所有书籍资源的读取权限

book:*:* 含义为所有书籍资源的所有操作权限

book:* 含义为所有书籍资源的所有操作权限

book 含义为所有书籍资源的所有操作权限

*:*:* 含义为所有资源的所有操作权限

*:* 含义为所有资源的所有操作权限

* 含义为所有资源的所有操作权限

添加 API 鉴权拦截器 ​

在 GenAuth 定义了 API 之后,你需要在你的实际业务 API 接口增加 API 鉴权拦截器,对于受保护的资源,只放行携带了合法的 AccessToken 且具备所需权限的来访者。 代码示例如下:

javascript
var express = require('express');
var app = express();
var jwt = require('express-jwt');
var jwks = require('jwks-rsa');
var port = process.env.PORT || 8080;
var jwtCheck = jwt({
  secret: jwks.expressJwtSecret({
    cache: true,
    rateLimit: true,
    jwksRequestsPerMinute: 5,
    jwksUri: 'https://{应用域名}.authing.cn/oidc/.well-known/jwks.json',
  }),
  audience: '{编程访问账号 ID}',
  issuer: 'https://{应用域名}.authing.cn/oidc',
  algorithms: ['RS256'],
});
// 检验 AccessToken 合法性
app.use(jwtCheck);

app.post('/article', function(req, res) {
  // 检验 AccessToken 是否具备所需要的权限项目
  if (!req.user.scope.split(' ').incldues('write:article')) {
    return res.status(401).json({ code: 401, message: 'Unauthorized' });
  }
  res.send('Secured Resource');
});

app.listen(port);

有关 Token 检验的其他内容请参考验证 Token。