角色层次与继承:角色树设计
角色继承,说白了就是让下级角色自动拥有上级角色的部分权限。这样做的好处很明显——省事儿,维护成本低。我在企业级RBAC系统里见过上百个角色,要是每个角色都得手工配权限,那真是地狱级工作量。
角色树怎么设计
角色继承的典型结构是一棵树。根节点是“超级管理员”或者“系统管理员”,往下分业务角色、功能角色。每个子节点只存自己的增量权限,父节点的权限自动透传下来。
举个例子:
- 运营总监:管理所有订单、查看报表、审批退款
- 高级运营:继承运营总监的部分权限,额外加一些编辑权限
- 初级运营:只能查看订单、处理简单工单
这种结构下,你只需要给“运营总监”配一次基础权限,后面新增角色直接挂在对应父节点下就行。
个人习惯:我设计角色树时,通常不超过4层深度。太深了,权限传递链路会变得很难追踪。
权限向下继承机制
继承机制的核心是——子角色自动获得所有父节点已授权的权限,并且可以额外增加自己独有的权限。但要注意,子角色不能剥夺父角色的权限,只能叠加。
怎么实现?我一般用一张父子关系表:
CREATE TABLE role_hierarchy (
parent_role_id INT NOT NULL,
child_role_id INT NOT NULL,
PRIMARY KEY (parent_role_id, child_role_id),
FOREIGN KEY (parent_role_id) REFERENCES roles(id),
FOREIGN KEY (child_role_id) REFERENCES roles(id)
);
查询权限时,递归地向上找所有祖先角色,合并权限集。SQL写法很多,比如用CTE:
WITH RECURSIVE role_tree AS (
SELECT id FROM roles WHERE id = :current_role_id
UNION ALL
SELECT r.id FROM roles r
JOIN role_hierarchy rh ON rh.parent_role_id = r.id
JOIN role_tree rt ON rh.child_role_id = rt.id
)
SELECT DISTINCT p.* FROM permissions p
JOIN role_permissions rp ON rp.permission_id = p.id
WHERE rp.role_id IN (SELECT id FROM role_tree);
我曾经掉过的坑:直接递归查询没有做深度限制,结果角色树出现循环引用,数据库查询直接死循环,导致线上服务挂了20分钟。后来我强制限制了递归深度最大20层,并且每次新增关系时做环检测。
避免循环继承
角色树如果出现A继承B,B继承C,C又继承A,那就成死循环了。查询时会无限递归,数据库爆炸是小事,业务逻辑也会彻底乱掉。
避免循环继承的常见方法:
- 在写入时做环检测:新增父子关系前,先检查child是否已经是parent的祖先。
- 使用闭包表:一张表存所有可达路径,维护成本略高,但查询快且天然防环。
- 限制继承深度:业务上限定最多3~5层,减少出错概率。
我偏爱闭包表,因为查起来简单,不用写递归SQL:
CREATE TABLE role_closure (
ancestor_id INT NOT NULL,
descendant_id INT NOT NULL,
depth INT NOT NULL,
PRIMARY KEY (ancestor_id, descendant_id)
);
-- 新增关系时,把新角色的所有祖先与旧后代的所有后笛卡尔积插入
-- 如果发现插入后存在 descendant_id = ancestor_id 的记录(depth=0除外),说明产生了循环
实战建议:无论用哪种方案,永远不要在应用层手动遍历角色树做权限检查。我在项目里见过有人写递归循环读取数据库,性能惨不忍睹。一定要把权限平铺到一张快照表里,定期刷新,或者用内存缓存。
实际案例:电商后台角色树设计
我去年帮一家中型电商重构权限系统,他们原有200多个角色,权限混乱得一塌糊涂。我给他们设计了以下角色树:
| 层级 | 角色 | 继承自 | 核心权限 |
|---|---|---|---|
| 1 | 超级管理员 | 无 | 所有权限 |
| 2 | 运营主管 | 超级管理员 | 订单管理、退款审批、报表查看 |
| 2 | 商品主管 | 超级管理员 | 商品增删改、类目管理、品牌管理 |
| 3 | 运营专员 | 运营主管 | 订单查看、工单处理、客户留言回复 |
| 3 | 商品编辑 | 商品主管 | 商品编辑、上下架、价格修改 |
这么一整理,角色数量从200多降到了18个。每个角色只需要配几十条权限,而不是像之前那样全量拷贝。维护起来清爽多了。
我个人的小技巧:设计角色树时,先画出业务域的组织架构图,再映射成角色树。千万别让技术直接拍脑袋,不然最后肯定要返工。
继承带来的权限膨胀问题
角色继承虽然方便,但有个副作用——权限容易膨胀。比如“运营专员”继承了“运营主管”的报表权限,但后来报表权限下放给了更多角色,结果“运营专员”无意中看到了不该看的数据。
解决办法:
- 权限显式声明:每个子角色都必须明确声明“我需要继承谁的权限”,而不是默认全继承。
- 定期审计:每季度跑一遍全量权限矩阵,看看有没有“越级”的继承路径。
- 白名单机制:父角色的大权限可以设置“敏感级别”,子角色需要额外申请才能继承。
我曾经犯过的错:线上开了一个“临时角色”,挂在CEO下面,结果这个临时角色因为继承关系,自动拿到了所有数据库删除权限。幸好只是测试环境,不然就出大事故了。从那以后,我强制所有角色树必须做权限最小化设计,继承来的权限也允许手动降级(虽然理论上有争议,但业务上实用)。