基于规则的权限系统
Last updated on January 12, 2025 pm
提到鉴权,我们往往会联想到基于 Cookie-Session
/ Access Token
/ JWT
/ Siganature
的方式,然而这些方式的解决的问题是在无状态的系统中进行用户身份识别从而达到有状态的目的的。
然而在实际的业务场景中,我们还有一种更为复杂的权限需求,比如:
- A 用户可以查看所有用户的信息,B 则只能查看自己的信息
- A 有权修改系统内的数据,B 则只能查看
这类场景更偏向 「鉴权」 而非 「认证」,因为它们更多的是在描述用户在系统中的行为权限而非身份。所以你可以称之为 Access Control
or Permission Control
。
而在鉴权的场景下,一般有两种最佳实践:
- 基于角色的权限控制(RBAC, Role-based access control)
- 基于属性的权限控制(ABAC, Attribute-based access control)
RBAC
RBAC 是一种基于角色的权限控制,它的核心思想是将用户分配到不同的角色,而角色拥有不同的权限。这种方式的优势在于简单易懂,易于维护,适用于中小型系统。
比如我们可以定义三种角色:
- Admin
- User
- Guest
对于不同的角色,我们可以定义不同的权限:
- Admin: CRUD
- User: CR
- Guest: R
最后,我们赋予每个 User 不同的 Role:
- User A
- Admin
- User B
- User
- User C
- Guest
然后我们可以“硬编码”我们想要的权限逻辑,然后在用户试图进行 CRUD 任意一种操作时,我们基于用户身份,找到其对应的角色,再根据角色的权限判断当前请求是否允许通过即可。
classDiagram
class user{
+int64 user_id
}
class role{
+int64 role_id
+String desc
}
class user_role_ref{
+int64 user_id
+int64 role_id
}
user o--o user_role_ref
role o--o user_role_ref
鉴权方式类似于:check_role(user)
。
ABAC
简单来说,ABAC 的权限校验点相比于 RBAC 从 Role
转化为了 Attribute
,从而让鉴权粒度更细了,并且组合起来更自由了。沿用上面的例子,我们可以使用 ABAC 实现如下:
我们不再需要 Role 这个角色的定义,所有人都视为 User:
- User
- User A
- User B
- User C
然后把 Role 具有的权限抽象为 Attribute
:
- Attribute
- Create
- Read
- Update
- Delete
最后赋予每个 User 不同的 Attribute:
- User A
- Create + Read + Update + Delete
- User B
- Create + Read
- User C
- Read
鉴权方式类似于: check_attribute(user_id, attribute)
。
事实上,k8s 中就可以使用 ABAC 来进行权限控制,参见:使用 ABAC 鉴权
R+A
在实际的业务场景中,我们往往会使用 RBAC 和 ABAC 的混合方式,因为 Attribute 的数量可能会非常多,因此直接管理 Attribute 的成本比较高,业务一般不会这么设计。
通常来说,业务都会基于 R - A 的双层结构来进行不同粒度的权限管控,代入实际例子可以这么说:
- R:系统管理员可以访问某个 resource
- A:普通用户被授予访问该 resource 的权限后,虽然他不是管理员,但也可以访问
这么做还有一个好处,就是方便用户进行快捷的权限管控,用户把某个员工设置为系统管理员,显然比用户把所有 Attribute 都添加到该员工头上来的简单。
除此之外,我们一般可以使用 R 进行快速决策,当 R 无法满足时,再引入 A 进行复杂决策,这么做也可以提升鉴权本身的效率。
总而言之,我们可以使用 Role 来进行基本的权限控制,然后使用 Attribute 来进行更细粒度的权限控制。
常见架构设计
1. 架构总体设计
鉴权系统可以分为以下几个核心模块:
1.1 数据层
用于存储用户、角色、权限、属性和规则。数据层需要保证高效查询和及时更新。
-
用户数据:包含用户基本信息和关联的角色。
-
角色数据:存储角色与权限、属性的映射关系。
-
属性数据:存储 ABAC 中需要的用户属性、资源属性等信息。
-
规则存储:存储复杂场景下的规则,比如条件表达式或策略脚本。
1.2 数据同步层
负责从各个业务系统同步数据(用户信息、角色、属性等)到鉴权系统。
-
拉模式:定期从业务系统拉取更新(如使用 REST API 或消息队列)。
-
推模式:业务系统在数据变化时推送更新到鉴权系统。
-
缓存层:对于频繁访问的数据,可以使用内存缓存(如 Redis)以提升性能。
1.3 鉴权核心
负责权限判断的核心模块。支持 RBAC 和 ABAC 的组合逻辑。
-
RBAC 模块:通过用户关联的角色判断权限。
-
ABAC 模块:通过用户、资源的属性和规则判断权限。
-
规则引擎:支持动态规则匹配,可能使用 DSL 或标准语言(如 JSONLogic 或 Drools)。
-
决策引擎:综合 RBAC 和 ABAC 结果,根据优先级或策略作出最终鉴权决策。
1.4 API 接口层
提供统一的鉴权接口供业务系统调用。
- 简单接口:例如 checkAttr(userId, attr),用于快速校验简单场景。
- 复杂接口:例如 checkPermission(userId, action, resource),支持复杂规则判断。
1.5 日志和审计
记录所有鉴权请求和结果,便于后续排查和分析。
- 日志存储:记录访问日志。
- 审计功能:提供接口供管理员查询权限决策过程。
2. 数据同步设计
鉴权系统需要构建一套机制,与各个业务系统同步数据:
2.1 数据源和 ETL
-
用户数据源:从用户管理系统同步用户信息。
-
角色数据源:从权限管理系统同步角色及权限。
-
属性数据源:从多个业务系统同步 ABAC 所需的属性(如用户部门、资源类型)。
-
ETL 管道:实现数据提取、清洗和加载到鉴权系统。
2.2 数据实时性
对于属性变化可能影响权限的场景,需要实现实时同步或延迟最小化。
-
使用事件驱动架构(如 Kafka)捕获数据变化。
-
增量更新而非全量同步。
2.3 数据缓存
-
对于高频访问的权限数据,可以使用 Redis 等缓存。
-
缓存失效策略应与同步机制匹配,保证数据一致性。
3. 规则引擎设计
复杂场景下,需要引入规则引擎来动态判断权限。
3.1 规则表达
规则可以用 DSL 或标准语言表示:
-
DSL(领域特定语言):如
role == "admin" AND resource.type == "file" AND resource.owner == user.id
-
标准规则语言:如 JSONLogic、Drools 或 Open Policy Agent (OPA)。
3.2 规则存储
-
存储规则到数据库(如 PostgreSQL)。
-
对复杂规则,可以用版本化管理,支持回滚。
3.3 规则执行
-
RBAC 优先:先快速检查角色权限,减少不必要的规则计算。
-
ABAC 动态计算:根据规则动态计算属性权限。
3.4 规则优化
- 使用缓存存储常用规则的结果。
- 分析规则的覆盖范围,优化冗余规则。
4. API 接口层
提供对外访问接口,并确保性能与安全性。
4.1 简单接口
-
checkAttr(userId, attr):适用于 RBAC 和简单 ABAC 场景。
-
快速校验是否具备某属性(如管理员身份)。
4.2 复杂接口
-
checkPermission(userId, action, resource):综合用户角色和属性判断权限。
-
支持返回详细的决策依据(如角色命中、规则匹配等)。
4.3 批量接口
支持批量鉴权的接口(如批量检查多个资源的权限),提高效率。
5. 示例工作流
假设用户 user1 想访问资源 file123:
-
数据层查询 user1 的角色和属性,以及 file123 的属性。
-
RBAC 模块判断用户是否具备相关角色权限。
-
如果 RBAC 无法决定,则调用 ABAC 模块:
- 加载 user1 和 file123 的属性。
- 加载相关规则并执行。
- 决策引擎综合 RBAC 和 ABAC 的结果,返回最终判断。
6. 扩展功能
-
动态更新规则:支持管理员实时更新规则,立即生效。
-
多租户支持:为不同业务系统提供隔离的鉴权规则和数据。
-
性能优化:分析高频鉴权请求,使用缓存或索引优化查询。
实例
这里有一个以我在飞书工作期间为灵感的一个鉴权原型服务, 这个 Demo 主要用于演示 / 验证 RBAC + ABAC 的结合, 并没有完整的工程实现, 例如目前并没有考虑 API 设计 / ETL 数据同步等。
See: raac-demo