4. 权限矩阵基础:RBAC模型定义、用户-角色-权限关系、权限矩阵的数据库表结构设计

在构建后端权限体系时,RBAC(Role-Based Access Control,基于角色的访问控制) 是业界最成熟、应用最广泛的模型。它将权限从用户个体解耦到“角色”这一中间层,极大降低了大规模系统中的权限管理复杂度。本节将深入剖析RBAC的核心定义、三元组关系以及对应的数据库表结构设计,为后续的缓存策略打下坚实的理论基础。

4.1 RBAC模型核心定义

RBAC模型的核心思想是:权限不直接授予用户,而是授予角色;用户通过扮演角色来获得权限。这种间接关系带来了极高的灵活性和可维护性。

根据NIST标准,RBAC模型通常分为四个层级,在实际生产中最常用的是RBAC1(层级RBAC)和RBAC2(约束RBAC)的组合。下表对比了各层级的特点:

层级 名称 核心特征 典型应用场景
RBAC0 基础RBAC 用户-角色-权限三元组,无继承 小型CMS、简单后台管理
RBAC1 层级RBAC 角色支持继承(上级角色拥有下级所有权限) 企业OA、多级组织架构
RBAC2 约束RBAC 引入职责分离(SSD/DSD)、角色互斥、基数约束 金融系统、合规敏感业务
RBAC3 统一RBAC 同时支持层级继承与约束规则 大型分布式企业级平台
架构师视角: 在课程后续的缓存设计中,我们默认采用 RBAC1 + RBAC2 的混合模型。角色继承关系会直接影响缓存键的设计(需要处理角色链的递归查询),而约束规则(如互斥角色)则需要在权限校验的缓存穿透防护中额外处理。

4.2 用户-角色-权限三元组关系

RBAC模型的核心由三个实体和两个关联关系构成。理解这些关系是设计数据库表和缓存策略的前提。

  • 用户(User):系统的操作主体,可以是人、服务账号或API客户端。
  • 角色(Role):权限的集合,代表一类职责或岗位,如“管理员”、“审计员”、“普通用户”。
  • 权限(Permission):对某个资源执行特定操作的许可,通常细粒度到“资源+操作”,如“订单-创建”、“订单-删除”。

两个核心关联关系:

  1. 用户-角色关联(User-Role Assignment):多对多关系。一个用户可拥有多个角色,一个角色可被多个用户持有。
  2. 角色-权限关联(Role-Permission Assignment):多对多关系。一个角色可包含多个权限,一个权限可被多个角色引用。

下图展示了RBAC模型的三元组关系与数据流转路径:

RBAC 三元组关系与权限校验路径 用户 (User) 角色 (Role) 权限 (Permission) N:N 分配 N:N 包含 权限校验路径(JWT令牌携带角色信息) ① 用户登录 → 服务端查询用户关联的角色列表 ② 根据角色列表,聚合所有角色的权限集合(去重) ③ 将角色标识(如 role_id 列表)写入 JWT 令牌的 payload ④ 后续请求携带 JWT → 后端从令牌解析角色 → 查询缓存/DB 获取权限矩阵 ⑤ 执行权限校验:当前请求的资源+操作 是否在权限矩阵中
设计要点: 在JWT令牌中,不建议直接存放权限列表(会导致令牌体积过大,增加网络开销)。最佳实践是存放 角色ID列表,后端通过角色ID去缓存或数据库中拉取对应的权限矩阵。这正是本课程后续“缓存策略”章节要解决的核心性能问题。

4.3 权限矩阵的数据库表结构设计

基于上述三元组关系,我们需要设计5张核心表来实现RBAC模型。其中3张为实体表,2张为关联表。以下是标准的数据库设计:

4.3.1 实体表设计

表名 字段 类型 说明
sys_user id BIGINT PK 用户主键
username VARCHAR(64) 登录名,唯一索引
password_hash VARCHAR(256) 加密后的密码
status TINYINT 0-禁用,1-启用
sys_role id BIGINT PK 角色主键
role_name VARCHAR(64) 角色名称,如 admin、auditor
parent_role_id BIGINT 父角色ID(支持RBAC1层级继承,NULL为顶级角色)
sys_permission id BIGINT PK 权限主键
resource VARCHAR(128) 资源标识,如 order、user、report
action VARCHAR(64) 操作标识,如 create、read、update、delete
description VARCHAR(256) 权限描述,如“创建订单”

