4. RBAC 角色约束模型(RBAC2):职责分离与基数约束

RBAC2 在核心 RBAC0 的基础上引入了约束机制,确保权限分配不会违反组织的安全策略或业务流程。约束是 RBAC 模型中最具实战价值的部分,它直接决定了权限系统能否在真实企业环境中落地。

核心认知: 没有约束的 RBAC 只是角色-权限的简单映射,有了约束才成为可治理的企业级权限体系。RBAC2 定义了四种约束类型:互斥角色(静态职责分离)、基数约束、先决条件角色、动态职责分离。

4.1 静态职责分离(SSD)与互斥角色

静态职责分离(Static Separation of Duty, SSD)在用户-角色分配阶段实施约束,防止同一用户同时拥有两个或多个互斥角色。这是最常见的约束类型,用于防范利益冲突或权限过度集中。

4.1.1 互斥角色的定义与数据表设计

互斥角色是指不能同时分配给同一用户的角色集合。例如:出纳员与会计审核员、采购员与验收员、系统管理员与安全审计员。

约束类型 检查时机 违反后果 典型场景
静态互斥(SSD) 用户-角色分配时 分配被拒绝 出纳与会计审核
动态互斥(DSD) 会话-角色激活时 激活被拒绝 同一人不同时段切换角色
基数约束 分配或激活时 超过上限被拒绝 一个部门只能有 1 个经理
先决条件角色 分配时 缺少前置角色被拒绝 必须先有“员工”才能有“经理”

互斥角色的数据表设计需要在关系层面表达约束关系:

-- 互斥角色组表:定义一组互斥的角色
CREATE TABLE ssd_role_set (
    set_id      INT PRIMARY KEY,
    set_name    VARCHAR(100) NOT NULL,
    description VARCHAR(255),
    cardinality INT NOT NULL DEFAULT 1  -- 该组中最多允许分配的角色数
);

-- 互斥角色组成员表:关联角色到互斥组
CREATE TABLE ssd_role_set_member (
    set_id  INT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (set_id, role_id),
    FOREIGN KEY (set_id) REFERENCES ssd_role_set(set_id),
    FOREIGN KEY (role_id) REFERENCES role(role_id)
);

-- 检查逻辑示例(PL/pgSQL):
-- 当给用户分配角色时,检查该角色所属的所有互斥组,
-- 如果用户已拥有该组中 cardinality 个角色,则拒绝分配。
实战要点: cardinality 字段控制互斥的严格程度。cardinality=1 表示完全互斥(只能选其一);cardinality=2 表示最多可选两个;cardinality=N 表示最多可选 N 个。这种设计比简单的二元互斥更灵活。

4.1.2 SSD 约束的验证流程

SSD 约束在用户-角色分配操作中实时验证,流程如下:

  1. 接收请求: 用户 U 请求分配角色 R
  2. 查询互斥组: 查找角色 R 所属的所有 SSD 互斥组
  3. 遍历检查: 对每个互斥组,统计用户 U 已分配的角色中属于该组的数量
  4. 阈值判断: 如果已分配数量 ≥ 该组的 cardinality,则拒绝分配
  5. 通过: 所有互斥组检查通过,允许分配
SSD 互斥角色验证流程 分配角色请求 查询角色所属互斥组 统计用户已分配角色 已分配数 ≥ cardinality? 是 拒绝分配 否 允许分配

4.2 动态职责分离(DSD)

动态职责分离(Dynamic Separation of Duty, DSD)在会话-角色激活阶段实施约束。用户可以被分配多个互斥角色,但在同一个会话中不能同时激活它们。DSD 比 SSD 更灵活,适用于需要同一人不同时段扮演不同角色的场景。

4.2.1 DSD 与 SSD 的核心区别

对比维度 SSD(静态) DSD(动态)
检查时机 用户-角色分配时 会话-角色激活时
用户能否拥有互斥角色 不能 可以
同一会话能否激活互斥角色 不适用 不能
典型场景 出纳 vs 会计审核 值班经理 vs 普通操作员
实现复杂度 低(分配时一次检查) 高(每次激活时检查)

4.2.2 DSD 数据表设计

-- 动态互斥角色组表
CREATE TABLE dsd_role_set (
    set_id      INT PRIMARY KEY,
    set_name    VARCHAR(100) NOT NULL,
    cardinality INT NOT NULL DEFAULT 1  -- 同一会话中最多可激活的角色数
);

-- 动态互斥组成员表
CREATE TABLE dsd_role_set_member (
    set_id  INT NOT NULL,
    role_id INT NOT NULL,
    PRIMARY KEY (set_id, role_id),
    FOREIGN KEY (set_id) REFERENCES dsd_role_set(set_id),
    FOREIGN KEY (role_id) REFERENCES role(role_id)
);

