RBAC 模型详解:核心RBAC、层级RBAC、约束RBAC

RBAC 不是什么新鲜概念,但能把它的三种变体用透的人,说实话不多。我在不少项目里见过“伪 RBAC”——用户绑了一堆角色,角色绑了一堆权限,结果代码里到处都是 if-else 判断。那不是 RBAC,那是权限面条。

这一节我们就把 RBAC 的三个主力版本聊透:核心、层级、约束。你想想看,从最简单的“用户-角色-权限”到能支持几千人组织的继承体系,再到银行系统里那种“不能既审核又打款”的安全约束,这三层正好对应了从小团队到大型企业的真实演进路径。

3.1 核心RBAC(Core RBAC)——地基

核心 RBAC 定义了四个基本元素:用户(User)、角色(Role)、权限(Permission)、会话(Session)。说白了,就是用户登录时激活一组角色,角色授权一组操作。

我刚开始做权限时犯过一个错:把权限直接挂在用户上。后来一个 200 人的系统改了 30 多次权限表,表结构乱成一锅粥。嗯,从那以后我再也不干这种事了。

核心 RBAC 的要点用表格列一下:

元素含义举例
用户人/系统主体张三(用户ID=1001)
角色权限的集合运营专员、审核主管
权限对资源的具体操作创建订单、查看报表、删除记录
会话用户激活角色的一次登录张三今天登录后激活了“运营专员”角色

这里有个容易被忽略的点:会话是动态的。用户可以有多个角色,但不一定全部激活。我记得在做一个 SaaS 租户系统时,用户既是“租户管理员”又是“普通成员”,如果同时激活两个身份会导致权限叠加出 bug。后来我强制会话只能激活一个角色组,问题就解决了。

技巧:在设计用户-角色关联表时,加一个is_default字段,用来标记用户登录后默认激活哪些角色。配合会话管理,可以干净地实现“最小权限激活”。

代码层面,核心 RBAC 的授权判断其实就三行逻辑:

// 伪代码:核心RBAC授权判断
function checkAccess(user, action, resource) {
    roles = getUserActiveRoles(user);        // 获取用户当前会话激活的角色
    permission = findPermission(action, resource); // 查找需要的权限
    return hasAnyRolePermission(roles, permission); // 看角色集里谁有这个权限
}

你看,没有硬编码,没有魔数。角色就是权限的容器,用户就是角色的容器。

3.2 层级RBAC(Hierarchical RBAC)——继承的力量

核心 RBAC 有一个硬伤:每个角色都要重复定义权限。比如“高级运营”和“初级运营”,前者多一个数据导出权限,后者没有。如果没有继承,你得把两个角色的权限列表全列一遍。

层级 RBAC 引入了角色继承。子角色自动拥有父角色的所有权限,并且可以增加自己的专有权限。这在组织架构里特别实用——一个部门经理自动拥有他手下所有助理的权限,再加上审批权。

我曾经在一个金融项目中踩过坑:用了多继承(一个角色从两个父角色继承),结果“审计员”同时继承了“管理员”和“分析师”,权限瞬间膨胀到不可控。后来我们规定单继承 + 显式拒绝(用 deny 覆盖 inherit)才控制住局面。

层级 RBAC 的关系图可以这样理解:

角色继承是方向性传递:如果“高级运营”继承“运营专员”,而“运营专员”继承“普通员工”,那么“高级运营”也拥有“普通员工”的所有权限。

实际建表时,可以存一张role_hierarchy表:

-- 角色继承表
CREATE TABLE role_hierarchy (
    parent_role_id INT NOT NULL,
    child_role_id  INT NOT NULL,
    PRIMARY KEY (parent_role_id, child_role_id)
);

