角色层次与继承:角色树设计

角色继承,说白了就是让下级角色自动拥有上级角色的部分权限。这样做的好处很明显——省事儿,维护成本低。我在企业级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,那就成死循环了。查询时会无限递归,数据库爆炸是小事,业务逻辑也会彻底乱掉。

避免循环继承的常见方法:

  1. 在写入时做环检测:新增父子关系前,先检查child是否已经是parent的祖先。
  2. 使用闭包表:一张表存所有可达路径,维护成本略高,但查询快且天然防环。
  3. 限制继承深度:业务上限定最多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下面,结果这个临时角色因为继承关系,自动拿到了所有数据库删除权限。幸好只是测试环境,不然就出大事故了。从那以后,我强制所有角色树必须做权限最小化设计,继承来的权限也允许手动降级(虽然理论上有争议,但业务上实用)。