1. 第一章:为什么需要权限系统
1.1 从一个故事开始
想象你经营一家公司,公司有一间档案室。最初只有三个人,大家彼此信任,档案室的门从不上锁。
随着公司发展到 30 人,你发现:
-
实习生不小心删除了重要合同
-
销售看到了不该看的薪资表
-
离职员工仍然能进入档案室
于是你开始锁门,给不同的人发不同的钥匙。这就是权限系统的起源——当信任不再是默认的,我们需要一套规则来回答:「谁」可以对「什么」做「什么操作」。
1.2 权限系统的本质
从第一性原理出发,权限系统要回答的核心问题只有一个:
主体(Subject)能否对客体(Object)执行某个动作(Action)?
主体(Subject) → 你(员工)
客体(Object) → 某扇门(资源)
操作(Action) → 打开(动作)
例如:
-
张三(主体)能否查看(动作)合同文档(客体)?
-
工程团队(主体)能否编辑(动作)代码仓库(客体)?
-
匿名用户(主体)能否访问(动作)公开页面(客体)?
所有权限系统,无论多复杂,本质上都是在用不同的方式回答这个问题。区别在于——用什么样的数据结构和计算方式来高效、准确地给出答案。
1.3 企业权限系统的原则与挑战
一句话总结:让正确的人,在正确的时间,访问正确的资源,执行正确的操作。
-
最小权限原则:每个人只拥有完成工作所需的最小权限
-
职责分离原则:关键操作需要多人协同,防止单点腐败
-
可审计原则:谁在什么时候对什么做了什么,必须有迹可循
| 挑战 | 说明 | 举例 |
|---|---|---|
| 正确性 | 权限判断必须 100% 准确,错放等于安全漏洞,错禁等于功能故障 | 用户应该能看到自己的订单,但不能看到别人的 |
| 灵活性 | 业务规则千变万化,权限模型必须能表达复杂的组织关系 | 部门经理可以审批下属的报销,但不能审批平级的 |
| 性能 | 每个 API 请求都可能触发权限检查,延迟必须极低 | 一个页面加载可能触发几十次权限检查,总耗时不能超过 100ms |
2. 第二章:权限模型的演进之路
理解权限模型的演进,就像理解从手工记账到 ERP 系统的演变——每一次进化都是为了解决前一代模型的局限性。
2.1 第一代:访问控制列表(ACL)
核心思想:给每个资源维护一张"谁可以访问"的列表。
合同文档.doc → [张三: 读写, 李四: 只读, 王五: 只读]
薪资表.xlsx → [HR-小刘: 读写, 财务-老陈: 只读]
优点:直观、简单、容易理解。
致命缺陷:当公司有 1000 个人和 10000 个文档时,理论上需要维护 1000 × 10000 = 一千万条权限记录。
-
新员工入职要逐个添加到数百个资源上
-
调岗要逐个修改数百条记录
-
无法表达"整个部门的人都可以访问"这种规则
类比:这就像给公司每扇门都单独配一把钥匙,每个人要带一大串钥匙出门。
2.2 第二代:基于角色的访问控制(RBAC)
核心思想:引入「角色」作为中间层,将权限赋予角色,再将角色赋予用户。
角色定义:
管理员 → [读, 写, 删除, 管理]
编辑者 → [读, 写]
查看者 → [读]
角色分配:
张三 → 管理员
李四 → 编辑者
实习生小王 → 查看者
优点:大幅减少管理复杂度。新人入职只需分配角色,不需要逐个设置资源。
局限性:
-
角色爆炸:当业务复杂后,你需要为每种细微差异创建角色——"华东区-销售-高级"、"华南区-销售-初级"、"总部-销售-管理者"……角色数量可能超过用户数量。
-
无法表达关系:RBAC 无法自然表达"张三可以查看他自己创建的文档"、"经理可以审批下属的报销单"这类依赖于对象间关系的规则。
- 缺乏层级继承:项目文件夹下的文档,不能自动继承文件夹的权限。
- 缺乏上下文感知:"只有在工作时间内才能访问"这种条件,RBAC 无法表达。
2.3 第三代:基于属性的访问控制(ABAC)
核心思想:权限判断不仅取决于"谁"和"什么资源",还取决于运行时的属性和环境。
规则:只有在工作日的 9:00-18:00,且从公司 IP 登录时,才能访问财务报表。
属性:
用户属性:部门=财务部
资源属性:敏感度=高
环境属性:时间=14:30, IP=10.0.1.x
优点:极其灵活,能表达几乎任何规则。
局限性:
-
规则难以理解和审计:当规则复杂到数百条时,没人能说清"张三到底能访问哪些文档"
-
性能问题:每次判断都要实时计算大量属性
-
难以回答反向查询:"谁能访问这份文档?"需要遍历所有用户和规则
类比:这就像给每扇门装了一个智能锁,能根据人脸、时间、天气等各种因素决定开不开。功能强大,但你永远不确定它什么时候会把你锁在外面。
2.4 第四代:基于关系的访问控制(ReBAC)
2019 年,Google 发表了 Zanzibar 论文,揭示了其内部统一权限系统的设计。这个系统支撑着 Google Drive、YouTube、Google Cloud 等数十亿用户级别的产品。
核心思想:权限由对象之间的「关系」决定。不再是"张三有编辑角色",而是"张三是这篇文档的编辑者"。
传统 RBAC 思维:
张三 拥有 "文档编辑者" 角色 → 张三可以编辑文档
ReBAC 思维:
张三 是 文档A 的 编辑者 → 张三可以编辑文档A
文档A 属于 文件夹X → 文件夹X的查看者可以查看文档A
张三 是 工程团队 的 成员 → 张三可以访问工程团队的所有资源
核心优势:
-
关系即权限:权限自然地嵌入在业务关系中——你是项目成员,所以你能访问项目文档;你是团队经理,所以你能审批团队报销。
-
层级继承:文件夹的权限自动传递给文件夹内的文档,组织的权限传递给下属团队。
-
可反向查询:可以高效回答"张三能访问哪些文档"和"谁能访问这份文档"。
-
关系可组合和扩展:用交集、并集、差集等集合运算组合复杂规则。不需要预定义所有角色,随时定义新的关系类型。
这就是 Google 在其内部系统 Zanzibar 中采用的方案,也是 OpenFGA 项目实现的核心模型。
2.5 演进总结
ACL ──→ RBAC ──→ ABAC ──→ ReBAC
简单 角色 属性 关系图
每一代都不是取代前一代,而是包含前一代:
- ReBAC 可以用"角色"关系模拟 RBAC,RBAC 是 ReBAC 的子集——"角色"只是一种特殊的"关系"
- ReBAC 可以用"条件"扩展支持 ABAC
- ReBAC 的直接关系就是 ACL
| 维度 | ACL | RBAC | ABAC | ReBAC |
|---|---|---|---|---|
| 管理复杂度 | 高 | 低 | 中 | 低 |
| 表达能力 | 弱 | 中 | 强 | 强 |
| 层级继承 | 不支持 | 不支持 | 需编码 | 原生支持 |
| 反向查询 | 困难 | 一般 | 极难 | 原生支持 |
| 性能 | 快 | 快 | 慢 | 快 |
| 审计友好 | 是 | 是 | 否 | 是 |
3. 第三章:ReBAC 的第一性原理
3.1 万物皆关系
让我们回到最基本的思考。在现实世界中,权限几乎总是由「关系」决定的:
-
你能进自己家的门——因为你是这个房子的住户
-
你能查看班级成绩——因为你是这个班的老师
-
你能审批报销单——因为你是提交人的上级
ReBAC 的核心洞察就是:与其定义抽象的"权限规则",不如直接建模现实世界中的关系,权限自然地从关系中推导出来。
3.2 原子单元:三元组(Tuple)
ReBAC 的一切都建立在一个极其简单的数据结构上——三元组(Tuple):
(客体, 关系, 主体)
(Object, Relation, User)
每个三元组表达一个事实:"某个主体与某个客体之间存在某种关系"。
(document:合同A, viewer, user:张三) → 张三是合同A的查看者
(folder:法务, member, team:法务部) → 法务部是法务文件夹的成员
(org:字节跳动, employee, user:李四) → 李四是字节跳动的员工
在 OpenFGA 的实现中,三元组的字符串表示为:
document:合同A#viewer@user:张三
<客体类型:客体ID>#<关系>@<主体类型:主体ID>
这就是 ReBAC 的"原子"——所有复杂的权限逻辑,都是由这些简单的三元组组合而成的。
3.3 主体的三种形态
三元组中的"主体"(User)不只是一个人,它有三种形态:
| 形态 | 格式 | 含义 | 示例 |
|---|---|---|---|
| 具体用户 | type:id | 一个具体的人或服务 | user:张三 |
| 用户集合 | type:id#relation | 某个对象的某类关系人的集合 | team:工程部#member(工程部的所有成员) |
| 通配符 | type:* | 某类型的所有实体 | user:*(所有用户,即公开访问) |
用户集是 ReBAC 的精髓所在——它让权限可以通过关系链传递。你可以说"工程部的所有成员都可以查看这份文档",而不需要逐个列举。
(document:设计稿, viewer, team:工程部#member)
这一条三元组,就等价于给工程部的所有人(不管有多少个)都赋予了查看权限。而且当有新人加入工程部时,他自动获得这个权限;有人离开工程部时,权限自动收回。
3.4 类型与关系:权限的"蓝图"
三元组描述的是具体的关系数据,那什么来定义"可以存在哪些关系"呢?这就是授权模型(Authorization Model)的作用。
授权模型就像数据库的 Schema,它定义了:
-
系统中有哪些类型(Type)
-
每种类型可以有哪些关系(Relation)
-
每种关系的定义规则是什么
用 OpenFGA 的 DSL(领域特定语言)表示:
model
schema 1.1
type user # 定义用户类型
type document # 定义文档类型
relations
define owner: [user] # 文档可以有 owner 关系,直接指向 user
define editor: [user] # 文档可以有 editor 关系,直接指向 user
define viewer: [user] or editor or owner
# viewer 可以是:直接指定的用户 OR 编辑者 OR 所有者
model / schema 1.1:声明模型和版本,这个模型告诉系统:
-
世界上有
user和document两种实体-
type user:定义了一种类型"user"(用户),它没有关系定义,是纯粹的"主体"类型 -
type document:定义了一种类型"document"(文档)
-
-
文档可以有
owner、editor、viewer三种关系-
文档的
viewer包括:直接指定的查看者+ 所有编辑者 + 所有所有者 -
类型限制的意义:
[user]就像数据库的外键约束。它确保你不会意外写入一条document:1#viewer@folder:x这样无意义的元组(文件夹怎么能是查看者呢?)
-
模型是"规则",三元组是"数据"。规则定义了权限如何计算,数据记录了具体的关系事实。
3.5 权限判断:图的遍历
当系统收到一个权限查询——"张三能不能查看文档A?"——它实际上在做一次图遍历:
Check(document:文档A, viewer, user:张三)
思考过程:
1. viewer 的定义是 [user] or editor or owner
2. 路径1:张三是不是直接被指定为 viewer? → 查找三元组
3. 路径2:张三是不是 editor? → 查找三元组
4. 路径3:张三是不是 owner? → 查找三元组
5. 任一路径为真 → 允许访问
这就像在一张关系图上,从目标节点出发,沿着不同的路径搜索,看能不能找到请求的用户。
关键洞察:权限检查本质上是一个图搜索问题。 OpenFGA 的核心引擎就是一个高度优化的图遍历引擎。
4. 第四章:核心概念——用企业场景说话
4.1 场景设定:一个 SaaS 文档协作平台
为了让概念不再抽象,我们以一个真实的企业场景贯穿整个教程——一个类似飞书/Notion 的文档协作平台。
业务需求:
-
公司内有多个团队
-
每个团队有自己的文件夹空间
-
文件夹下可以创建文档
-
不同角色(所有者、编辑者、查看者)有不同权限
-
团队文件夹的权限应该能继承到文档
-
有些文档可以公开给全公司
4.2 类型(Type)——给世界建模
类型是你系统中实体的分类。 设计权限系统的第一步,就是识别你系统中有哪些类型。
type user # 用户——系统中的人
type team # 团队——人的集合
relations
define member: [user] # 团队有成员
type folder # 文件夹——文档的容器
relations
define owner: [user] # 文件夹所有者
define viewer: [user, team#member] # 文件夹查看者:可以是用户,也可以是团队成员
type document # 文档——核心业务对象
relations
define parent: [folder] # 文档所属的文件夹
define owner: [user] # 文档所有者
define editor: [user, team#member] # 文档编辑者
define viewer: [user, user:*] or editor or owner or viewer from parent
# 文档查看者 = 直接指定的用户 + 所有人(公开) + 编辑者 + 所有者 + 父文件夹的查看者
设计原则:
类型不是越多越好,而是要与你的业务领域模型对齐。每个类型应该对应一个需要做权限控制的实体。
4.3 关系(Relation)——连接万物
关系是类型之间的纽带。设计关系时,思考方式是:"对于这个类型的对象,谁会与它产生关联?以什么方式关联?"
关系的定义有几种方式:
4.3.1 直接关系 —— 明确指定
define viewer: [user]
含义:viewer 这个关系只能通过直接写入三元组来建立。
适用场景:明确的权限分配,如"把张三加为这个文档的编辑者"。
4.3.2 计算关系 —— 从其他关系推导
define viewer: editor
含义:所有 editor 自动也是 viewer(角色继承)。
适用场景:权限层级——所有者 > 编辑者 > 查看者,高级角色自动拥有低级角色的权限。
4.3.3 传递关系 —— 跨对象继承
define viewer: viewer from parent
含义:如果用户是父级对象的 viewer,那他也是当前对象的 viewer。
适用场景:文件夹→文档的权限继承,组织→团队的权限继承。
4.3.4 集合运算 —— 组合逻辑
define viewer: [user] or editor or viewer from parent # 并集(OR)
define can_delete: owner and approved # 交集(AND)
define can_view: [user] but not blocked # 差集(BUT NOT)
适用场景:复杂业务规则的表达。
4.4 三元组(Tuple)——关系的实例数据
授权模型定义好之后,就可以写入具体的关系数据了:
# 张三是工程团队的成员
team:工程团队#member@user:张三
# 李四是工程团队的成员
team:工程团队#member@user:李四
# 工程团队的成员可以查看"技术方案"文件夹
folder:技术方案#viewer@team:工程团队#member
# "API设计文档"的父级是"技术方案"文件夹
document:API设计文档#parent@folder:技术方案
# 张三是"API设计文档"的编辑者
document:API设计文档#editor@user:张三
有了这些数据,系统就可以自动推导出:
-
✅ 张三可以查看"API设计文档"(他是编辑者,编辑者自动是查看者)
-
✅ 李四可以查看"API设计文档"(他是工程团队成员 → 可以查看技术方案文件夹 → 文档继承文件夹权限)
-
❌ 王五不可以查看"API设计文档"(他不在任何相关关系链上)
4.5 Store——多租户隔离
在企业级 SaaS 场景中,你需要为不同的客户/租户隔离权限数据。OpenFGA 通过 Store 概念实现:
Store A (客户: 字节跳动)
├── 授权模型 v1
├── 授权模型 v2 (当前生效)
└── 关系数据 (三元组)
Store B (客户: 腾讯)
├── 授权模型 v1 (当前生效)
└── 关系数据 (三元组)
每个 Store 拥有:
-
独立的授权模型(可多版本)
-
独立的关系数据
-
完全的数据隔离
这意味着不同租户可以有完全不同的权限结构,互不影响。
5. 第五章:六大关系模式详解
OpenFGA 提供了六种关系定义模式,从简单到复杂,它们就像乐高积木一样可以自由组合,构建出任意复杂的权限规则。
5.1 模式一:直接授权关系
场景:用户被明确地、直接地分配到某个关系。
模型:
model
schema 1.1
type user
type document
relations
define viewer: [user]
写入数据:
document:1#viewer@user:zhangsan
判断:
Check: document:1#viewer@user:zhangsan → ✅ true
Check: document:1#viewer@user:lisi → ❌ false
Check: document:2#viewer@user:zhangsan → ❌ false(不同文档)
适用场景:个人分享、指定审批人、临时授权。
产品经理视角:这就是"管理员手动给某人授予某个资源的某个权限"。适用于最精确的、一对一的权限分配。
5.2 模式二:计算关系(权限继承)
场景:一个关系自动继承另一个关系的所有成员。
模型:
type user
type document
relations
define owner: [user]
define editor: [user] or owner # 编辑者 = 直接编辑者 + 所有者
define viewer: [user] or editor # 查看者 = 直接查看者 + 编辑者(含所有者)
数据:
document:项目计划#owner@user:张三
判断:
Check: user:张三 是 document:项目计划 的 owner? ✅
Check: user:张三 是 document:项目计划 的 editor? ✅(owner 自动是 editor)
Check: user:张三 是 document:项目计划 的 viewer? ✅(editor 自动是 viewer)
设计技巧:权限从高到低形成链式继承:owner → editor → viewer。只需要写一条三元组,三个级别的权限全部生效。
权限金字塔:
owner(最高权限)
↓ 自动包含
editor(中等权限)
↓ 自动包含
viewer(基础权限)
产品经理视角:这就是"编辑者自动拥有查看权限"。你不需要给编辑者额外授予查看权限——这是从权限模型中自动推导出来的。这在产品设计中极其常见:管理员 ⊃ 编辑者 ⊃ 查看者。
5.3 模式三:组织结构与分组
场景:团队成员自动获得团队资源的访问权限。当新人加入团队时自动获得权限,离开时自动失去。
模型:
type user
type team
relations
define member: [user, team#member] # 成员可以是用户,也可以是其他团队的成员(嵌套组)
type document
relations
define viewer: [team#member] # 某个团队的所有成员可以查看
数据:
# 张三和李四是前端团队成员
team:前端#member@user:张三
team:前端#member@user:李四
# 前端团队是工程部的子团队(嵌套组)
team:工程部#member@team:前端#member
# 王五直接是工程部成员
team:工程部#member@user:王五
# 工程部成员可以查看架构文档
document:架构文档#viewer@team:工程部#member
判断:
Check: user:张三 是 document:架构文档 的 viewer?
→ 张三是前端#member → 前端#member 是 工程部#member → 工程部#member 是 架构文档的 viewer
→ ✅ 是
Check: user:王五 是 document:架构文档 的 viewer?
→ 王五直接是 工程部#member → 工程部#member 是 架构文档的 viewer
→ ✅ 是
核心价值:组的嵌套可以无限层级,完美映射企业的组织架构树。
公司
├── 工程部
│ ├── 前端团队 → [张三, 李四]
│ ├── 后端团队 → [赵六, 钱七]
│ └── 王五(直接成员)
└── 产品部
└── 产品团队 → [孙八]
5.4 模式四:层级继承(Tuple-to-Userset)
含义:通过对象之间的关系,将权限从一个对象传递到另一个对象。
场景:文件夹的权限自动传递到文件夹内的文档。
这是 ReBAC 最强大的模式之一,它让你不需要给每个文档单独设置权限——只要设置文件夹级别的权限,文档自动继承。
模型:
type user
type folder
relations
define viewer: [user]
type document
relations
define parent: [folder] # 文档属于哪个文件夹
define viewer: [user] or viewer from parent # 文档的查看者 = 直接查看者 + 父文件夹的查看者
关键语法:viewer from parent
这句话的含义是:"找到当前文档的 parent(父文件夹),然后检查目标用户是否是那个父文件夹的 viewer。"
数据:
# 张三可以查看"产品文档"文件夹
folder:产品文档#viewer@user:张三
# "需求规格说明书"属于"产品文档"文件夹
document:需求规格说明书#parent@folder:产品文档
# "竞品分析"属于"产品文档"文件夹
document:竞品分析#parent@folder:产品文档
判断:
Check: user:张三 是 document:需求规格说明书 的 viewer?
→ 路径1:张三是直接 viewer?→ 没有这条三元组 → ❌
→ 路径2:viewer from parent → 需求规格说明书的 parent 是 folder:产品文档
→ 张三是 folder:产品文档 的 viewer? → ✅ 是!
→ 最终结果:✅(路径2通过)
核心价值:一次设置,整个文件夹树下的所有文档都继承权限。这在大型企业中可以减少数万条权限配置。
组织空间
├── 工程文件夹 (viewer: 工程部#member)
│ ├── 前端文档 → 自动继承工程文件夹权限
│ ├── 后端文档 → 自动继承工程文件夹权限
│ └── 架构设计 → 自动继承工程文件夹权限
└── 产品文件夹 (viewer: 产品部#member)
├── PRD → 自动继承产品文件夹权限
└── 竞品分析 → 自动继承产品文件夹权限
5.5 模式五:集合运算(交集与差集)
5.5.1 交集(AND)——必须同时满足
场景:只有既是付费用户又被授权的人才能访问高级功能。
模型:
type user
type feature
relations
define subscriber: [user] # 付费订阅者
define allowed_user: [user] # 被授权的用户
define can_access: subscriber and allowed_user # 必须同时满足
数据:
feature:高级报表#subscriber@user:张三
feature:高级报表#subscriber@user:李四
feature:高级报表#allowed_user@user:张三
feature:高级报表#allowed_user@user:王五
判断:
Check: user:张三 → can_access? ✅(既是 subscriber 又是 allowed_user)
Check: user:李四 → can_access? ❌(只是 subscriber,不是 allowed_user)
Check: user:王五 → can_access? ❌(只是 allowed_user,不是 subscriber)
5.5.2 差集(BUT NOT)——排除特定人群
场景:所有人可以查看,但被拉黑的人除外。
模型:
type user
type document
relations
define viewer: [user]
define blocked: [user]
define can_view: viewer but not blocked # 查看者中排除被拉黑的人
数据:
document:公告#viewer@user:张三
document:公告#viewer@user:李四
document:公告#blocked@user:李四
判断:
Check: user:张三 → can_view? ✅(是 viewer 且没被 blocked)
Check: user:李四 → can_view? ❌(是 viewer 但也被 blocked 了)
适用场景:黑名单机制、封禁机制、合规限制。
5.6 模式六:条件授权(ABAC 扩展)
场景:权限不仅取决于关系,还取决于运行时的属性条件(如时间、地点、金额等)。
ReBAC 可以通过**条件(Condition)**扩展支持 ABAC 场景。
模型:
type user
type document
relations
define viewer: [user with within_business_hours]
condition within_business_hours(current_hour: int) {
current_hour >= 9 && current_hour < 18
}
数据:
document:机密报告#viewer@user:张三 (condition: within_business_hours)
判断:
Check(document:机密报告, viewer, user:张三, context={current_hour: 10})
→ 条件计算: 10 >= 9 && 10 < 18 → true
→ ✅ 允许
Check(document:机密报告, viewer, user:张三, context={current_hour: 22})
→ 条件计算: 22 >= 9 && 22 < 18 → false
→ ❌ 拒绝(非工作时间)
更多条件示例:
# 金额限制:单笔审批金额不超过10万
condition amount_limit(amount: double) {
amount <= 100000.0
}
# 时间限制:在有效期内
condition before_expiry(expiry: timestamp, current: timestamp) {
current < expiry
}
# IP 限制:只能从办公网络访问
condition from_office_network(ip_prefix: string) {
ip_prefix == "10.0."
}
核心价值:在不放弃 ReBAC 图模型优势的前提下,融入 ABAC 的灵活性。关系决定"谁有资格",条件决定"资格是否当前有效"。
5.7 六大模式总结
| 模式 | 关键语法 | 解决的问题 | 企业场景 |
|---|---|---|---|
| 直接授权 | [user] | 点对点授权 | 文档分享、指定审批人 |
| 权限继承 | editor or owner | 权限层级 | owner > editor > viewer |
| 组织分组 | [team#member] | 按组授权 | 部门权限、项目组权限 |
| 层级继承 | viewer from parent | 资源树权限继承 | 文件夹→文档、组织→团队 |
| 集合运算 | and / but not | 组合条件 | 付费+授权、查看-拉黑 |
| 条件授权 | [user with condition] | 运行时属性 | 时间限制、金额限制 |
6. 第六章:三个核心查询——权限系统的 API 设计
一个权限系统对外提供的 API,本质上就是三个查询和一个写入操作。理解这四个 API,就理解了权限系统的全部能力。
6.1 Check——"这个人能不能做这件事?"
这是权限系统最核心、调用频率最高的 API。
请求: Check(object: "document:合同A", relation: "viewer", user: "user:张三")
响应: { allowed: true }
调用时机:每当用户尝试执行一个操作时——打开文档、点击编辑、提交审批——你的应用都会调用 Check。
工作原理:
Check 本质上是在关系图上做一次搜索。以下面的场景为例:
模型: document 的 viewer = [user] or editor or viewer from parent
数据:
document:合同A#parent@folder:法务
folder:法务#viewer@team:法务部#member
team:法务部#member@user:张三
查询: Check(document:合同A, viewer, user:张三)
系统的搜索过程像一棵展开的决策树:
document:合同A#viewer@user:张三?
├── 路径1: 直接 viewer? → 查数据库 → 没找到 ❌
├── 路径2: 是 editor? → 查数据库 → 没找到 ❌
└── 路径3: viewer from parent?
→ 合同A 的 parent 是谁? → folder:法务
→ folder:法务#viewer@user:张三?
└── 路径3.1: 直接 viewer? → 查数据库 → 没找到 ❌
但找到了 folder:法务#viewer@team:法务部#member
→ user:张三 是 team:法务部#member 吗?
→ 查数据库 → 找到 team:法务部#member@user:张三 ✅
结果:✅ 允许(通过路径3,经过了两次跳转才找到)
性能特点:
-
平均查询时间应在毫秒级(< 10ms)
-
支持缓存中间结果以加速重复查询
-
支持设定搜索深度限制(防止无限递归)
6.2 ListObjects——"这个人能访问哪些东西?"
请求: ListObjects(user: "user:张三", type: "document", relation: "viewer")
响应: { objects: ["document:合同A", "document:需求文档", "document:周报模板"] }
调用时机:用户打开文件列表页面时,需要展示"他能看到哪些文档"。
工作原理(两阶段算法):
阶段1: 反向展开(Reverse Expand)
从 user:张三 出发,沿关系图反向搜索,收集所有可达的 document 对象。
→ 直接是某文档的 viewer/editor/owner → 加入结果
→ 是某团队的 member → 团队是某文件夹的 viewer → 文件夹下的文档 → 加入候选
阶段2: 验证候选(Check 验证)
对于涉及交集(AND)或差集(BUT NOT)的候选对象,
逐个调用 Check 确认是否真的有权限。
→ 通过验证 → 加入最终结果
为什么需要两个阶段?
对于简单的并集(OR)关系,阶段1 就够了。但当模型中有交集或差集时,仅凭反向展开可能找到"疑似有权限"的对象,需要通过 Check 进一步确认。
举例:
模型: document 的 can_edit = editor and not_frozen
阶段1 反向展开找到张三是 document:A 和 document:B 的 editor → 候选 [A, B]
阶段2 Check 验证:
- document:A not_frozen? → true → ✅ 加入结果
- document:B not_frozen? → false(文档被冻结)→ ❌ 排除
最终结果: [document:A]
6.3 ListUsers——"谁能访问这个东西?"
请求: ListUsers(object: "document:合同A", relation: "viewer", user_filters: ["user"])
响应: { users: ["user:张三", "user:李四", "user:王五"] }
调用时机:管理员查看"谁有权限访问这份敏感文档"时;权限审计时。
工作原理:
与 ListObjects 方向相反——从对象出发,沿关系图正向展开,收集所有可达的用户。
document:合同A 的 viewer 有谁?
├── 直接 viewer: user:张三
├── editor(自动是 viewer): user:李四
├── viewer from parent:
│ └── folder:法务 的 viewer:
│ └── team:法务部#member:
│ ├── user:王五
│ └── team:子团队#member:
│ └── user:赵六
└── 结果: [user:张三, user:李四, user:王五, user:赵六]
6.4 Write——"建立或删除关系"
请求: Write(
writes: [
{ object: "document:合同A", relation: "editor", user: "user:张三" },
{ object: "document:合同A", relation: "viewer", user: "user:李四" }
],
deletes: [
{ object: "document:合同A", relation: "viewer", user: "user:王五" }
]
)
特点:
-
事务性:一次请求中的所有写入和删除要么全部成功,要么全部失败
-
批量操作:每次最多 100 条三元组
-
即时生效:写入后立刻影响后续的 Check 结果
6.5 四个 API 的协作关系
┌──────────┐
管理员设置权限 ──→│ Write │──→ 写入关系数据
└──────────┘
│
▼
┌──────────────┐
│ 关系数据存储 │
└──────────────┘
↗ ↑ ↖
/ │ \
┌──────┐ ┌────────────┐ ┌──────────┐
│Check │ │ListObjects │ │ListUsers │
└──────┘ └────────────┘ └──────────┘
↑ ↑ ↑
用户操作时 用户查看列表时 管理员审计时
7. 第七章:产品经理的权限设计思维框架
当你面对一个新系统的权限设计需求时,按以下四步走:
7.1 第一步:枚举资源类型
列出系统中所有需要权限控制的资源。
问自己:系统中有哪些"东西"需要被保护?
示例(项目管理工具):
- 组织(Organization)
- 团队(Team)
- 项目(Project)
- 任务(Task)
- 评论(Comment)
- 文件(File)
7.2 第二步:定义关系类型
为每种资源定义有意义的关系(角色)。
问自己:对于每种资源,人和它之间有哪些有意义的关系?
示例:
- Project: owner, admin, editor, viewer
- Task: assignee, reporter, viewer
- File: uploader, viewer
7.3 第三步:绘制资源层级图
明确资源之间的从属关系,这决定了权限如何传递。
Organization
└── Team
└── Project
├── Task
└── File
7.4 第四步:定义权限传递规则
用自然语言描述权限如何沿层级传递,然后翻译成 OpenFGA DSL。
自然语言:
- 组织管理员自动成为所有项目的管理员
- 项目成员可以查看项目下的所有任务
- 任务的创建者自动成为任务的报告者
翻译成 DSL:
type project
relations
define org: [organization]
define admin: [user] or admin from org
define viewer: [user] or member from team or admin
8. 第八章:实战——从零设计一个 SaaS 协作平台的权限体系
现在让我们把所有知识串起来,完整设计一个企业级 SaaS 平台的权限系统。
8.1 业务需求分析
假设我们正在建设「CloudWork」——一个 B2B 项目管理与文档协作平台:
组织结构:
-
每个客户公司是一个「组织」
-
组织下有多个「团队」
-
团队可以嵌套(如:工程部 > 前端组)
-
用户可以属于多个团队
资源结构:
-
组织下有「项目」
-
项目下有「文件夹」
-
文件夹下有「文档」
-
文档可以有「评论」
权限需求:
-
组织管理员可以管理组织内的一切
-
项目有四种角色:管理员、编辑者、评论者、查看者
-
文件夹权限应继承到子文档
-
文档可以单独分享给组织外的人(访客)
-
某些文档可以设为全组织公开
-
合规要求:离职员工立即失去所有权限
8.2 第一步:识别类型
从业务需求中提取实体类型:
type user # 用户(员工、外部访客)
type organization # 组织(客户公司)
type team # 团队
type project # 项目
type folder # 文件夹
type document # 文档
type comment # 评论
8.3 第二步:设计关系
对每个类型,思考"谁会与它产生什么关系":
model
schema 1.1
# ============================================
# 基础类型
# ============================================
type user
# ============================================
# 组织——权限体系的顶层
# ============================================
type organization
relations
# 组织管理员:可以管理一切
define admin: [user]
# 组织成员:所有员工
define member: [user] or admin
# ============================================
# 团队——人的分组,支持嵌套
# ============================================
type team
relations
# 团队所属的组织
define org: [organization]
# 团队成员:可以是用户,也可以是其他团队的成员(嵌套)
define member: [user, team#member]
# ============================================
# 项目——业务工作的容器
# ============================================
type project
relations
# 项目所属的组织
define org: [organization]
# 四级权限层级
define admin: [user, team#member] or admin from org
define editor: [user, team#member] or admin
define commenter: [user, team#member] or editor
define viewer: [user, organization#member, team#member] or commenter
# ============================================
# 文件夹——资源容器,支持层级继承
# ============================================
type folder
relations
# 所属项目
define project: [project]
# 父文件夹(支持文件夹嵌套)
define parent: [folder]
# 权限定义:直接授权 + 从父文件夹继承 + 从项目继承
define editor: [user, team#member] or editor from parent or editor from project
define viewer: [user, team#member] or editor or viewer from parent or viewer from project
# ============================================
# 文档——核心业务对象
# ============================================
type document
relations
# 所属文件夹
define parent: [folder]
# 所有者(创建者)
define owner: [user]
# 权限层级 + 从文件夹继承 + 支持公开和访客
define editor: [user, team#member] or owner or editor from parent
define commenter: [user, team#member] or editor
define viewer: [user, user:*, team#member] or commenter or viewer from parent
# user:* 表示全组织公开
# ============================================
# 评论——附属于文档
# ============================================
type comment
relations
# 所属文档
define document: [document]
# 评论的作者(只有作者可以编辑/删除自己的评论)
define author: [user]
# 谁可以查看评论:文档的查看者都可以看
define viewer: viewer from document
8.4 第三步:验证设计——走查业务场景
设计完模型后,必须用具体场景验证。让我们模拟一个真实企业的权限数据:
# === 组织结构 ===
# 字节跳动有两个管理员
organization:字节跳动#admin@user:CEO张一鸣
organization:字节跳动#member@user:工程师小王
organization:字节跳动#member@user:产品经理小李
organization:字节跳动#member@user:设计师小赵
# === 团队结构 ===
team:工程部#org@organization:字节跳动
team:工程部#member@user:工程师小王
team:前端组#member@user:前端小陈
team:工程部#member@team:前端组#member # 前端组嵌套在工程部
# === 项目 ===
project:抖音重构#org@organization:字节跳动
project:抖音重构#admin@user:工程师小王
project:抖音重构#editor@team:工程部#member
project:抖音重构#viewer@organization:字节跳动#member # 全组织可查看
# === 文件夹 ===
folder:技术方案#project@project:抖音重构
folder:前端方案#parent@folder:技术方案 # 子文件夹
# === 文档 ===
document:架构设计v2#parent@folder:技术方案
document:架构设计v2#owner@user:工程师小王
document:React迁移方案#parent@folder:前端方案
document:React迁移方案#owner@user:前端小陈
# === 外部分享 ===
document:架构设计v2#viewer@user:外部顾问老刘 # 访客查看
现在验证各种场景:
场景1: 工程师小王能编辑"架构设计v2"吗?
Check(document:架构设计v2, editor, user:工程师小王)
→ 小王是 owner → owner 自动是 editor → ✅
场景2: 前端小陈能查看"架构设计v2"吗?
Check(document:架构设计v2, viewer, user:前端小陈)
→ 小陈是前端组 member → 前端组 member 是 工程部 member
→ 工程部 member 是 project:抖音重构 的 editor
→ editor from project 传递到 folder:技术方案 的 editor
→ editor from parent 传递到 document:架构设计v2 的 editor
→ editor 自动是 commenter → commenter 自动是 viewer → ✅
场景3: 产品经理小李能编辑"架构设计v2"吗?
Check(document:架构设计v2, editor, user:产品经理小李)
→ 小李不是 owner,不是任何团队 member
→ 不在任何 editor 路径上 → ❌
场景4: 产品经理小李能查看"架构设计v2"吗?
Check(document:架构设计v2, viewer, user:产品经理小李)
→ 小李是 organization:字节跳动 的 member
→ organization#member 是 project:抖音重构 的 viewer
→ viewer from project 传递到 folder → 传递到 document → ✅
场景5: 外部顾问老刘能编辑"架构设计v2"吗?
Check(document:架构设计v2, editor, user:外部顾问老刘)
→ 老刘只被分享了 viewer 权限 → ❌
场景6: 工程师小王能查看哪些文档?
ListObjects(user:工程师小王, document, viewer)
→ [document:架构设计v2, document:React迁移方案]
8.5 第四步:处理特殊场景
离职处理
当员工离职时,只需要删除组织和团队中的成员关系:
Write(deletes: [
{ object: "organization:字节跳动", relation: "member", user: "user:工程师小王" },
{ object: "team:工程部", relation: "member", user: "user:工程师小王" },
{ object: "project:抖音重构", relation: "admin", user: "user:工程师小王" }
])
由于所有权限都是从关系推导的,删除关系后,所有相关权限立即失效。不需要逐个文档去清理。
临时权限(用条件实现)
condition before_deadline(deadline: timestamp, now: timestamp) {
now < deadline
}
# 给外部审计员临时查看权限,截止到2024年12月31日
document:财务报告#viewer@user:审计员老张 (condition: before_deadline)
Check(document:财务报告, viewer, user:审计员老张,
context: {deadline: "2024-12-31T23:59:59Z", now: "2024-06-15T10:00:00Z"})
→ ✅ (还没过期)
Check(document:财务报告, viewer, user:审计员老张,
context: {deadline: "2024-12-31T23:59:59Z", now: "2025-01-15T10:00:00Z"})
→ ❌ (已过期)
全组织公开文档
# 使用通配符让文档对所有用户可见
document:新员工手册#viewer@user:*
8.6 设计原则总结
从这个实战案例中,我们提炼出权限设计的核心原则:
| 原则 | 说明 | 在案例中的体现 |
|---|---|---|
| 最小权限 | 默认无权限,必须显式授予 | 外部顾问只能查看,不能编辑 |
| 关系驱动 | 权限从业务关系自然推导 | 团队成员自动获得项目权限 |
| 层级继承 | 高层权限自动向下传递 | 文件夹权限传递到文档 |
| 角色层级 | 高角色自动包含低角色权限 | admin > editor > commenter > viewer |
| 关系删除即失权 | 删除关系等于收回所有推导权限 | 离职只需删除组织成员关系 |
| 可审计 | 能清楚回答"谁有什么权限" | ListUsers 可以列出所有有权限的人 |
9. 第九章:常见陷阱与最佳实践
9.1 模型设计陷阱
陷阱1:类型爆炸
错误做法:为每种细微差异创建不同类型
# ❌ 不要这样做
type internal_document
type external_document
type draft_document
type archived_document
正确做法:用关系和条件区分
# ✅ 用一个类型 + 不同关系
type document
relations
define internal_viewer: [organization#member]
define external_viewer: [user]
define viewer: internal_viewer or external_viewer
陷阱2:过深的继承链
# ⚠️ 继承链不要超过 5-6 层
org → department → team → sub_team → project → folder → sub_folder → document
# 每增加一层,Check 的图搜索就多一级跳转,性能线性下降
建议:控制继承链深度在 4-5 层以内。如果业务确实需要更深层级,考虑在中间层做直接授权以"短路"搜索。
陷阱3:循环依赖
# ❌ 会导致无限循环
type A
relations
define r1: r2 from b_ref
type B
relations
define r2: r1 from a_ref
OpenFGA 通过搜索深度限制(默认 25 层)来防止无限循环,但循环依赖会导致性能问题和难以理解的权限逻辑。
陷阱4:没有规划"超级管理员"逃生舱
✅ 必须设计:
type system
relations
define super_admin: [user]
type any_resource
relations
define admin: [user] or super_admin from system
确保至少有一个"超级管理员"角色可以绕过所有权限,以便在权限配置出错时能够恢复。
陷阱5:将业务逻辑混入权限模型
❌ 错误:在权限模型中定义 "can_publish: editor and word_count_gt_500"
✅ 正确:权限模型只管"谁能做什么",业务规则(字数检查)由业务层处理
原则:权限系统回答"是否允许",业务系统回答"是否合理"。二者分离。
9.2 数据管理最佳实践
实践1:在应用层维护关系同步
权限数据必须与业务数据保持同步。当业务操作发生时,同步更新权限关系:
// 创建文档时
createDocument(doc) {
db.insert(doc) // 写入业务数据
openfga.write({object: doc.id, relation: "owner", user: currentUser}) // 写入权限
openfga.write({object: doc.id, relation: "parent", user: doc.folderId}) // 写入层级
}
// 删除团队成员时
removeTeamMember(team, user) {
db.delete(teamMembership) // 删除业务数据
openfga.write(deletes: [{object: team, relation: "member", user: user}]) // 删除权限
}
实践2:批量操作提高效率
# 新人入职,一次性建立所有关系
Write(writes: [
{ object: "organization:公司", relation: "member", user: "user:新人" },
{ object: "team:工程部", relation: "member", user: "user:新人" },
{ object: "project:主项目", relation: "viewer", user: "user:新人" }
])
实践3:先用 ListUsers 审计,再修改权限
在修改权限模型之前,先用 ListUsers 审计现有权限:
# 修改前:看看谁现在有权限
ListUsers(object: "document:敏感文档", relation: "viewer")
→ [user:A, user:B, user:C, user:D]
# 修改权限模型后再次审计
ListUsers(object: "document:敏感文档", relation: "viewer")
→ [user:A, user:B] # 确认权限收缩符合预期
9.3 性能优化最佳实践:多层缓存策略
在大型系统中,每个页面加载可能触发几十次 Check 调用。不做缓存,数据库会被压垮。
OpenFGA 采用三层缓存架构:
请求进入
│
▼
┌─────────────────────┐
│ 第一层: 查询结果缓存 │ 缓存 Check 的最终结果 (allowed/denied)
│ TTL: 10秒 │ 命中后直接返回,不查数据库
└─────────────────────┘
│ 未命中
▼
┌─────────────────────┐
│ 第二层: 数据迭代缓存 │ 缓存从数据库读出的关系数据
│ TTL: 10秒 │ 同一批次的多次 Check 可以复用
└─────────────────────┘
│ 未命中
▼
┌─────────────────────┐
│ 第三层: 数据库查询 │ 最终兜底,查询持久化存储
└─────────────────────┘
一致性权衡:缓存带来了延迟——权限变更后最多需要 TTL 时间才能生效。OpenFGA 允许每个请求指定一致性级别:
| 一致性级别 | 行为 | 适用场景 |
|---|---|---|
| 最终一致(默认) | 使用缓存,快但可能有几秒延迟 | 普通页面访问 |
| 高一致性 | 跳过缓存,直接读数据库 | 权限变更后的立即验证 |
9.4 安全:并发控制与深度限制
权限图可能非常深(组织嵌套 10 层),也可能非常宽(一个团队有 10000 人)。不加控制,一个恶意查询可能耗尽服务器资源。
安全护栏:
├── 搜索深度限制: 最多展开 25 层(防止无限递归)
├── 搜索宽度限制: 每层最多并发展开 10 个分支(防止内存爆炸)
├── 请求超时: 单个 Check 最多 3 秒
├── 批量限制: BatchCheck 每次最多 50 个查询
└── 写入限制: 每次 Write 最多 100 条三元组
9.5 高可用:存储抽象
权限系统是企业的关键基础设施——如果权限系统宕机,所有业务系统都无法正常工作。
OpenFGA 通过存储接口抽象,支持多种数据库后端:
┌──────────────────┐
│ 权限引擎核心 │
│ (图遍历 + 缓存) │
└────────┬─────────┘
│
┌────────┴─────────┐
│ 存储接口抽象 │
└────────┬─────────┘
│
┌────────┬───────┼───────┬────────┐
│ │ │ │ │
Postgres MySQL SQLite 内存 (可扩展)
(生产) (生产) (开发) (测试)
关键设计决策:
-
支持读写分离(主库写入,从库读取)
-
连接池管理(最大/最小连接数、空闲超时)
-
变更日志(Changelog)用于缓存失效和审计追踪
9.6 可观测性:看见权限的运行
在生产环境中,你需要能回答:
-
"为什么张三无法访问这个文档?"(调试)
-
"Check API 的 P99 延迟是多少?"(性能监控)
-
"今天有多少次权限被拒绝?"(安全审计)
可观测性三支柱:
├── 指标 (Metrics): 请求延迟、缓存命中率、数据库查询数、允许/拒绝比
├── 链路追踪 (Tracing): 每个 Check 的图遍历路径可视化
└── 日志 (Logging): 结构化 JSON 日志,包含请求 ID 用于问题定位
9.7 模型版本管理
授权模型会随着业务发展不断迭代。OpenFGA 支持模型版本管理:
Store: 我的应用
├── 模型 v1 (2024-01-01): 基础权限
├── 模型 v2 (2024-03-01): 新增条件授权
├── 模型 v3 (2024-06-01): 新增审批流 ← 当前生效
-
默认使用最新版本
-
可以指定特定版本(用于灰度发布)
-
旧数据自动兼容新模型
这就像数据库的 Migration——你可以安全地演进权限模型,不需要停机。
10. 附录:术语表
| 术语 | 英文 | 定义 |
|---|---|---|
| 三元组 | Tuple | (object, relation, user) —— 权限系统的原子数据单元 |
| 类型 | Type | 系统中实体的分类,如 user、document、folder |
| 关系 | Relation | 类型上定义的命名关系,如 viewer、editor、owner |
| 授权模型 | Authorization Model | 定义类型和关系规则的 Schema |
| 用户集 | Userset | 某对象的某关系的所有用户集合,如 team:工程部#member |
| 通配符 | Wildcard | type:* 表示某类型的所有实体 |
| 计算关系 | Computed Relation | 从其他关系推导的关系,如 viewer: editor |
| 传递关系 | TTU (Tuple-to-Userset) | 跨对象继承的关系,如 viewer from parent |
| Store | Store | 多租户隔离的基本单位,包含独立的模型和数据 |
| Check | Check | 核心 API:判断某用户是否对某对象有某种关系 |
| ReBAC | Relationship-Based Access Control | 基于关系的访问控制模型 |
| RBAC | Role-Based Access Control | 基于角色的访问控制模型 |
| ABAC | Attribute-Based Access Control | 基于属性的访问控制模型 |
| ACL | Access Control List | 访问控制列表 |
| 条件 | Condition | 附加在关系上的运行时判断条件(ABAC 扩展) |
11. 结语
权限系统的设计,本质上是在回答一个永恒的问题:如何在保证安全的前提下,让正确的人在正确的时间做正确的事。
从 ACL 到 RBAC 到 ABAC 再到 ReBAC,每一代模型都在这个问题上走得更远。而 ReBAC(关系型访问控制)之所以成为当下最先进的方案,是因为它回归了第一性原理——权限源于关系,关系就是现实世界的映射。
当你下一次设计权限系统时,记住:
- 先识别你的实体类型
- 再定义实体间的关系
- 让权限从关系中自然推导
- 用集合运算处理复杂逻辑
- 用条件扩展处理动态属性
- 始终保持可审计、可理解
祝你设计出既安全又优雅的权限体系。
本教程基于 OpenFGA 开源项目深度分析编写。OpenFGA 是 Google Zanzibar 论文的开源实现,被众多企业用于生产环境的权限管理。