-- 会话激活角色表(用于运行时检查)
CREATE TABLE session_active_role (
    session_id   INT NOT NULL,
    role_id      INT NOT NULL,
    activated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (session_id, role_id),
    FOREIGN KEY (session_id) REFERENCES session(session_id),
    FOREIGN KEY (role_id) REFERENCES role(role_id)
);
性能注意: DSD 检查发生在每次角色激活时,高频场景下需要缓存互斥关系。建议将 DSD 规则加载到 Redis 等缓存中,避免每次激活都查询数据库。

4.3 基数约束

基数约束限制某个角色可以被分配给的用户数量上限,或者某个用户可以被分配的角色数量上限。基数约束用于控制权限的集中度,防止某个角色被过度授权。

4.3.1 基数约束的两种类型

  • 角色基数: 一个角色最多能分配给多少个用户。例如:“总经理”角色只能分配给 1 个用户。
  • 用户基数: 一个用户最多能拥有多少个角色。例如:普通员工最多分配 5 个角色。

4.3.2 基数约束数据表设计

-- 在角色表中增加基数字段
ALTER TABLE role ADD COLUMN max_users INT DEFAULT NULL COMMENT '角色最大分配用户数,NULL表示不限制';

-- 在用户-角色关联表中增加基数检查触发器
CREATE OR REPLACE FUNCTION check_role_cardinality()
RETURNS TRIGGER AS $$
DECLARE
    current_count INT;
    max_allowed INT;
BEGIN
    -- 获取角色最大用户数
    SELECT max_users INTO max_allowed FROM role WHERE role_id = NEW.role_id;
    
    IF max_allowed IS NOT NULL THEN
        -- 统计当前已分配用户数
        SELECT COUNT(*) INTO current_count 
        FROM user_role 
        WHERE role_id = NEW.role_id;
        
        IF current_count >= max_allowed THEN
            RAISE EXCEPTION '角色 % 已达到最大用户数限制 %', NEW.role_id, max_allowed;
        END IF;
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_check_role_cardinality
    BEFORE INSERT ON user_role
    FOR EACH ROW
    EXECUTE FUNCTION check_role_cardinality();

4.4 先决条件角色

先决条件角色约束要求用户在获得某个角色之前,必须先拥有另一个角色。这种约束用于建立角色的层级依赖关系,确保权限的渐进式授予。

4.4.1 先决条件角色的数据表设计

-- 先决条件角色关系表
CREATE TABLE role_prerequisite (
    role_id          INT NOT NULL,  -- 目标角色
    prerequisite_id  INT NOT NULL,  -- 前置角色
    PRIMARY KEY (role_id, prerequisite_id),
    FOREIGN KEY (role_id) REFERENCES role(role_id),
    FOREIGN KEY (prerequisite_id) REFERENCES role(role_id)
);

-- 检查逻辑:分配角色 R 时,用户必须已拥有 R 的所有前置角色
-- 示例:分配“高级工程师”前,用户必须已有“工程师”角色
实战案例: 某企业的角色晋升路径为:实习生 → 初级工程师 → 工程师 → 高级工程师 → 架构师。每个高级角色都以前一个角色为先决条件。这种设计天然防止了“跳级授权”的安全风险。

4.5 四种约束的综合架构

在实际系统中,四种约束需要协同工作。下图展示了约束在权限管理全生命周期中的检查位置:

RBAC2 四种约束在权限生命周期中的检查位置 角色定义 用户-角色分配 会话-角色激活 使用 约束检查: • SSD 互斥角色 • 基数约束 • 先决条件角色 约束检查: • DSD 动态互斥 约束检查总结: • 角色定义阶段:无约束检查,但需定义互斥组和基数上限 • 用户-角色分配阶段:SSD、基数约束、先决条件角色 • 会话-角色激活阶段:DSD 动态互斥 • 权限使用阶段:无额外约束,由激活的角色决定权限

4.6 约束设计的实战建议

最佳实践:
  • 优先使用 SSD: 静态约束在分配时检查,性能开销小,逻辑简单。能用 SSD 解决的场景不要用 DSD。
  • DSD 用于特殊场景: 只有需要同一人不同时段扮演互斥角色时才使用 DSD,例如值班制度。
  • 基数约束要留余量: 设置基数上限时考虑未来业务扩展,避免频繁修改。
  • 先决条件避免循环: 设计先决条件关系时,必须做环检测,防止 A→B→A 的死循环。
  • 约束组合使用: 一个角色可以同时受多种约束限制,例如“财务经理”既有基数约束(最多 1 人),又有 SSD 约束(不能同时拥有“出纳”角色)。
常见陷阱: 不要在设计阶段过度约束。约束越多,系统的灵活性越低,维护成本越高。建议从核心安全需求出发,逐步增加约束,而不是一开始就设计一个“完美”的约束体系。