土法炼钢兴趣小组的算法知识备份

【身份与访问控制工程】RBAC、ABAC、ReBAC:权限模型怎么选

文章导航

分类入口
architecturesecurity
标签入口
#authorization#RBAC#ABAC#ReBAC#XACML#permissions

目录

授权系统的设计,在业务上线前三个月看起来永远都很简单:画一张表,几个角色,CRUD 就能搞定。真正的痛苦发生在第二年。产品经理要求”某个部门的经理可以审批本部门三级以下员工的报销单,但只有在非休假状态且 IP 在公司网段时才生效”,合规团队要求”所有访问 PII 的操作必须带工单号与目的字段”,客户 CEO 要求”我作为 owner 能把某个文档 share 给外部合作伙伴,但只能 view,且 30 天后失效”。这时你翻开数据库,发现 roles 表已经膨胀到两千行,一大半的角色名长这样:finance_manager_dept_12_level_3_readonly_project_alpha

这不是产品经理不讲道理,也不是 DBA 偷懒。这是权限模型选型错了。本文从”角色爆炸”的真实病灶切入,把 RBAC(NIST 四级)、ABAC(XACML)、ReBAC(Zanzibar 启发)三种主流模型拆到 SQL 层和策略层,讲清楚它们各自能解决什么、不能解决什么,以及为什么现实系统里几乎没人只用其中一种。

权限模型对比

关于授权系统整体架构(PEP/PDP/PIP/PAP 分离、授权服务的部署形态、缓存策略)的宏观讨论,请参见 授权架构总览;关于 Zanzibar 的实现细节(zookies、Leopard 索引、一致性窗口),请参见 Zanzibar 风格权限系统。本文聚焦在模型本身——数据结构、表达力、典型陷阱与选型决策。

一、角色爆炸:RBAC 的边界

从一个报销系统说起

假设你在一家中型公司做报销系统。初版需求很清晰:

于是三个角色就够了:employeemanagerfinanceadmin。皆大欢喜。

半年后需求演变:

  1. 公司有 10 个部门,经理只能审批本部门的报销
  2. 每个部门有 5 个项目,项目经理只能审批本项目的报销
  3. 金额分 4 个档位(< 1k、1k–10k、10k–100k、> 100k),不同档位需要不同级别的审批人
  4. 有些敏感项目只有持有 secret 密级的人才能看到报销明细

如果你坚持纯 RBAC,角色数量会变成:

3 个基础角色 × 10 个部门 × 5 个项目 × 4 个金额档位 = 600 个角色

再叠加 2 个密级(normalsecret),就是 1200 个角色。每新增一个项目,角色表要 insert 12 行;新员工入职,HR 要在 6–8 个角色之间精确选择;某个项目解散,清理角色的 SQL 脚本跑半天。这就是角色爆炸(role explosion)。

角色爆炸的根因

角色爆炸的本质原因是:RBAC 把”谁能做什么”这件事完全折叠进了”谁是什么”。当授权决策需要依赖运行时上下文(金额、部门、时间、IP、密级、资源归属)时,你要么把这些维度塞进角色名(导致笛卡尔积爆炸),要么在应用代码里硬编码 if 分支(导致策略散落)。两条路都走不远。

角色爆炸并不代表 RBAC 不能用。它代表你正在用 RBAC 模型解决一个不适合 RBAC 的问题。正确的动作是识别哪些维度属于”稳定的身份属性”(适合 RBAC),哪些属于”动态的上下文”(适合 ABAC),哪些属于”资源与主体的关系”(适合 ReBAC)。

三种模型的一句话定义

模型 核心问题 授权判定依据
RBAC 谁? 用户绑定的角色
ABAC 当时是什么情况 主体、资源、动作、环境的属性组合
ReBAC 你与资源有什么关系 主体到资源的可达路径

接下来逐个拆开。

二、RBAC 模型:NIST 四级详解

RBAC 不是一个模型,而是一个模型家族。NIST 在 2000 年的论文 “The NIST Model for Role-Based Access Control: Towards a Unified Standard” 里把它分成四个渐进级别:RBAC0、RBAC1、RBAC2、RBAC3。理解这四级是用好 RBAC 的前提。

