3、RBAC进阶模型:RBAC1角色继承、RBAC2职责分离(SSD/DSD)、RBAC3统一模型
好,咱们继续往下走。上一章我们把RBAC0这个基础模型聊透了,说白了就是「用户-角色-权限」的铁三角。但实际落地的时候你会发现,光有基础模型根本不够用。
举个例子:你公司有「销售总监」和「销售专员」两个角色。专员能看自己的客户,总监能看全团队的客户。如果按RBAC0来做,你得给这两个角色分别配权限。但仔细想想,总监的权限其实完全覆盖了专员的权限,对吧?
这就是RBAC1要解决的问题——角色继承。
3.1 RBAC1:角色继承模型
角色继承,说白了就是「上级角色拥有下级角色的所有权限」。这在现实世界里太常见了:经理能做的,总监肯定能做;总监能做的,VP肯定能做。
我早期做第一个SaaS项目时,就踩过这个坑。当时给每个层级都单独配权限,结果客户公司有7个管理层级,配了整整两周,还经常出错。后来改用角色继承,一天就搞定了。
核心概念:
- 父角色:拥有更广泛的权限集合
- 子角色:继承父角色的所有权限,并可额外拥有自己的权限
- 继承关系:通常是偏序关系,支持多级继承
重要原则:角色继承是单向的。子角色继承父角色,但父角色不能反过来拥有子角色的权限。这就像你爸能管你,但你不能管你爸——嗯,道理是一样的。
代码示例:角色继承的数据结构
// 角色继承关系定义
{
"roles": [
{
"id": "role_admin",
"name": "系统管理员",
"parent_id": null, // 顶级角色
"permissions": ["system.config", "user.manage"]
},
{
"id": "role_manager",
"name": "部门经理",
"parent_id": "role_admin", // 继承管理员权限
"permissions": ["report.view", "team.manage"]
},
{
"id": "role_staff",
"name": "普通员工",
"parent_id": "role_manager", // 继承经理权限
"permissions": ["task.create", "task.view"]
}
]
}
// 获取用户最终权限(递归合并)
function getEffectivePermissions(userRole) {
let permissions = new Set();
let current = userRole;
while (current) {
current.permissions.forEach(p => permissions.add(p));
current = findParent(current.parent_id);
}
return Array.from(permissions);
}
你想想看,这种继承关系一旦建立起来,权限管理就变得特别优雅。你只需要维护每个角色「额外」的权限,公共权限全部由继承搞定。
我的经验:角色继承层级建议控制在3-5层。太深了容易混乱,而且性能会受影响。我曾经见过一个客户搞了8层继承,结果查一次权限要递归8次,页面加载慢得像蜗牛。
3.2 RBAC2:职责分离模型
RBAC1解决了权限复用的问题,但带来了一个新问题:权限过度集中。
举个例子:一个员工既可以是「采购员」又可以是「验收员」。那他自己采购、自己验收,岂不是想怎么搞就怎么搞?这在金融、医疗等合规要求高的行业,是绝对不允许的。
RBAC2就是专门解决这个问题的。它分两种:
3.2.1 静态职责分离(SSD)
SSD的意思是:一个用户不能同时拥有两个互斥的角色。这是在用户分配角色时就做检查的。
典型场景:
- 「采购员」和「验收员」不能是同一个人
- 「出纳」和「会计」不能是同一个人
- 「审批人」和「申请人」不能是同一个人
代码示例:SSD约束检查
// 定义互斥角色对
const SSD_RULES = [
{ roleA: "purchaser", roleB: "inspector" },
{ roleA: "cashier", roleB: "accountant" },
{ roleA: "approver", roleB: "applicant" }
];
function assignRoleToUser(userId, newRole) {
// 获取用户已有角色
const userRoles = getUserRoles(userId);
// 检查互斥
for (let rule of SSD_RULES) {
if (newRole === rule.roleA && userRoles.includes(rule.roleB)) {
throw new Error(`用户已有${rule.roleB}角色,不能同时分配${rule.roleA}`);
}
if (newRole === rule.roleB && userRoles.includes(rule.roleA)) {
throw new Error(`用户已有${rule.roleA}角色,不能同时分配${rule.roleB}`);
}
}
// 通过检查,分配角色
userRoles.push(newRole);
saveUserRoles(userId, userRoles);
}
3.2.2 动态职责分离(DSD)
DSD比SSD更灵活一些。它允许用户拥有互斥角色,但在同一个会话或同一个操作流程中不能同时使用。
举个例子:一个员工可以同时是「采购员」和「验收员」,但他不能在同一笔采购单上既采购又验收。换句话说,他可以在A单上做采购,在B单上做验收,但不能A单上又采购又验收。
注意:DSD的实现比SSD复杂得多。你需要记录操作上下文(比如订单ID、流程实例ID),在运行时动态检查。我曾经在一个ERP项目中实现DSD,光状态管理就写了2000多行代码。
SSD vs DSD 对比:
| 维度 | SSD(静态) | DSD(动态) |
|---|---|---|
| 检查时机 | 角色分配时 | 操作执行时 |
| 灵活性 | 低,严格互斥 | 高,上下文相关 |
| 实现复杂度 | 简单 | 复杂 |
| 适用场景 | 合规要求严格的行业 | 需要灵活性的企业 |
3.3 RBAC3:统一模型
RBAC3就是把RBAC1和RBAC2合并起来。它既支持角色继承,又支持职责分离。
你可能会问:这两个东西合在一起会不会冲突?
会的。举个例子:如果「经理」继承了「员工」的角色,而「员工」和「审批人」是互斥的,那「经理」能不能同时拥有「审批人」角色?
这就要看你的继承策略了。我个人习惯的做法是:
- 继承优先:子角色自动继承父角色的所有权限,包括互斥约束
- 约束覆盖:如果子角色自身有互斥约束,以子角色为准
- 传递性检查:检查继承链上所有角色的互斥关系
代码示例:RBAC3的权限检查
function checkRBAC3Permission(userId, operation, context) {
// 1. 获取用户所有角色(包括继承来的)
const allRoles = getAllRolesWithInheritance(userId);
// 2. 检查SSD约束
if (hasSSDConflict(allRoles)) {
return { allowed: false, reason: "角色互斥冲突" };
}
// 3. 检查DSD约束(如果有上下文)
if (context && hasDSDConflict(allRoles, operation, context)) {
return { allowed: false, reason: "动态职责分离限制" };
}
// 4. 合并所有角色的权限
const effectivePermissions = mergePermissions(allRoles);
// 5. 检查具体操作权限
return {
allowed: effectivePermissions.includes(operation),
roles: allRoles.map(r => r.name)
};
}
核心要点:RBAC3不是简单的1+2,而是要考虑继承和约束的相互作用。我建议在实现时,先画一张角色继承树,然后在树上标注互斥关系,这样一目了然。
3.4 实际项目中的选择建议
说了这么多,你可能会问:我到底该用哪个?
我的建议是这样的:
- 小团队(10人以下):RBAC0就够了,别搞复杂了
- 中型企业(10-100人):RBAC0 + 部分RBAC1(角色继承),职责分离用手工控制
- 大型企业(100人以上):RBAC3全上,但建议分阶段实施
- 金融、医疗等合规行业:直接上RBAC3,SSD和DSD都要实现
避坑指南:我曾经帮一个客户从RBAC0迁移到RBAC3,花了整整3个月。最大的教训是——不要一次性把所有约束都加上。先跑通继承,再加SSD,最后加DSD。每一步都要充分测试。
好了,这一章的内容就到这里。RBAC1、RBAC2、RBAC3这三个模型,说白了就是给基础模型加了两层「保险」:继承让权限管理更高效,职责分离让权限控制更安全。下一章我们聊聊怎么把这些模型落地到数据库设计中,那才是真正考验架构能力的地方。