企业权限体系设计的前世今生

5 阅读40分钟

转载自github.com/dataelement…

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)

核心思想:引入「角色」作为中间层,将权限赋予角色,再将角色赋予用户。

角色定义:
  管理员 → [读, 写, 删除, 管理]
  编辑者 → [读, 写]
  查看者 → [读]

角色分配:
  张三 → 管理员
  李四 → 编辑者
  实习生小王 → 查看者

优点:大幅减少管理复杂度。新人入职只需分配角色,不需要逐个设置资源。

局限性

  1. 角色爆炸:当业务复杂后,你需要为每种细微差异创建角色——"华东区-销售-高级"、"华南区-销售-初级"、"总部-销售-管理者"……角色数量可能超过用户数量。

  2. 无法表达关系:RBAC 无法自然表达"张三可以查看他自己创建的文档"、"经理可以审批下属的报销单"这类依赖于对象间关系的规则。

  1. 缺乏层级继承:项目文件夹下的文档,不能自动继承文件夹的权限。
  • 缺乏上下文感知:"只有在工作时间内才能访问"这种条件,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
  张三 是 工程团队 的 成员        → 张三可以访问工程团队的所有资源

核心优势

  1. 关系即权限:权限自然地嵌入在业务关系中——你是项目成员,所以你能访问项目文档;你是团队经理,所以你能审批团队报销。

  2. 层级继承:文件夹的权限自动传递给文件夹内的文档,组织的权限传递给下属团队。

  1. 可反向查询:可以高效回答"张三能访问哪些文档"和"谁能访问这份文档"。

  2. 关系可组合和扩展:用交集、并集、差集等集合运算组合复杂规则。不需要预定义所有角色,随时定义新的关系类型。

这就是 Google 在其内部系统 Zanzibar 中采用的方案,也是 OpenFGA 项目实现的核心模型。

2.5 演进总结

ACL ──→ RBAC ──→ ABAC ──→ ReBAC
简单     角色      属性     关系图

每一代都不是取代前一代,而是包含前一代:
- ReBAC 可以用"角色"关系模拟 RBAC,RBAC 是 ReBAC 的子集——"角色"只是一种特殊的"关系"
- ReBAC 可以用"条件"扩展支持 ABAC
- ReBAC 的直接关系就是 ACL
维度ACLRBACABACReBAC
管理复杂度
表达能力
层级继承不支持不支持需编码原生支持
反向查询困难一般极难原生支持
性能
审计友好

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:声明模型和版本,这个模型告诉系统:

  1. 世界上有 userdocument 两种实体

    • type user:定义了一种类型"user"(用户),它没有关系定义,是纯粹的"主体"类型

    • type document:定义了一种类型"document"(文档)

  2. 文档可以有 ownereditorviewer 三种关系

    1. 文档的 viewer 包括:直接指定的查看者+ 所有编辑者 + 所有所有者

    2. 类型限制的意义:[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 项目管理与文档协作平台:

组织结构

  • 每个客户公司是一个「组织」

  • 组织下有多个「团队」

  • 团队可以嵌套(如:工程部 > 前端组)

  • 用户可以属于多个团队

资源结构

  • 组织下有「项目」

  • 项目下有「文件夹」

  • 文件夹下有「文档」

  • 文档可以有「评论」

权限需求

  1. 组织管理员可以管理组织内的一切

  2. 项目有四种角色:管理员、编辑者、评论者、查看者

  1. 文件夹权限应继承到子文档

  2. 文档可以单独分享给组织外的人(访客)

  1. 某些文档可以设为全组织公开

  2. 合规要求:离职员工立即失去所有权限

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
通配符Wildcardtype:* 表示某类型的所有实体
计算关系Computed Relation从其他关系推导的关系,如 viewer: editor
传递关系TTU (Tuple-to-Userset)跨对象继承的关系,如 viewer from parent
StoreStore多租户隔离的基本单位,包含独立的模型和数据
CheckCheck核心 API:判断某用户是否对某对象有某种关系
ReBACRelationship-Based Access Control基于关系的访问控制模型
RBACRole-Based Access Control基于角色的访问控制模型
ABACAttribute-Based Access Control基于属性的访问控制模型
ACLAccess Control List访问控制列表
条件Condition附加在关系上的运行时判断条件(ABAC 扩展)

11. 结语

权限系统的设计,本质上是在回答一个永恒的问题:如何在保证安全的前提下,让正确的人在正确的时间做正确的事。

从 ACL 到 RBAC 到 ABAC 再到 ReBAC,每一代模型都在这个问题上走得更远。而 ReBAC(关系型访问控制)之所以成为当下最先进的方案,是因为它回归了第一性原理——权限源于关系,关系就是现实世界的映射。

当你下一次设计权限系统时,记住:

  1. 先识别你的实体类型
  2. 再定义实体间的关系
  1. 让权限从关系中自然推导
  2. 用集合运算处理复杂逻辑
  1. 用条件扩展处理动态属性
  2. 始终保持可审计、可理解

祝你设计出既安全又优雅的权限体系。


本教程基于 OpenFGA 开源项目深度分析编写。OpenFGA 是 Google Zanzibar 论文的开源实现,被众多企业用于生产环境的权限管理。