授权系统的设计,在业务上线前三个月看起来永远都很简单:画一张表,几个角色,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 的边界
从一个报销系统说起
假设你在一家中型公司做报销系统。初版需求很清晰:
- 普通员工可以提交自己的报销单
- 经理可以审批下属的报销单
- 财务可以打款所有已批准的报销单
- 管理员可以配置报销规则
于是三个角色就够了:employee、manager、finance、admin。皆大欢喜。
半年后需求演变:
- 公司有 10 个部门,经理只能审批本部门的报销
- 每个部门有 5 个项目,项目经理只能审批本项目的报销
- 金额分 4 个档位(< 1k、1k–10k、10k–100k、> 100k),不同档位需要不同级别的审批人
- 有些敏感项目只有持有
secret密级的人才能看到报销明细
如果你坚持纯 RBAC,角色数量会变成:
3 个基础角色 × 10 个部门 × 5 个项目 × 4 个金额档位 = 600 个角色
再叠加 2
个密级(normal、secret),就是
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)。
- SSD:同一个用户不能同时被分配互斥角色。例如”发起付款”与”审批付款”不能由同一人持有。SSD
在
user_roles插入时校验。 - 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 开始力不从心的地方:
- 授权依赖资源实例(这个用户对这个文档有权限,而不是对所有文档)
- 授权依赖运行时上下文(时间、IP、MFA 状态)
- 主体与资源有多对多的关系网(社交、协作场景)
这时就该看 ABAC 和 ReBAC 了。
三、ABAC 模型:属性驱动的动态授权
XACML 架构:PEP / PDP / PIP / PAP
ABAC(Attribute-Based Access Control)的标准化参考是 OASIS 的 XACML(eXtensible Access Control Markup Language),核心组件就四个缩写:
- PEP(Policy Enforcement Point):策略执行点。通常是业务服务里的中间件或代理,拦截请求、调用 PDP、按返回值放行或拒绝。
- PDP(Policy Decision Point):策略决策点。把属性喂给策略引擎,返回 Permit / Deny / NotApplicable / Indeterminate。
- PIP(Policy Information Point):策略信息点。PDP 评估时缺哪个属性就从 PIP 拉,典型来源有 LDAP、HR 系统、资源元数据库、风控系统。
- PAP(Policy Administration Point):策略管理点。策略的创建、版本管理、发布。
关于这套架构的部署形态(sidecar、SDK、远程调用)请看 授权架构总览。
属性的四个类别
XACML 把属性分成四类,每一类都要在建模时想清楚来源与时效:
| 类别 | 含义 | 示例 | 来源 |
|---|---|---|---|
| Subject | 主体属性 | user.department、user.clearance、user.mfa_level |
IdP、HR 系统 |
| Resource | 资源属性 | doc.owner、doc.classification、doc.project_id |
业务数据库 |
| Action | 动作属性 | action=read、action=approve、action.amount=5000 |
请求上下文 |
| Environment | 环境属性 | time、ip、geo、device_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
都可以做策略热更新,策略与代码解耦,审计团队可以直接读策略文件。
代价也很明确:
- 属性获取的延迟与一致性:授权判定要攒齐所有属性才能开始算。
user.department如果刚在 HR 系统改了还没同步到 IdP,就会做出错误决策。PIP 要做缓存,但缓存 TTL 越长,权限变更生效越慢。 - 策略的可调试性:一条授权失败是哪条规则 deny 的?Rego 提供了 trace,但生产环境默认不会开。调试 ABAC 问题比 RBAC 难一个数量级。
- 策略的性能:Rego 虽然会做 partial evaluation 和索引优化,但如果策略量上千、属性维度多,PDP 的 p99 很容易从 1ms 涨到 10ms+。高 QPS 场景要把策略编译、属性缓存、PEP 降级路径一起考虑。
- 没有直接回答”谁能访问这个资源”的能力:这是 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 编码了非常丰富的语义:
editor: [user] or owner——owner 继承 editor 的权限(union)viewer: ... or editor or viewer from parent——父目录的 viewer 自动是子文档的 viewer(tuple-to-userset,也叫 TTU)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 有两个杀手锏:
- 资源实例级授权:每一条元组都绑定到具体资源 ID。你可以让 alice 是 doc1 的 owner,但对 doc2 完全没权限。RBAC 要做到这点需要为每个资源单独建角色,ABAC 要做到这点需要在每次判定时查资源表,而 ReBAC 这是第一公民。
- 双向查询:
Check(object, relation, user):用户能不能做这件事?Expand(object, relation):谁能对这个资源做这件事?ListObjects(user, relation, type):这个用户能操作哪些资源?(这是协作产品”我的文档列表”的实现基础)
ABAC 天然只能做 Check,后两个要靠反向求解,非常难做。ReBAC 因为数据结构就是图,三个方向都是原生的。
ReBAC 也不是银弹
ReBAC 的代价:
- 运维复杂度:你需要维护一个专门的授权存储(Zanzibar 用 Spanner,OpenFGA 支持 Postgres/MySQL),元组与业务数据要双写一致。漏写一条元组就是权限漏洞。
- 策略表达力有限:ReBAC 天然不好表达”金额小于 10 万”或”工作时间才能访问”这种动态条件。SpiceDB 在 1.0 之后引入了 caveats(带条件的元组),OpenFGA 有 conditions,但本质上是把 ABAC 往 ReBAC 里塞,语义复杂度会上升。
- 调试困难:一次
Check失败,要沿着关系图回溯,看到底哪条路径缺了元组。这是 ReBAC 产品必须提供debug trace工具的原因。
五、混合模型:现实系统的选择
几乎所有成熟系统都是混合模型。纯 RBAC 解决不了资源级授权,纯 ABAC 解决不了关系传递与反向查询,纯 ReBAC 解决不了动态条件。
GitHub 的组合拳
GitHub 就是一个经典案例:
- RBAC:组织角色(owner / member / billing manager)、仓库角色(admin / maintain / write / triage / read)
- ReBAC:个人账号与仓库的所属关系、组织/团队/仓库的三层包含关系、团队嵌套
- ABAC:branch protection(谁能 push 到 main 分支?需要 reviewer 数量、status check、签名)、IP allowlist、SAML SSO enforcement
一个 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,老系统迁移时无痛
- condition 可以存 Rego 片段、CEL 表达式,也可以存一段 SQL where 子句
- 审计时
role_permissions仍然是主证据,condition 作为限制条件附加
混合模型(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 且仍在增长 ──> 强烈警示:该换模型了
几个常见具体场景的建议:
- 公司内部后台:RBAC3,简单直接
- SaaS 多租户:RBAC(租户内角色)+ ReBAC(资源共享)+ ABAC(租户级策略)
- 协作工具(文档、代码、设计):ReBAC 为主
- 金融 / 合规系统:RBAC2(SoD)+ ABAC(金额、时间、风控信号)
- 社交网络(朋友圈、动态):ReBAC(关注/好友关系决定可见性)
- 开发者平台(云控制台):RBAC(账户级)+ ABAC(资源标签)+ ReBAC(资源所属关系)
七、工程坑点
模型选对了只是起点。真正的工程挑战在实施阶段。下面是三种模型在生产环境里最常见的坑。
坑 1:RBAC 的角色爆炸
症状:roles 表超过 200
行还在增长;运维抱怨新项目上线要配 30 个角色;HR
抱怨入职流程 checklist 比工资条还长。
根因识别:把角色名拆开看,如果角色名长这样
<function>_<scope>_<level>_<tenant>,说明你在用角色名编码资源维度。
修复路径:
- 识别哪些维度是”身份属性”(基础 role 保留),哪些是”资源维度”(移到 ReBAC 元组或 ABAC 属性)
- 引入角色模板:一个
manager角色 +scope字段,而不是manager_dept1、manager_dept2 - 渐进迁移:先上 RBAC+ABAC 混合层,新功能走新模型,老功能保留直到重构
坑 2:ABAC 的属性来源延迟与不一致
症状:用户刚被从 finance
部门调到 hr 部门,还能访问 finance
数据十分钟;或者 HR 系统宕机,所有 ABAC 判定都挂了。
根因:PIP 缓存 TTL 与属性来源 SLA 没对齐。
工程对策:
- 属性分级:敏感属性(部门、密级、角色)缓存 TTL 设短(30s–1min);稳定属性(国籍、入职日期)可以长缓存(小时级)。
- 事件驱动失效:HR 系统变更时发事件到消息队列,授权服务订阅消息主动失效缓存,而不是被动等 TTL。
- 降级策略:属性源不可用时,授权服务要有明确的 fallback 语义——是”fail close”(拒绝)还是”fail open”(放行但标记为高风险)?必须写进 SLO 文档,不能靠开发者临场判断。
- 双写一致性:关键属性(如”是否离职”)不要只信一个来源,做双向核对。离职员工权限没及时收回是合规事故。
坑 3:ReBAC 的扇出与缓存失效
症状:某个顶层 group
添加一个新成员,下游 10 万个资源的 Check
缓存全部需要失效;一次大 team 的重组导致授权服务 CPU
飙高。
根因:ReBAC 的关系图是传递性的,一个元组变更可能影响大量叶子节点的判定结果。
工程对策:
- 元组变更时不要遍历下游失效:用 zookie(Zanzibar 的一致性令牌)做 MVCC,每次写入生成新 zookie,读取时带 zookie 决定是否穿透缓存。
- 热点关系分层:高 fan-in 的对象(如组织级 admin 组)单独加更激进的缓存预热与副本;高 fan-out 的对象(如 CEO 能访问所有东西)避免让它出现在关系图里,走 RBAC 或直接 shortcut。
- 异步索引:Zanzibar 的 Leopard
索引本质是把”所有能访问对象 X 的用户集合”异步物化。适合
ListObjects/Expand这类反向查询。 - 批量 Check:单个请求要判定多个资源时(列表页),一定要用批量 API 或 parallel check,否则 p99 炸穿。
坑 4:策略与数据的双写漏洞
不管是 ABAC 还是 ReBAC,只要授权数据与业务数据分开存,就有双写一致性问题。典型漏洞:
- 创建文档成功,但写入
doc:123#owner@user:alice元组失败——用户看不到自己刚建的文档 - 分享文档成功,但业务表里 share_count += 1 失败——数据统计对不上
工程对策:
- 事务内双写:把元组写在同一事务里(需要授权系统支持本地存储或 CDC)
- outbox 模式:业务库写 outbox 表 + 授权元组表在一个事务,后台服务投递
- 幂等 + reconciliation:定时扫描业务数据与授权数据,发现不一致自动修复并告警
坑 5:审计的反向可读性
合规团队最常见的两个问题:
- “过去 90 天,谁访问过文档 X?”
- “用户 Y 当前能访问哪些资源?”
纯 RBAC 两个问题都容易答。ABAC 与 ReBAC 因为有关系传递、策略条件,答起来不太直观。
对策:
- 记录决策轨迹:每次授权决策记录使用的规则 / 路径 / 命中的元组,不只记 allow/deny
- 定期物化访问关系:用 Leopard 索引或等价手段,把”用户 → 可访问资源”的集合定期物化,便于合规审计直接查询
- 策略版本化:所有策略变更走 Git,审计时按时间点回放策略版本
坑 6:性能测试要按模型设计
压测 RBAC 授权只要压”一个用户对一个资源一个动作”的判定就够了。压测 ReBAC 完全不同,至少要覆盖:
Check:p99 能不能压在 10ms 以内?Expand:一个资源的所有可访问用户集合有多大?极端情况下是不是几十万?ListObjects:一个用户的可访问资源列表查询 p99 多少?- 写入:元组写入 QPS 上限?冷启动时能不能顶住 bootstrap 时大量元组回灌?
提前在基准测试里覆盖这些场景,比上线之后救火便宜十倍。
八、选型建议
讲完机制和坑,最后给几条直接可落地的经验:
不要一开始就上 ReBAC,除非你在做协作工具。RBAC 在组织结构相对稳定的 B 端系统里能撑到百万级用户。过早引入 Zanzibar 类系统是过度工程。
RBAC 看到第 200 个角色就该警觉。角色超过 500 基本意味着模型错了,再打补丁只会越陷越深。
一个务实的迁移路径通常是:先把 RBAC 收干净,再局部补 ABAC / ReBAC。具体做法是先清掉重复角色、把角色命名和组织结构对齐;接着把最容易爆炸的那 10% 场景(例如「跨部门审批」「文档分享」「按数据密级控制」)单独抽出来,用属性或关系模型承接;最后保留 RBAC 作为兜底门禁。这样比「一次性全站切 Zanzibar / OPA」成功率高得多。
ABAC 的策略代码化:把 Rego / Cedar 策略放进 Git,走 Code Review、CI、策略单测。绝对不要让运营在 UI 上手写策略条件,那是事故之源。
关键决策记录 why:一次授权决策不仅要记 “user X allow/deny action Y on resource Z”,还要记”因为命中了规则 R 或路径 P”。线上排障时 5 分钟定位比 5 小时定位,差别就在这里。
权限变更的发生要有明确事件流**:不管是角色分配、元组写入还是策略发布,都应该产生审计事件并推送到 SIEM。合规审计不是”导个 CSV” 就完事的。
模型与组织结构对齐:RBAC 的角色命名应该跟 HR 的职位体系对齐,ReBAC 的 namespace 应该跟产品的资源类型对齐,ABAC 的属性来源应该跟 IdP/HR 的权威源对齐。模型跟组织失配,代码无论写得多漂亮都救不了。
留后路:引入 ABAC/ReBAC 时保留 RBAC 层作为兜底门禁。即使策略引擎出问题,RBAC 还能守住大门,不至于所有请求都失效。
别迷信单一方案:GitHub、Google、AWS 内部都不是单一模型。你的系统能活多久,很大程度取决于能不能在合适的层用合适的模型。
九、参考资料
- Ferraiolo, D. F., Sandhu, R., et al. The NIST Model for Role-Based Access Control: Towards a Unified Standard. RBAC 2000.
- OASIS. eXtensible Access Control Markup Language (XACML) Version 3.0. 2013.
- Pang, R., Caceres, R., Burrows, M., et al. Zanzibar: Google’s Consistent, Global Authorization System. USENIX ATC 2019.
- Open Policy Agent 文档:https://www.openpolicyagent.org/docs/
- OpenFGA 文档:https://openfga.dev/docs/
- SpiceDB / AuthZed 文档:https://authzed.com/docs/
- AWS Cedar 策略语言:https://www.cedarpolicy.com/
- 本站:授权架构总览
- 本站:Zanzibar 风格权限系统
上一篇:服务身份:mTLS、SPIFFE/SPIRE 与 Workload Identity
下一篇:Zanzibar 风格权限系统
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【身份与访问控制工程】Zanzibar 风格权限系统
深入解析 Google Zanzibar 论文(USENIX ATC 2019)的核心设计:Relation Tuple、Namespace、Userset Rewrite,一致性模型(Zookies)与开源实现 SpiceDB、OpenFGA、Ory Keto 的工程对比,以及适用与不适用场景。
【身份与访问控制工程】B2B SaaS 多租户权限设计
B2B SaaS 的权限问题远比 B2C 复杂:多个企业客户、各自的内部角色体系、跨租户协作、行列级数据权限、租户自助管理。本文从隔离模型出发,给出租户内 RBAC + 租户间 ReBAC 的混合方案、超级管理员设计、行级权限实现,以及 GitHub、Slack、Notion 的权限模型速览。
【系统架构设计百科】授权架构:RBAC、ABAC 与策略引擎
授权是安全架构的核心环节。本文从 RBAC 的角色爆炸问题出发,深入剖析 ABAC、ReBAC 与 Google Zanzibar 模型,并结合 OPA 策略引擎的集成实践,给出权限数据存储与缓存的工程方案。
【身份与访问控制工程】OAuth 2.1 与 PKCE:现代授权主路径
从一次 SPA 安全事故出发,系统梳理 OAuth 2.1 相对 2.0 的收敛动作、PKCE 的密码学原理、授权码流程的完整参数细节,以及 DPoP、PAR、JAR、RAR 等现代扩展与常见攻击面