权限设计的一些思路
在许多企业服务网站或者后台系统需要制定各种权限来限制和隔离不同用户对资源的访问和操作
以下一些思路主要描述(我见过)的一些权限场景设计
设计概念
最常见的应用模式是 RBAC (Role-based access control)
基于角色的访问控制机制。并且可以不增加复杂性的套用 MAC 和 DAC 的策略。
简单来说,RBAC 将 权限 <—> 用户 之间 加入 角色 这个概念进行关联和解耦,通过将含有不同权限的角色赋予用户,实现权限分配。
如果没有角色的话,用户的权限分配过程应该是这样:
PLACEHOLDER
单独进行每一种权限的配置,操作和管理成本比较高
而利用角色赋予用户权限,则减少了这样操作的成本,且能够大大减轻管理负担,同时将用户与权限解耦,提供更大的灵活性。
用户与角色的对应关系可以是 one to one,也可以是 one to many。
当关系是 one to many 时,用户的权限为角色组权限的并集
但这里并没有考虑互斥的情况
- 角色赋予的是主体,主体可以是用户,也可以是组
- 角色是权限的集合
RBAC 还具有以下细分的模型:
- core RBAC
- 最常见/简单的 RBAC 模型,我们这里主要讨论的是这一种
- hierarchical RBAC, which adds support for inheritance between roles
- 引入了角色继承(Hierarchical Role)概念,即角色之间具有分级的关系,
- 比如管理职位权限分级,角色有 总监/组长/员工,上层角色继承下层角色的全部权限,而且可单独赋予更多独立
- constrained RBAC, which adds separation of duties
- 加入了角色的约束控制,包括但不限:互斥角色/基数约束/先决条件角色
我们日常也有很多用到 RBAC 模型的设计,比如 MacOS 的用户组
具体实现
权限的规划拆分
为了实现拆分 权限 这个概念,我们首先要明确一般产品的权限 由 页面访问/页面操作/页面数据 构成
而 页面访问 与 页面操作 必定是关联的关系,也就是 页面操作的权限 前置依赖是角色具有 页面访问 的权限
而在大部分场景,我们可以把 页面操作 和 页面数据 作为统一的整体来设计
权限控制的具体实现
相信过程
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,其余全部为 0(
2^n
)
先来上操作方式的总结:
|
赋予权限&
校验权限& (~R)
删除权限
Linux 的文件权限分为读、写和执行,有字母和数字等多种表现形式:
let r = 0b100let w = 0b010let 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 = 0b100let w = 0b010let 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 = 0b100let w = 0b010let x = 0b001let 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
所以最终的权限 由 Function
与 Action
组合而成
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
,然后去匹配各个页面模块或功能具有的权限配置。
这样我们就实现了满足需求的简单权限系统。