9 min read

权限设计的一些思路

在许多企业服务网站或者后台系统需要制定各种权限来限制和隔离不同用户对资源的访问和操作

以下一些思路主要描述(我见过)的一些权限场景设计

设计概念

最常见的应用模式是 RBAC (Role-based access control)

基于角色的访问控制机制。并且可以不增加复杂性的套用 MACDAC 的策略。

简单来说,RBAC 将 权限 <—> 用户 之间 加入 角色 这个概念进行关联和解耦,通过将含有不同权限的角色赋予用户,实现权限分配。

如果没有角色的话,用户的权限分配过程应该是这样:

PLACEHOLDER

单独进行每一种权限的配置,操作和管理成本比较高

而利用角色赋予用户权限,则减少了这样操作的成本,且能够大大减轻管理负担,同时将用户与权限解耦,提供更大的灵活性。

image

用户与角色的对应关系可以是 one to one,也可以是 one to many。

当关系是 one to many 时,用户的权限为角色组权限的并集

但这里并没有考虑互斥的情况

  • 角色赋予的是主体,主体可以是用户,也可以是组
  • 角色是权限的集合

RBAC 还具有以下细分的模型:

  1. core RBAC
    • 最常见/简单的 RBAC 模型,我们这里主要讨论的是这一种
  2. hierarchical RBAC, which adds support for inheritance between roles
    • 引入了角色继承(Hierarchical Role)概念,即角色之间具有分级的关系,
    • 比如管理职位权限分级,角色有 总监/组长/员工,上层角色继承下层角色的全部权限,而且可单独赋予更多独立
  3. constrained RBAC, which adds separation of duties
    • 加入了角色的约束控制,包括但不限:互斥角色/基数约束/先决条件角色

我们日常也有很多用到 RBAC 模型的设计,比如 MacOS 的用户组

image

具体实现

权限的规划拆分

为了实现拆分 权限 这个概念,我们首先要明确一般产品的权限 由 页面访问/页面操作/页面数据 构成

而 页面访问 与 页面操作 必定是关联的关系,也就是 页面操作的权限 前置依赖是角色具有 页面访问 的权限

而在大部分场景,我们可以把 页面操作 和 页面数据 作为统一的整体来设计

权限控制的具体实现

相信过程

Step - 1

以 Integer 约定权限,比如我们有一组定义不同成员的权限:

const ROLE_AUTH = {
OWNER: 100,
ADMIN: 90,
ENGINEER: 80,
DESIGNER: 75
GUEST: 30,
};

我们定义 ROLE_AUTH 为权限表的集合,其中有不同权限的角色,权限的划分基于数字。

权限的数值越大表示权利越大且权利是向下覆盖的,也就是 owner 会拥有所有其以下角色的权限。

所以我们在代码中进行权限处理是这个样子:

if (user.permission >= ROLE_AUTH.ENGINEER) {
// do something
}

这种实现方式比较简单,但仔细看会有权限强覆盖的问题,比如各个角色之间是强关联的

ENGINEER 会拥有 DESIGNER 的全部权限

且未来增减改动角色权限都不容易

Step - 2

利用 位运算

Linux 的文件权限 就是利用位运算实现的

在深入之前,我们先假定两个前提

  1. 每种权限对应唯一的码
  2. 权限码为二进制数形式,有且只有一位值为 1,其余全部为 0(2^n

先来上操作方式的总结:

  • |  赋予权限
  • &  校验权限
  • & (~R) 删除权限

Linux 的文件权限分为读、写和执行,有字母和数字等多种表现形式:

Linux 的文件权限

let r = 0b100
let w = 0b010
let x = 0b001
// 给用户赋全部权限(使用前面讲的 | 操作)
let user = r | w | x
console.log(user)
// 7
console.log(user.toString(2))
// 111
// r = 0b100
// w = 0b010
// r = 0b001
// r|w|x = 0b111
// 表明拥有了全部三个权限
// ------------------------------------
let r = 0b100
let w = 0b010
let x = 0b001
// 给用户赋 r w 两个权限
let user = r | w
// user = 6
// user = 0b110 (二进制)
console.log((user & r) === r) // true 有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
// ------------------------------------
let r = 0b100
let w = 0b010
let x = 0b001
let user = 0b110 // 有 r w 两个权限
// 删除 r 权限
user = user & ~r
console.log((user & r) === r) // false 没有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
console.log(user.toString(2)) // 现在 user 是 0b010
// 再执行一次
user = user & ~r
console.log((user & r) === r) // false 没有 r 权限
console.log((user & w) === w) // true 有 w 权限
console.log((user & x) === x) // false 没有 x 权限
console.log(user.toString(2)) // 现在 user 还是 0b010,并不会新增

在用此种实现方式,可以注意到,用户/角色/权限 之间的对应关系减少了,进制转换的方法则可以省略对应关系表,减少查询,节省空间

适合应用极其多,但用户和角色比较少的场景

位运算的可读性不高,增加后期可维护性

Step - 3

最后的方案

我们想实现一个角色之间是没有关联的权限系统,也就是说,角色的权限可以有交集也可以有差异,并且支持自定义配置。

实现的方式是设计一张权限表,那么如何设计呢?

PLACEHOLDER

上图很直观的描述了权限归原的方式

我们来介绍一下 如何定义 Permission

Function

表示一个模块或者一个功能

{
function: 'WEBSITE', // 网站 模块
}

Action

表示一个动作或者一个操作

{
action: 'View', // View / Create / Update / Delete
}

Permission

所以最终的权限 由 FunctionAction 组合而成

Permission = Function + Action

// 代表拥有 网站访问 的权限
{
function: 'WEBSITE',
action: 'View',
}

注意不要使用 Anti-patterns

// 访问网站应当是 Permission = 'WEBSITE' + 'View'
{
function: 'VIEW_WEBSITE'
}

这样我们就可以定义许多有独立作用域的具体权限了

接下来我们就可以定义 Role

Role 表示一个 角色 或者 用户组

还可以多定义一个 RolePermission 表示一个 Role 具有哪些 Permission

我们可以为自定义 RolePermission 所包含的哪些Permission,并且将 RolePermission 集合 赋予 Role

所以只要去获取当前 Role 对应的 RolePermission 集合中具有哪些 Permission ,然后去匹配各个页面模块或功能具有的权限配置。

这样我们就实现了满足需求的简单权限系统。

Reference