-- 查询某个角色及其所有父角色的权限(递归,MySQL 8+)
WITH RECURSIVE cte AS (
    SELECT role_id FROM roles WHERE role_id = ?  -- 当前角色
    UNION ALL
    SELECT rh.parent_role_id FROM role_hierarchy rh
    INNER JOIN cte ON rh.child_role_id = cte.role_id
)
SELECT DISTINCT p.* FROM permissions p
JOIN role_permissions rp ON p.permission_id = rp.permission_id
WHERE rp.role_id IN (SELECT role_id FROM cte);

嗯,递归查询在数据量大时要注意性能。我建议在业务层做角色树的缓存,而不是每次都跑递归。

3.3 约束RBAC(Constrained RBAC)——安全底线

约束 RBAC 是在层级 RBAC 的基础上加了控制规则,专门解决“权限滥用”问题。最常见的是两类约束:

  • 职责分离(SoD):一个人不能同时拥有冲突的角色。比如不能同时是“采购员”和“验收员”,否则就能自己买自己验,容易腐败。
  • 基数约束:限制某个角色最多被多少人拥有。比如“系统管理员”最多只能 2 个人。

我曾经在一个政务系统里处理过动态职责分离:用户可以在不同会话里扮演不同角色,但同一个会话中不能同时激活两个互斥角色。比如张三上午以“审批员”身份工作,下午换“审计员”角色登录,这是允许的;但如果在同一个会话里同时激活这两个角色,系统会抛异常。实现起来就是在会话激活角色时做一次互斥校验。

约束 RBAC 的配置通常像下面这样:

// 静态职责分离配置(SSD)
{
  "ssd_policies": [
    { "conflict_roles": ["采购员", "验收员"], "threshold": 1 },
    { "conflict_roles": ["制单员", "审核员"], "threshold": 1 }
  ]
}

// 动态职责分离(DSD)可以在会话层做校验
function activateRole(user, session, roleId) {
    const currentRoles = session.getActiveRoles();
    const rules = getDsdRules(); // 获取动态互斥规则
    for (let rule of rules) {
        if (rule.conflict_roles.includes(roleId) &&
            currentRoles.some(r => rule.conflict_roles.includes(r))) {
            throw new Error('DSD violation: conflict roles');
        }
    }
    session.addRole(roleId);
}
注意:职责分离的粒度需要和业务对齐。有些场景需要“硬分离”(角色根本不分配给同一个人),有些只需要“软分离”(同一人可以持有角色,但不能在同一个会话中激活)。搞混了会导致业务方天天投诉。

三种RBAC的对比

类型核心能力适用场景复杂度
核心RBAC用户-角色-权限映射小型系统、用户数<100低
层级RBAC角色继承,权限复用中大型组织、部门层级多中
约束RBAC职责分离、基数控制金融、政务等安全敏感场景高

你想想看,如果团队只有 5 个人,用核心 RBAC 就够了;400 人的公司必须上层级 RBAC,否则权限维护成本会爆炸;涉及钱和敏感数据的系统,一定要把约束 RBAC 加上,不然审计过不了。

下面这张图把三者的关系做了可视化:

RBAC三种模型结构关系图 RBAC 三种模型结构 核心RBAC 用户 角色 权限 会话 层级RBAC 上级角色 下级角色 继承所有父角色权限 约束RBAC 静态职责分离 (用户不能同时拥有角色A和B) 基数约束 (角色最多3人拥有) 动态职责分离(会话级) 层级RBAC可建立在核心RBAC之上,约束RBAC可叠加在层级RBAC之上

从图中能看出,这三种模型不是互斥的,而是递进关系。你可以从核心 RBAC 起步,随着组织规模和安全需求增加,逐步引入层级和约束。我见过很多团队一上来就上约束 RBAC,结果把简单的事情做复杂了——先跑通核心模型,再慢慢加继承和规则,才是务实路线。

好啦,关于 RBAC 的三种形态就说到这。下次你再听到“我们权限系统是 RBAC 的”,可以追问一句:“核心、层级,还是约束?” 答案不同,背后的设计差异可大着呢。