4.3.2 关联表设计

表名 字段 类型 说明
sys_user_role user_id BIGINT 联合主键,外键关联 sys_user.id
role_id BIGINT 联合主键,外键关联 sys_role.id
sys_role_permission role_id BIGINT 联合主键,外键关联 sys_role.id
permission_id BIGINT 联合主键,外键关联 sys_permission.id

4.3.3 核心SQL示例

以下SQL展示了如何查询某个用户的所有权限(含角色继承):

-- 1. 查询用户直接关联的角色
SELECT role_id FROM sys_user_role WHERE user_id = ?;

-- 2. 递归查询所有父角色(支持RBAC1层级继承)
WITH RECURSIVE role_tree AS (
    SELECT id, parent_role_id FROM sys_role WHERE id IN (?, ?, ...)  -- 第一步查出的角色ID
    UNION ALL
    SELECT r.id, r.parent_role_id FROM sys_role r
    INNER JOIN role_tree rt ON r.id = rt.parent_role_id
)
SELECT DISTINCT id FROM role_tree;

-- 3. 根据所有角色ID查询权限
SELECT DISTINCT p.resource, p.action
FROM sys_role_permission rp
INNER JOIN sys_permission p ON rp.permission_id = p.id
WHERE rp.role_id IN (?, ?, ...);  -- 第二步递归出的所有角色ID
性能警告: 上述递归CTE查询在角色层级较深(超过5层)或角色数量较大(超过1000个)时,数据库查询延迟会显著增加。在生产环境中,绝对不能在每次HTTP请求中都执行此查询。这正是我们需要引入缓存层(Redis)来存储“用户→权限矩阵”映射关系的根本原因。后续章节将详细讲解如何设计缓存键、缓存粒度以及缓存更新策略。

4.4 权限矩阵的内存表示

在缓存和运行时校验中,权限矩阵通常以位图(Bitmap)或哈希集合(Hash Set)的形式存在。以下是一个典型的权限矩阵内存结构示例:

// 权限矩阵缓存结构(Redis Hash)
Key: "perm:matrix:{role_id}"
Field: "order"   Value: 0b1111  (代表 create=1, read=1, update=1, delete=1)
Field: "user"    Value: 0b1010  (代表 create=1, read=0, update=1, delete=0)
Field: "report"  Value: 0b0010  (代表 create=0, read=0, update=1, delete=0)

// 权限校验伪代码
public boolean checkPermission(long userId, String resource, String action) {
    // 1. 从JWT中解析角色列表
    List<Long> roleIds = parseRolesFromJwt(token);
    // 2. 从缓存中批量获取角色的权限矩阵
    Map<String, Integer> permMatrix = cache.batchGet(roleIds);
    // 3. 按位与运算校验
    int actionBit = getActionBit(action); // create=1, read=2, update=4, delete=8
    return (permMatrix.get(resource) & actionBit) != 0;
}

下图展示了从数据库表结构到缓存中权限矩阵的映射关系:

数据库表 → 缓存权限矩阵映射 数据库(MySQL) sys_user_role 表 user_id=1001 → role_id=1,2 sys_role_permission 表 role_id=1 → perm_id=10,11,12 role_id=2 → perm_id=11,13 sys_permission 表 perm_id=10 → order:create perm_id=11 → order:read 聚合+转换 缓存(Redis) Key: "perm:user:1001" Field: "order" Value: 0b0011 Field: "user" Value: 0b1000 Field: "report" Value: 0b0100 TTL: 3600秒(自动过期) 更新策略:角色变更时主动删除 查询耗时:< 1ms(内存操作)
架构决策: 在缓存中,我们选择以 用户ID 为Key,而不是以角色ID为Key。这是因为:① 用户维度的缓存更符合业务查询模式(每次请求都是校验当前用户);② 避免在应用层做角色权限的合并计算;③ 缓存失效时只需删除单个用户Key,粒度更细,影响面更小。代价是当角色权限变更时,需要遍历所有持有该角色的用户并删除其缓存,后续章节将介绍如何通过“缓存版本号”或“延迟双删”策略来解决这个问题。