3. 系统整体架构设计:RBAC权限系统分层与模块划分
做RBAC系统,最怕一上来就写代码。我在项目里踩过这个坑——没有整体架构直接开干,结果权限判断散落在业务代码各个角落,后期要改个角色继承逻辑,简直要翻遍整个项目。后来我学乖了,先画架构图,再分模块,最后才落代码。
说白了,RBAC权限系统不是独立存在的。它必须嵌入到你的业务系统里,但又不能和业务逻辑纠缠太深。我个人习惯分四层来思考:接入层、策略层、数据层、审计层。这四层各管各的事,互相通过接口通信,耦合度降到最低。
3.1 四层架构总览
先看整体结构,我画了一张图帮你理解。这张图是我做过的几个项目后总结出来的通用模式,基本能覆盖大部分业务场景。
这张图的核心思想:请求从上往下穿透,每一层只关注自己的职责。接入层不碰业务逻辑,策略层不存数据,数据层不做决策,审计层只记录。你想想看,这样是不是特别好维护?
3.2 接入层:请求拦截与上下文构建
接入层是RBAC系统的第一道门。它负责把每个请求里的用户身份、请求路径、HTTP方法等信息提取出来,包装成一个统一的「权限上下文」对象。
我之前见过一个项目直接在业务Controller里写if(user.hasRole("admin")),这种写法有多坑我就不多说了。正确的做法是在Filter或Interceptor里统一处理。
· 从请求中获取用户身份(JWT / Session / )
· 从数据库中查询用户关联的角色和权限缓存
· 构建 Subject 对象,存放 userId、roleIds、permissionTags
· 构建 ResourceAction 对象,存放请求的 URI + Method + 参数
· 将 Subject 和 ResourceAction 传递给策略层
3.3 策略层:核心决策引擎
策略层是整个系统的脑髓。它接收接入层传过来的 Subject 和 ResourceAction,然后执行策略匹配。
RBAC 的基本决策逻辑其实很简单:
def check_access(subject, resource_action):
# 1. 获取用户所有角色的权限集合
role_ids = subject.role_ids
permissions = permission_cache.get_permissions_by_roles(role_ids)
# 2. 判断请求的资源+操作是否在权限集合中
required = (resource_action.resource_code, resource_action.action_code)
if required in permissions:
return True
# 3. 特殊策略:比如上级审批、白名单、IP限制等
return extra_policy_check(subject, resource_action)
嗯,这里要注意一个陷阱——角色继承和权限合并。很多RBAC系统允许角色之间继承关系,比如「管理员」继承「普通用户」的所有权限。在一开始设计策略引擎时,就要把继承的展开逻辑放在数据加载阶段,而不是每次检查都去递归,否则性能会崩。
3.4 数据层:存储与缓存设计
数据层主要管五张核心表:用户表、角色表、权限表、用户-角色关联表、角色-权限关联表。有继承关系的还需要一张角色继承关系表。
这里我列一个推荐的表结构设计,你在项目里可以直接参考:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, username, password_hash, status | 不存角色信息,通过关联表获取 |
| role | id, role_code, role_name, level, parent_id | parent_id实现角色继承 |
| permission | id, perm_code, resource_code, action_code, description | resource_code + action_code 唯一 |
| user_role | user_id, role_id | 多对多关联 |
| role_perm | role_id, perm_id | 多对多关联 |
| role_hierarchy | ancestor_id, descendant_id, depth | 闭包表,方便快速查询所有祖先/后代 |
缓存策略上,我习惯用「双缓存 + 失效通知」的模式:用户登录时把该用户所有展开后的权限集合写入Redis,同时用数据库的updated_at时间戳做增量同步。一旦后台修改了角色-权限关系,就主动删除对应用户的缓存,下次请求时重新加载。
3.5 审计层:一切都要留痕
这一层很容易被忽略,但真出了权限事故,你就知道它多重要了。审计层要记录:谁在什么时间访问了什么资源、结果是被允许还是被拒绝了、是哪个策略生效的。
我个人习惯把审计日志分两部分存储:
- 操作日志:记录每一次鉴权请求的结果,用于排查异常和性能分析。用Elasticsearch或MongoDB存,查询快。
- 变更日志:记录管理员对角色、权限、用户-角色关系的所有修改。用普通关系表存,因为要支持事务完整性。
我曾经因为没记录变更日志,被客户问到「这个管理员是谁在什么时候加上的?」——完全答不上来。从那以后,我把所有数据层的写操作都切成了事件驱动的记录模式,每条数据变更都有审计轨迹。
3.6 模块划分与接口映射
有了分层架构,接下来就是模块划分。我通常把整个权限系统拆成以下几个模块:
- 权限管理模块:对外提供权限配置的后台API,比如创建角色、分配权限、查看策略。
- 鉴权服务模块:对内提供鉴权接口,业务系统调用它来判断当前用户能否执行某个操作。
- 数据同步模块:处理缓存和数据库之间的同步,以及跨服务之间的权限数据分发。
- 审计模块:负责日志采集、存储和查询。
接口映射上,我建议遵循一个原则:鉴权接口只问「能不能」,不问「是谁」。也就是说,业务系统只需要传一个(userId, resourceCode, actionCode)三元组,鉴权服务返回true/false。角色是谁、策略怎么配,那是权限管理模块的事,业务系统根本不需要知道。
authService.check(userId, "order", "cancel")。如果返回false,就灰显按钮。这样哪怕权限模型从RBAC换成了ABAC,业务代码一行都不用改。
接口映射时还需注意批量鉴权的场景。一个页面可能有十几个按钮都需要鉴权,一个一个调太慢了。我一般会提供一个批量接口,传入多个(resource, action)对,一次返回所有结果。说白了,就是空间换时间。
好了,这四层架构和模块划分基本覆盖了RBAC权限系统从接收请求到返回结果的完整链路。下一层咱们具体讲数据模型的设计,我在那章会给你看完整的建表SQL和索引优化策略。