RBAC0:核心模型

RBAC0 是最朴素的版本:用户(User)、角色(Role)、权限(Permission)、会话(Session)。用户通过会话激活若干角色,激活的角色集合决定了会话能做什么。

核心的 SQL 模型:

CREATE TABLE users (
    id           BIGSERIAL PRIMARY KEY,
    username     VARCHAR(64) UNIQUE NOT NULL,
    email        VARCHAR(255) UNIQUE NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE roles (
    id           BIGSERIAL PRIMARY KEY,
    name         VARCHAR(64) UNIQUE NOT NULL,
    description  TEXT,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE permissions (
    id           BIGSERIAL PRIMARY KEY,
    resource     VARCHAR(64) NOT NULL,
    action       VARCHAR(32) NOT NULL,
    UNIQUE (resource, action)
);

CREATE TABLE user_roles (
    user_id      BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    role_id      BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    granted_at   TIMESTAMPTZ NOT NULL DEFAULT now(),
    granted_by   BIGINT REFERENCES users(id),
    PRIMARY KEY (user_id, role_id)
);

CREATE TABLE role_permissions (
    role_id        BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    permission_id  BIGINT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
    PRIMARY KEY (role_id, permission_id)
);

CREATE INDEX idx_user_roles_user ON user_roles(user_id);
CREATE INDEX idx_role_perms_role ON role_permissions(role_id);

一次授权判定的 SQL:

SELECT 1
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p ON p.id = rp.permission_id
WHERE ur.user_id = $1
  AND p.resource = $2
  AND p.action   = $3
LIMIT 1;

三次 join,索引命中时 p99 可以压在 1ms 以内。这是 RBAC 最美好的地方——结构简单,查询高效,易于审计。

RBAC1:角色继承

RBAC1 在 RBAC0 基础上引入角色层级(role hierarchy):senior_dev 自动继承 dev 的所有权限,manager 继承 senior_dev。层级既可以是”偏序”(partial order,一个角色可以继承多个父角色)也可以是”树形”(tree)。

CREATE TABLE role_inheritance (
    parent_role_id  BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    child_role_id   BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    PRIMARY KEY (parent_role_id, child_role_id),
    CHECK (parent_role_id <> child_role_id)
);

查询用户所有(含继承)角色,用 PostgreSQL 的递归 CTE:

WITH RECURSIVE effective_roles(role_id) AS (
    -- 直接绑定的角色
    SELECT role_id FROM user_roles WHERE user_id = $1
    UNION
    -- 继承链上的祖先角色
    SELECT ri.parent_role_id
    FROM role_inheritance ri
    JOIN effective_roles er ON ri.child_role_id = er.role_id
)
SELECT DISTINCT er.role_id
FROM effective_roles er;

工程注意:递归 CTE 在深层级上性能会退化,一旦角色继承深度超过 5 层,建议离线计算闭包表(closure table)或物化视图:

CREATE TABLE role_closure (
    ancestor_id    BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    descendant_id  BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    depth          INT NOT NULL,
    PRIMARY KEY (ancestor_id, descendant_id)
);

-- 每次角色继承变更时重算,或用触发器维护

闭包表的代价是写放大(每次继承变更要 insert/delete 多行),但查询变成一次等值匹配。高频读、低频写的授权场景下值得。

RBAC2:职责分离

RBAC2 引入约束(constraint),最重要的就是 SoD(Separation of Duties,职责分离),分为静态职责分离(SSD)与动态职责分离(DSD)。

CREATE TABLE role_mutex (
    role_a_id  BIGINT NOT NULL REFERENCES roles(id),
    role_b_id  BIGINT NOT NULL REFERENCES roles(id),
    type       VARCHAR(8) NOT NULL CHECK (type IN ('SSD', 'DSD')),
    PRIMARY KEY (role_a_id, role_b_id)
);

-- 分配角色前的 SSD 校验
SELECT 1
FROM role_mutex m
JOIN user_roles ur ON ur.role_id = m.role_b_id
WHERE m.role_a_id = $new_role_id
  AND m.type = 'SSD'
  AND ur.user_id = $user_id
LIMIT 1;
-- 查到即拒绝

SoD 在金融、政府、医疗系统里是硬性合规要求(SOX、PCI-DSS、HIPAA 都有相关条款),绕不过去。

RBAC3:层级 + 约束

RBAC3 就是 RBAC1 + RBAC2,同时支持角色继承与职责分离。NIST 的四级模型在 RBAC3 处收官。

RBAC 的真正边界

RBAC 能漂亮解决的问题:

RBAC 开始力不从心的地方:

这时就该看 ABAC 和 ReBAC 了。

三、ABAC 模型:属性驱动的动态授权

XACML 架构:PEP / PDP / PIP / PAP

ABAC(Attribute-Based Access Control)的标准化参考是 OASIS 的 XACML(eXtensible Access Control Markup Language),核心组件就四个缩写:

关于这套架构的部署形态(sidecar、SDK、远程调用)请看 授权架构总览

属性的四个类别

XACML 把属性分成四类,每一类都要在建模时想清楚来源与时效:

类别 含义 示例 来源
Subject 主体属性 user.departmentuser.clearanceuser.mfa_level IdP、HR 系统
Resource 资源属性 doc.ownerdoc.classificationdoc.project_id 业务数据库
Action 动作属性 action=readaction=approveaction.amount=5000 请求上下文
Environment 环境属性 timeipgeodevice_trust 运行时 / 风控

一个好的 ABAC 系统最大的成本不在策略语言,而在 PIP——属性从哪来、缓存多久、过期了怎么办、来源不可用时 fallback 什么。后面第七节会再展开。

用 Rego 表达一条 ABAC 策略

OPA 的 Rego 语言是目前工业界用 ABAC 最流行的落地方案。下面是一条报销系统的真实策略:

package expense.authz

import future.keywords.if
import future.keywords.in

default allow := false

# 规则 1:员工可以读自己的报销单
allow if {
    input.action == "read"
    input.resource.type == "expense"
    input.resource.owner == input.subject.id
}

# 规则 2:同部门经理可以审批下属报销单,但金额有上限
allow if {
    input.action == "approve"
    input.resource.type == "expense"
    input.subject.role == "manager"
    input.subject.department == input.resource.department
    input.resource.amount <= manager_limit[input.subject.level]
    not self_approval
}

# 规则 3:金额超过 10 万需要 CFO,且仅工作时间在公司网段
allow if {
    input.action == "approve"
    input.resource.type == "expense"
    input.resource.amount > 100000
    input.subject.role == "cfo"
    is_business_hours
    is_corporate_network
}

# 辅助规则
self_approval if {
    input.subject.id == input.resource.owner
}

manager_limit := {1: 10000, 2: 50000, 3: 100000}

is_business_hours if {
    now := time.clock(time.now_ns())
    now[0] >= 9
    now[0] < 20
}

is_corporate_network if {
    net.cidr_contains("10.0.0.0/8", input.environment.ip)
}

PEP 调用 PDP 时传入的 input 大致长这样:

{
    "subject": {
        "id": "u_1234",
        "role": "manager",
        "department": "finance",
        "level": 2
    },
    "action": "approve",
    "resource": {
        "type": "expense",
        "id": "exp_9876",
        "owner": "u_5678",
        "department": "finance",
        "amount": 45000
    },
    "environment": {
        "ip": "10.2.3.4",
        "time": "2026-04-21T14:30:00Z"
    }
}

PDP 返回 {"allow": true}{"allow": false},PEP 据此放行或返回 403。

ABAC 的威力与代价

ABAC 的表达力非常强。同一份策略库可以覆盖 RBAC 能干的事(input.subject.role == "manager" 就是一个退化成 RBAC 的规则),又能表达时间、地理、金额等维度。Rego / Cedar / XACML 都可以做策略热更新,策略与代码解耦,审计团队可以直接读策略文件。

代价也很明确:

  1. 属性获取的延迟与一致性:授权判定要攒齐所有属性才能开始算。user.department 如果刚在 HR 系统改了还没同步到 IdP,就会做出错误决策。PIP 要做缓存,但缓存 TTL 越长,权限变更生效越慢。
  2. 策略的可调试性:一条授权失败是哪条规则 deny 的?Rego 提供了 trace,但生产环境默认不会开。调试 ABAC 问题比 RBAC 难一个数量级。
  3. 策略的性能:Rego 虽然会做 partial evaluation 和索引优化,但如果策略量上千、属性维度多,PDP 的 p99 很容易从 1ms 涨到 10ms+。高 QPS 场景要把策略编译、属性缓存、PEP 降级路径一起考虑。
  4. 没有直接回答”谁能访问这个资源”的能力:这是 ABAC 的根本缺陷——ABAC 是正向判定的(给定用户和资源判定允许/拒绝),但列出”所有能访问文档 X 的用户”需要反向求解,理论上可能需要枚举所有用户。这也是为什么协作类产品几乎不用纯 ABAC。

四、ReBAC 模型:关系图中的权限

基本思想:关系即权限

ReBAC(Relationship-Based Access Control)把授权建模为对象图:节点是主体或资源,边是命名关系,权限由”从主体到资源的可达路径”决定。“Alice 能读 doc1” 不是因为 Alice 有某个角色,而是因为存在 Alice → member → team_A → reader → doc1 这样一条可达路径。

Google 2019 年发表 Zanzibar 论文之后,ReBAC 的工程落地成熟了起来。OpenFGA、SpiceDB、Ory Keto 都是 Zanzibar 的开源实现。

关系元组:ReBAC 的原子数据

ReBAC 的核心数据是关系元组(relation tuple),格式是:

<object>:<object_id>#<relation>@<subject>

举几个 Google Docs 风格的例子:

doc:readme#owner@user:alice
doc:readme#editor@user:bob
doc:readme#viewer@user:carol
doc:readme#parent@folder:project_docs
folder:project_docs#viewer@group:engineering#member
group:engineering#member@user:dave

读法: - 第 1 行:alice 是 readme 这个文档的 owner - 第 4 行:readme 的父目录是 project_docs 文件夹 - 第 5 行:engineering 组的所有成员都是 project_docs 的 viewer(注意 group:engineering#member 是一个 userset,表示”所有满足 group:engineering#member 关系的主体”) - 第 6 行:dave 是 engineering 组的成员

由此可以推导出:dave → member → engineering → viewer → project_docs → parent → readme,所以 dave 可以 view readme。

命名空间配置:userset rewrite

光有元组还不够,还需要告诉系统”viewer 权限怎么来”。这在 Zanzibar 里叫 namespace configuration,核心工具是 userset rewrite rules。OpenFGA 的 DSL 表达非常清晰:

model
  schema 1.1

type user

type group
  relations
    define member: [user, group#member]

type folder
  relations
    define parent: [folder]
    define owner:  [user]
    define editor: [user, group#member] or owner
    define viewer: [user, group#member] or editor or viewer from parent

type doc
  relations
    define parent: [folder]
    define owner:  [user]
    define editor: [user] or owner
    define viewer: [user] or editor or viewer from parent

这几行 DSL 编码了非常丰富的语义:

  1. editor: [user] or owner——owner 继承 editor 的权限(union)
  2. viewer: ... or editor or viewer from parent——父目录的 viewer 自动是子文档的 viewer(tuple-to-userset,也叫 TTU)
  3. member: [user, group#member]——组可以嵌套(group 的 member 也可以是 group)

一次 Check(doc:readme, viewer, user:dave) 的判定,引擎内部会做:

Check(doc:readme#viewer@user:dave)
  = Check(doc:readme#viewer@user:dave)
    OR Check(doc:readme#editor@user:dave)          # union via editor
    OR Check(folder:project_docs#viewer@user:dave) # TTU via parent
  = ...
    OR Check(group:engineering#member@user:dave)   # via folder viewer binding
  = true                                            # 直接命中元组

这种图遍历的过程可以并行化、可以 memoize,Zanzibar 原论文里给出了 10ms 内做完百万级图遍历的工程手段(Leopard 索引、zookies 一致性、区域缓存),详见 Zanzibar 深度解析

ReBAC 的独门能力

相比 RBAC 与 ABAC,ReBAC 有两个杀手锏:

  1. 资源实例级授权:每一条元组都绑定到具体资源 ID。你可以让 alice 是 doc1 的 owner,但对 doc2 完全没权限。RBAC 要做到这点需要为每个资源单独建角色,ABAC 要做到这点需要在每次判定时查资源表,而 ReBAC 这是第一公民
  2. 双向查询
    • Check(object, relation, user):用户能不能做这件事?
    • Expand(object, relation):谁能对这个资源做这件事?
    • ListObjects(user, relation, type):这个用户能操作哪些资源?(这是协作产品”我的文档列表”的实现基础)

ABAC 天然只能做 Check,后两个要靠反向求解,非常难做。ReBAC 因为数据结构就是图,三个方向都是原生的。

ReBAC 也不是银弹

ReBAC 的代价:

五、混合模型:现实系统的选择

几乎所有成熟系统都是混合模型。纯 RBAC 解决不了资源级授权,纯 ABAC 解决不了关系传递与反向查询,纯 ReBAC 解决不了动态条件。

GitHub 的组合拳

GitHub 就是一个经典案例:

一个 git push 请求的完整判定大致是:

1) 你是不是这个仓库的 collaborator(ReBAC:user → team → repo)
2) 你在这个仓库的角色是不是 >= write(RBAC:collaborator role)
3) 你的 IP 是不是在组织 allowlist 内(ABAC:environment)
4) 你的目标分支是不是被 branch protection 规则覆盖(ABAC:resource attribute)
5) 如果是,PR 是不是有足够的 reviewer approval(ReBAC+ABAC)

五个问题五种模型的组合。不混合就做不出 GitHub 这种产品。

混合模型的 SQL 建模(RBAC + ABAC)

最轻量的混合是 RBAC + ABAC:基础权限用 RBAC 管,细粒度条件通过权限上的 condition 字段用策略语言表达。

CREATE TABLE permissions (
    id           BIGSERIAL PRIMARY KEY,
    resource     VARCHAR(64) NOT NULL,
    action       VARCHAR(32) NOT NULL,
    -- ABAC 条件:一段 Rego / CEL / SQL 片段,空表示无条件
    condition    TEXT,
    UNIQUE (resource, action, condition)
);

CREATE TABLE role_permissions (
    role_id        BIGINT NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
    permission_id  BIGINT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
    PRIMARY KEY (role_id, permission_id)
);

判定流程:

def check(user, resource, action, context):
    # 第一步:RBAC 找候选 permission
    perms = db.query("""
        SELECT p.condition
        FROM user_roles ur
        JOIN role_permissions rp ON rp.role_id = ur.role_id
        JOIN permissions p ON p.id = rp.permission_id
        WHERE ur.user_id = %s
          AND p.resource = %s
          AND p.action   = %s
    """, user.id, resource.type, action)

    if not perms:
        return False

    # 第二步:对每一条 permission 评估 condition
    for (condition,) in perms:
        if condition is None:
            return True  # 无条件权限
        if evaluate_policy(condition, user, resource, context):
            return True

    return False

这种设计的好处:

混合模型(RBAC + ReBAC)

另一种常见组合是 RBAC 管组织层 + ReBAC 管资源层。这也是 GitHub、Notion、Figma 之类协作产品的主流做法。

-- 组织层用 RBAC
CREATE TABLE org_members (
    org_id    BIGINT NOT NULL REFERENCES orgs(id),
    user_id   BIGINT NOT NULL REFERENCES users(id),
    role      VARCHAR(16) NOT NULL CHECK (role IN ('owner', 'admin', 'member')),
    PRIMARY KEY (org_id, user_id)
);

-- 资源层用 ReBAC(关系元组表)
CREATE TABLE relation_tuples (
    id            BIGSERIAL PRIMARY KEY,
    namespace     VARCHAR(32) NOT NULL,
    object_id     VARCHAR(64) NOT NULL,
    relation      VARCHAR(32) NOT NULL,
    subject_ns    VARCHAR(32) NOT NULL,
    subject_id    VARCHAR(64) NOT NULL,
    subject_rel   VARCHAR(32),       -- NULL 表示直接指向主体
    UNIQUE (namespace, object_id, relation, subject_ns, subject_id, subject_rel)
);

CREATE INDEX idx_tuples_object  ON relation_tuples(namespace, object_id, relation);
CREATE INDEX idx_tuples_subject ON relation_tuples(subject_ns, subject_id);

判定时,组织管理接口查 org_members,资源访问接口查 relation_tuples。两套数据互不污染,但组织 owner 通常会通过策略 DSL 强制注入为所有资源的 admin:

type doc
  relations
    define org_admin: [org#admin]   # 组织管理员自动是所有文档的 admin
    define owner:     [user]
    define editor:    [user] or owner or org_admin

三模型混合的判定管线

在真正复杂的系统里,三种模型可能都上。典型管线:

请求进入
    │
    ▼
[身份认证]     --- JWT/Cookie 解析,得到 subject
    │
    ▼
[PEP 拦截]     --- 提取 resource、action、environment
    │
    ▼
[RBAC 门禁]    --- 是否具备基础粗粒度权限?否则直接 Deny
    │
    ▼
[ReBAC 关系]   --- 主体到资源是否可达?建立有效关系集
    │
    ▼
[ABAC 条件]    --- 时间、IP、MFA、资源属性是否满足?
    │
    ▼
[决策 & 审计]  --- Permit/Deny,写入审计日志与 zookie

分层的好处是短路优化:RBAC 几毫秒内可以挡掉绝大多数非法请求,ReBAC 只对有组织权限的请求做图遍历,ABAC 只在最后做动态条件检查。每一层失败都是审计日志里独立的 deny 原因。

六、选型决策树

没有银弹,只有权衡。下面这张表对比常见维度:

维度 RBAC ABAC ReBAC
表达力 极高 中高(关系类)
性能(单次判定) 极好(1–3 次 join) 中等(策略评估 + PIP) 中等(图遍历,可缓存)
资源实例级授权 支持(但昂贵) 原生支持
反向查询(谁能访问?) 容易 很难 原生支持
动态上下文(时间/IP) 不支持 原生支持 需扩展(caveats)
审计便利性
建模成本
运维成本 中(PIP 依赖) 高(专用存储)
角色爆炸风险
策略一致性 不涉及 难(多版本策略) 中(元组一致性)
典型代表 LDAP、传统 ERP OPA、Cedar、XACML Zanzibar、OpenFGA、SpiceDB
最佳场景 内部后台、稳定职能组织 合规系统、多维条件 协作、社交、多租户 SaaS

决策树(文字版)

1. 授权粒度只到"某类资源的某类操作"?
   └─ 是 ──> 组织稳定、角色数 < 100?
            ├─ 是 ──> [RBAC0/1/3] 纯 RBAC 够用
            └─ 否 ──> 角色爆炸风险 ──> 考虑 ABAC 或混合

2. 权限判定依赖运行时上下文(时间、IP、金额、MFA、地理)?
   ├─ 是 ──> 是否同时需要资源实例级权限?
   │         ├─ 是 ──> [ReBAC + ABAC] 混合(Caveats / Conditions)
   │         └─ 否 ──> [RBAC + ABAC] 混合
   └─ 否 ──> 继续看 3

3. 存在"主体-资源"的复杂关系(共享、嵌套、团队继承)?
   ├─ 是 ──> 需要反向查询"谁能访问 / 我能访问什么"?
   │         ├─ 是 ──> [ReBAC 为主 + RBAC 管组织]
   │         └─ 否 ──> [ReBAC 或 ABAC 都可,看团队熟悉度]
   └─ 否 ──> 回到 1

4. 合规性要求(SoD、审计、职责分离)?
   └─ 硬要求 ──> 必须有 RBAC2 或等价约束层

5. 规模指标
   ├─ 资源数 > 千万级 & QPS > 1 万 ──> ReBAC 需要专用存储(Zanzibar 风格)
   ├─ 策略数 > 数百条             ──> ABAC 要考虑策略编译/索引
   └─ 角色数 > 200 且仍在增长      ──> 强烈警示:该换模型了

几个常见具体场景的建议:

七、工程坑点

模型选对了只是起点。真正的工程挑战在实施阶段。下面是三种模型在生产环境里最常见的坑。

坑 1:RBAC 的角色爆炸

症状roles 表超过 200 行还在增长;运维抱怨新项目上线要配 30 个角色;HR 抱怨入职流程 checklist 比工资条还长。

根因识别:把角色名拆开看,如果角色名长这样 <function>_<scope>_<level>_<tenant>,说明你在用角色名编码资源维度

修复路径

  1. 识别哪些维度是”身份属性”(基础 role 保留),哪些是”资源维度”(移到 ReBAC 元组或 ABAC 属性)
  2. 引入角色模板:一个 manager 角色 + scope 字段,而不是 manager_dept1manager_dept2
  3. 渐进迁移:先上 RBAC+ABAC 混合层,新功能走新模型,老功能保留直到重构

坑 2:ABAC 的属性来源延迟与不一致

症状:用户刚被从 finance 部门调到 hr 部门,还能访问 finance 数据十分钟;或者 HR 系统宕机,所有 ABAC 判定都挂了。

根因:PIP 缓存 TTL 与属性来源 SLA 没对齐。

工程对策

  1. 属性分级:敏感属性(部门、密级、角色)缓存 TTL 设短(30s–1min);稳定属性(国籍、入职日期)可以长缓存(小时级)。
  2. 事件驱动失效:HR 系统变更时发事件到消息队列,授权服务订阅消息主动失效缓存,而不是被动等 TTL。
  3. 降级策略:属性源不可用时,授权服务要有明确的 fallback 语义——是”fail close”(拒绝)还是”fail open”(放行但标记为高风险)?必须写进 SLO 文档,不能靠开发者临场判断。
  4. 双写一致性:关键属性(如”是否离职”)不要只信一个来源,做双向核对。离职员工权限没及时收回是合规事故。

坑 3:ReBAC 的扇出与缓存失效

症状:某个顶层 group 添加一个新成员,下游 10 万个资源的 Check 缓存全部需要失效;一次大 team 的重组导致授权服务 CPU 飙高。

根因:ReBAC 的关系图是传递性的,一个元组变更可能影响大量叶子节点的判定结果。

工程对策

  1. 元组变更时不要遍历下游失效:用 zookie(Zanzibar 的一致性令牌)做 MVCC,每次写入生成新 zookie,读取时带 zookie 决定是否穿透缓存。
  2. 热点关系分层:高 fan-in 的对象(如组织级 admin 组)单独加更激进的缓存预热与副本;高 fan-out 的对象(如 CEO 能访问所有东西)避免让它出现在关系图里,走 RBAC 或直接 shortcut。
  3. 异步索引:Zanzibar 的 Leopard 索引本质是把”所有能访问对象 X 的用户集合”异步物化。适合 ListObjects / Expand 这类反向查询。
  4. 批量 Check:单个请求要判定多个资源时(列表页),一定要用批量 API 或 parallel check,否则 p99 炸穿。

坑 4:策略与数据的双写漏洞

不管是 ABAC 还是 ReBAC,只要授权数据与业务数据分开存,就有双写一致性问题。典型漏洞:

工程对策

  1. 事务内双写:把元组写在同一事务里(需要授权系统支持本地存储或 CDC)
  2. outbox 模式:业务库写 outbox 表 + 授权元组表在一个事务,后台服务投递
  3. 幂等 + reconciliation:定时扫描业务数据与授权数据,发现不一致自动修复并告警

坑 5:审计的反向可读性

合规团队最常见的两个问题:

  1. “过去 90 天,谁访问过文档 X?”
  2. “用户 Y 当前能访问哪些资源?”

纯 RBAC 两个问题都容易答。ABAC 与 ReBAC 因为有关系传递、策略条件,答起来不太直观。

对策

  1. 记录决策轨迹:每次授权决策记录使用的规则 / 路径 / 命中的元组,不只记 allow/deny
  2. 定期物化访问关系:用 Leopard 索引或等价手段,把”用户 → 可访问资源”的集合定期物化,便于合规审计直接查询
  3. 策略版本化:所有策略变更走 Git,审计时按时间点回放策略版本

坑 6:性能测试要按模型设计

压测 RBAC 授权只要压”一个用户对一个资源一个动作”的判定就够了。压测 ReBAC 完全不同,至少要覆盖:

提前在基准测试里覆盖这些场景,比上线之后救火便宜十倍。

八、选型建议

讲完机制和坑,最后给几条直接可落地的经验:

  1. 不要一开始就上 ReBAC,除非你在做协作工具。RBAC 在组织结构相对稳定的 B 端系统里能撑到百万级用户。过早引入 Zanzibar 类系统是过度工程。

  2. RBAC 看到第 200 个角色就该警觉。角色超过 500 基本意味着模型错了,再打补丁只会越陷越深。

    一个务实的迁移路径通常是:先把 RBAC 收干净,再局部补 ABAC / ReBAC。具体做法是先清掉重复角色、把角色命名和组织结构对齐;接着把最容易爆炸的那 10% 场景(例如「跨部门审批」「文档分享」「按数据密级控制」)单独抽出来,用属性或关系模型承接;最后保留 RBAC 作为兜底门禁。这样比「一次性全站切 Zanzibar / OPA」成功率高得多。

  3. ABAC 的策略代码化:把 Rego / Cedar 策略放进 Git,走 Code Review、CI、策略单测。绝对不要让运营在 UI 上手写策略条件,那是事故之源。

  4. 关键决策记录 why:一次授权决策不仅要记 “user X allow/deny action Y on resource Z”,还要记”因为命中了规则 R 或路径 P”。线上排障时 5 分钟定位比 5 小时定位,差别就在这里。

  5. 权限变更的发生要有明确事件流**:不管是角色分配、元组写入还是策略发布,都应该产生审计事件并推送到 SIEM。合规审计不是”导个 CSV” 就完事的。

  6. 模型与组织结构对齐:RBAC 的角色命名应该跟 HR 的职位体系对齐,ReBAC 的 namespace 应该跟产品的资源类型对齐,ABAC 的属性来源应该跟 IdP/HR 的权威源对齐。模型跟组织失配,代码无论写得多漂亮都救不了。

  7. 留后路:引入 ABAC/ReBAC 时保留 RBAC 层作为兜底门禁。即使策略引擎出问题,RBAC 还能守住大门,不至于所有请求都失效。

  8. 别迷信单一方案:GitHub、Google、AWS 内部都不是单一模型。你的系统能活多久,很大程度取决于能不能在合适的层用合适的模型。

九、参考资料


上一篇服务身份:mTLS、SPIFFE/SPIRE 与 Workload Identity

下一篇Zanzibar 风格权限系统

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-21 · architecture / security

【身份与访问控制工程】Zanzibar 风格权限系统

深入解析 Google Zanzibar 论文(USENIX ATC 2019)的核心设计:Relation Tuple、Namespace、Userset Rewrite,一致性模型(Zookies)与开源实现 SpiceDB、OpenFGA、Ory Keto 的工程对比,以及适用与不适用场景。

2026-04-21 · architecture / security

【身份与访问控制工程】B2B SaaS 多租户权限设计

B2B SaaS 的权限问题远比 B2C 复杂:多个企业客户、各自的内部角色体系、跨租户协作、行列级数据权限、租户自助管理。本文从隔离模型出发,给出租户内 RBAC + 租户间 ReBAC 的混合方案、超级管理员设计、行级权限实现,以及 GitHub、Slack、Notion 的权限模型速览。


By .