Skip to main content

授权(备份)

概述#

授权指认证成功后系统对用户是否能对指定资源执行指定操作的权限控制。

用户能否对指定的资源执行指定的操作取决于:

  • 用户是否拥有执行该操作的权限(privileges),该检查结果取决于用户在系统中的角色

    后面我们会把角色定义为一个组织的成员中拥有一组特定权限的群组,所以这里所说角色是全局的,应称之为用户的类型。

    例如,免费用户、付费用户、管理员可能会用有不同的权限。

    论坛:Level1 用户没有发帖的权限、Level2 以后用户有发帖权限

  • 用户是否拥有对指定资源执行该操作的权限,该检查结果取决于业务逻辑

    例如,用户不能直接更改自己所下订单的应付金额。

    例如,用户不能直接更改自己所下订单的应付金额。

  • 指定的资源是否向该用户开放了执行该操作的权限,该检查结果取决于资源本身许可(permissions)的设置

    论坛:帖子的楼主关闭了帖子,除了楼主的论坛管理员所有其它人都无法查看

对于用户权限的检查,可以通过四个要素来分析:

  • 操作的执行人(即用户),主要关注其所拥有的权限
  • 操作的类型
  • 操作的资源
  • 操作的执行人与资源之间的关系

相关概念#

概念说明
用户(user执行操作的主体(principal
租户(tenant使用系统服务的企业或组织,在电商/支付平台具化为商户(merchant
用户角色(role通常一个系统中的用户会被分成若干个角色,或称之为用户群组(user group),同一角色的用户将用有一组相同的权限
用户权限、特权(privilege用户自身的或通过所属角色所获得的规定了其可以执行哪些操作的信息
资源访问权限许可(permission是资源的许可信息,说明了允许哪些用户执行哪些操作

一个大型的互联网应用可能会被设计为支持多租户,甚至多服务。

例如,淘宝作为电商平台是由众多第三方店铺构成的,而不论消费者还是商户用户,其账号都可以在阿里巴巴的众多服务中(如天猫、阿里云)使用。

此时,角色的作用域可能是商户的所有或部分(当商户存在子组织时)资源。

Alibaba Hierarchy

借鉴文件系统中的访问控制和权限#

参考资料:Wikipedia: File System Permissions

以下是几种常见操作系统中文件访问控制权限的设置示例,体现了为用户、用户群组授予可以对文件执行指定操作的权限的方式。

在类 Unix 系统中,文件访问控制权限针对文件所有者、所有者同组用户、其他用户三类用户对文件的读、写、执行操作进行控制。

Windows 7#

Windows7 Permissions

Mac OS X#

Mac OS X Permissions

Linux#

Ubuntu Permissions

互联网应用的权限控制#

然而,一个互联网应用的权限控制的场景要更复杂,主要体现在:

  • 存在更多的业务角色
  • 角色的作用域可能是有限的(而不是全局的)
  • 操作的类型更多

此时,合理的业务设计和逻辑实现将尤为重要。

如上述文件系统的访问控制的实现,以及 RESTful 定义的以资源为中心的设计风格,有助于合理的设计互联网应用的权限控制。

下面以具体用例进行说明。

用例:电商订单系统#

业务场景#

  • 用户C是消费者,用户S店铺S的店员,用户A是系统管理员
  • 用户C准备购买店铺S商品I,并创建了订单O1,然而,由于信息填写错误,用户C又取消了订单O1
  • 用户C重新下单,创建了订单O2
  • 由于符合商品信息中所描述的优惠条件,经过沟通,用户S同意将订单O2的订单总额下调
  • 用户C支付订单,完成交易
  • 如果由于意外原因导致交易失败而产生脏数据,用户A可以对脏数据进行处理、修复。

实体-关系#

实体实体类型说明实体间的关系
用户A用户管理员,超级用户
用户C用户消费者
用户S用户店员店铺S的成员
店铺S商户
商品I商品
订单O1订单O2订单所有者为用户C,商品所属店铺为店铺S

ER diagram

资源定义#

本例的操作资源为消费者所创建的订单。

消费者通过以下请求创建订单:

POST /users/用户C/orders
{ "items": [ "商品I" ], "shipping_address": "辽宁省 大连市 高新园区" }

消费者通过以下请求取消订单:

DELETE /users/用户C/orders/订单O1

店员通过以下请求对订单改价:

PATCH /stores/店铺S/customer-orders/订单O2
{ "amount_payable": 90 }

经买卖双方沟通,需要取消已支付订单时,管理原可以通过以下请求执行该操作:

DELETE /orders/订单O2

虽然 /users/用户C/orders/订单O2/stores/店铺S/customer-orders/订单O2/orders/订单O2 在业务上是同一个实体,但是在表现层应被定义为多个资源,这样我们便可以清晰地描述用户对订单执行操作时应执行的权限检查的逻辑。

操作接口权限检查逻辑
消费者取消订单DELETE /users/:user_id/orders/:order_id操作者必须为 ID 为 user_id 的用户(即订单的所有者)
店员更新消费者所下订单的总额PATCH /stores/:store_id/customer-orders/:order_id操作者必须为 ID 为 store_id 的店铺的店员(即订单商品所属商户的成员)
管理员取消已支付订单DELETE /orders/:order_id操作者必须为系统管理员

需求扩展#

在实际应用中可能还会存在以下需求:

  • 同一店铺的店员被划分为不同的角色,拥有不同的权限

    例如,一组店员只能维护商品,而另一组店员只能维护客户订单,而店铺管理员拥有所有权限。

  • 特定商品的操作只对部分店员开放

权限检查过滤器设计示例#

现在我们可以开始设计一个权限检查过滤器。

权限检查在服务器接收到客户端请求,完成用户令牌校验后执行,即此时以下信息已被确定:

  • 请求发送者的身份(用户的 ID、用户的类型、加入的组织及在各组织中的角色等信息)
  • 请求操作的资源
'use strict';
const mongoose = require('mongoose');
const ItemModel = mongoose.model('Item');
const OrderModel = mongoose.model('Order');
const intersection = require('lodash/intersection');
/**
* 用户操作权限检查。
* @param {object} user 访问令牌携带的用户信息
* @param {string} user.id 用户 ID
* @param {string} user.type 用户类型,默认为普通用户
* @param {[object]} user.groups 所属群组角色的映射表
* @param {object} permission 资源的访问许可设置
* @param {string} [permission.userType] 用户的类型,设置时,用户类型必须与规定的类型相同
* @param {string} [permission.ownerId] 所有者 ID,设置时,用户 ID 必须与规定的所有者 ID 相同
* @param {string} [permission.groupId] 群组 ID,设置时,用户必须已加入该群组
* @param {[string]} [permission.roles] 限制的角色,设置时,用户必须为规定的角色
* @param {string} permission.resourceType 资源的类型
* @param {string} permission.resourceId 资源的 ID
* @param {string} permission.actionType 操作的类型
*/
exports.checkUserPrivilege = async function(user, permission) {
if (!user) {
throw new Error('尚未登录'); // 401 Unauthorized
}
// 用户已加入组织的 ID 的列表
let userGroupIDs = Object.keys(user.groups);
// 检查内容:
// 1、用户的类型;
// 2、用户是否为资源所有者;
// 3、用户是否为资源所属组织的成员;
// 4、用户是否为资源所属组织的指定角色的成员。
if ((permission.userType && user.type !== permission.userType)
|| (permission.ownerId && user.id !== permission.ownerId)
|| (permission.groupId
&& userGroupIDs.indexOf(permission.groupId) < 0)
|| (permission.groupId
&& permission.roles
&& permission.roles.indexOf(user.groups[permission.groupId]) < 0)) {
throw new Error('无权执行此操作'); // 403 Forbidden
}
// 如果是对某一资源执行操作,则检查该资源是否向当前用户(或用户所属组织)开放了相应操作的权限
if (permission.resourceId) {
let Model = null;
// 取得指定类型资源的数据模型
// 提示:泛化的数据结构设计有助于减少不同数据模型的定义
switch (permission.resourceType) {
case 'item': Model = ItemModel; break;
case 'order': OrderModel = ItemModel; break;
// ……
}
// 取得资源的许可信息
let resource = await Model
.findOne({ id: permission.resourceId })
.select('permissions')
.lean();
if (!resource) {
throw new Error('指定的资源不存在'); // 404 Not Found
}
if (resource.permissions
&& resource.permissions[permission.actionType]
&& intersection(
resource.permissions[permission.actionType],
userGroupIDs.concat(user.id)
).length < 0) {
throw new Error('无权执行此操作'); // 403 Forbidden
}
}
// 通过权限检查
}

配置路由时,我们可以为每一个路由设置一个权限检查的参数,路由器根据客户端请求的路径以及这个权限检查配置参数解构出上述权限检查过滤器的 permission 参数,并将其与访问令牌携带的用户信息一起传递给权限检查过滤器函数。

例如,消费者取消未支付订单接口的路由定义及其权限检查配置参数为:

DELETE /users/:user_id/orders/:order_id
{
ownerId: 'user_id' // 将路径参数 user_id 作为权限检查过滤器函数的 permission.ownerId 参数
}

店员修改订单应付金额接口的路由定义及其权限检查配置参数为:

这里我们增加一个需求:店员在店铺的角色必须为销售员(salesman

PATCH /stores/:store_id/customer-orders/:order_id
{
groupId: 'store_id', // 将路径参数 store_id 作为权限检查过滤器函数的 permission.groupId 参数
roles: [ 'salesman' ] // 用户必须为所属组织的 salesman 角色
}

管理员取消已支付订单的路由定义及其权限检查配置参数为:

DELETE /orders/:order_id
{
userType: 'admin' // 用户的类型必须为管理员
}

下面是店员管理商品的接口的权限设计。

创建商品的路由定义及其权限检查配置参数:

POST /stores/:store_id/items
{
groupId: 'store_id', // 必须为店铺成员
roles: [ 'stock' ] // 必须为库存管理员
}

更新商品的路由定义及其权限检查配置参数:

PUT /stores/:store_id/items/:item_id
{
groupId: 'store_id', // 必须为店铺成员
roles: [ 'stock' ], // 必须为库存管理员
resourceType: 'item', // 操作资源类型为商品
resourceId: 'item_id', // 操作资源 ID 为路径参数中的 item_id
actionType: 'update' // 商品必须已向当前用户开放更新操作的权限
}

总结#

至此,我们实现了以下三个层面的访问控制:

  • 将用户划分为不同的人群,并规定个人群可以使用哪些接口;
  • 根据业务逻辑,判断用户能否对指定的资源执行指定的操作;
  • 根据目标资源自身的访问许可,判断用户能否对该资源执行指定的操作。

访问控制权限检查的业务流程为:

  1. 校验用户令牌,取得用户信息;
  2. 根据路由定义从 HTTP 请求路径中解构路径参数;
  3. 根据权限检查配置,使用路径参数构造权限检查过滤器的参数;
  4. 执行访问权限检查。
    • 用户类型检查;
    • 所有者检查;
    • 用户所属群组检查;
    • 用户所属群组角色检查;
    • 资源访问许可检查。