单租户的权限问题已经够复杂了(见 RBAC/ABAC/ReBAC 选型)。多租户额外引入了一个维度:同一个 SaaS 平台上有 100 个公司,每个公司内部有自己的角色体系、组织结构和审批规则。这意味着你需要在同一个数据库里同时运行 100 套互相隔离的权限系统。
一、租户隔离:物理 vs 逻辑
多租户的第一个决策是隔离级别:
| 隔离方式 | 数据层 | 认证层 | 授权复杂度 | 成本 |
|---|---|---|---|---|
| 单 DB + tenant_id 字段 | 逻辑隔离(WHERE tenant_id = ?) | 共享 IdP | 高(所有租户共用同一套权限表) | 低 |
| DB per Tenant | 物理隔离(独立数据库) | 可共享或独立 IdP | 低(每个租户有独立的权限表) | 高 |
| Schema per Tenant | PostgreSQL Schema 级别隔离 | 共享 IdP | 中 | 中 |
绝大多数 B2B SaaS 选择第一种方案(单 DB +
tenant_id),因为它在工程和运维复杂度之间取得了平衡。但要付出额外的代价:每个涉及权限的
SQL 查询都要带
WHERE tenant_id = ?,忘记加这个条件就会造成跨租户数据泄露。
工程陷阱 1:在”单 DB + tenant_id”模式下,大多数跨租户数据泄露漏洞不是来自”恶意攻击者精心构造请求”,而是来自”新加入的后端工程师在 JOIN 查询中忘了加
WHERE tenant_id = ?“。防御手段是:应用层的 ORM 自动注入 tenant_id 过滤器(如 Hibernate 的@Filter、Django 的 Row-level security),或者数据库层的 RLS(Row-Level Security,PostgreSQL 14+ 支持)。
二、多租户权限的四层架构
flowchart TD
L1["第一层:平台级权限<br/>(Platform Admin, Billing Admin)"]
L2["第二层:租户级功能权限<br/>(租户自定义角色: Admin, Manager, Viewer, ...)"]
L3["第三层:组织/部门级数据权限<br/>(只能看本部门和下属部门的数据)"]
L4["第四层:资源级协作权限<br/>(谁能看这个文档/这个项目)"]
L1 --> L2 --> L3 --> L4
2.1 第一层:平台级权限
平台运营团队需要的权限——管理所有租户、查看所有 billing 信息、介入客户支持。这层不与租户内部权限重叠,通常用全局 RBAC 角色(Platform Admin、Ops、Support)实现。
2.2 第二层:租户级功能权限
这是 RBAC 在租户维度的扩展——每个租户可能有自己的角色定义。
数据结构方案:
-- 全局角色表(平台预定义的角色模板)
CREATE TABLE roles (
id UUID PRIMARY KEY,
tenant_id UUID, -- NULL = 平台角色(跨租户共享), NOT NULL = 租户自定义
name VARCHAR(128),
permissions JSONB, -- ["user.read", "user.write", "report.export"]
UNIQUE (tenant_id, name)
);
-- 用户-角色分配
CREATE TABLE user_roles (
user_id UUID,
role_id UUID REFERENCES roles(id),
tenant_id UUID, -- 冗余字段用于快速筛选
PRIMARY KEY (user_id, role_id)
);租户自定义角色通过 tenant_id 隔离——租户 A 的
“Custom-Admin” 角色和租户 B 的 “Custom-Admin”
角色是两条不同的记录,带不同的权限。
2.3 第三层:组织树与数据权限
这是多租户权限中最复杂的部分。一个典型的 B2B 场景:
公司 A:
├── 总部 (Department)
│ ├── 工程部
│ │ ├── 平台组 (Team)
│ │ └── 产品组
│ └── 市场部
└── 分部 (Department)
└── 客户支持
需求:平台组的 Manager 应能看到本组和下属组(如果有)的数据,
同时能看到工程部的汇总数据,但看不到市场部的数据。
这种需要的是”组织树 + 作用域”模型:
CREATE TABLE org_nodes (
id UUID PRIMARY KEY,
tenant_id UUID NOT NULL,
parent_id UUID REFERENCES org_nodes(id), -- 树结构
name VARCHAR(255),
type VARCHAR(64) -- 'department', 'team', 'division'
);
-- 用户角色在特定作用域中生效
CREATE TABLE user_role_scopes (
user_id UUID,
role_id UUID,
org_node_id UUID, -- 该角色在哪个组织范围内生效
tenant_id UUID,
PRIMARY KEY (user_id, role_id, org_node_id)
);授权检查时,需要从用户当前组织节点出发,向上遍历祖先,收集所有
user_role_scopes
中生效的权限。如果用户是”工程部”的
Manager,在查询”平台组”的数据时,授权检查应沿组织树上溯,判断”工程部”的
Manager 角色是否覆盖”平台组”。
工程陷阱 2:组织树的深度通常不大(5-8 层),但每个授权检查都要做一次图遍历(从叶子节点上溯到根)。如果权限查询频繁(如每个 API 请求都需要),需要做组织树缓存——预计算每个用户的”有效组织范围列表”,缓存到 Redis,在组织变更时失效。
2.4 第四层:资源级权限(Zanzibar 风格)
文档、项目、文件夹的共享和协作权限适合用 Zanzibar 模型(见上一篇文章)。在多租户场景中,Zanzibar 的 namespace 需要是 tenant-aware 的——不同租户的 relation tuple 完全隔离:
# Zanzibar 式 relation tuple,带 tenant 前缀
⟨tenant:a/doc:readme.txt #viewer user:zhangsan⟩
⟨tenant:a/group:eng-leads #member user:lisi⟩
# 而非
⟨doc:readme.txt #viewer tenant:a/user:zhangsan⟩前者保证了不同租户之间的关系元组在物理索引上隔离,避免跨租户的图遍历。
三、跨租户授权
有些场景中,一个租户的用户需要访问另一个租户的数据。典型场景:
- 第三方服务商:某会计事务所是 SaaS 平台的客户之一,“审计师”角色需要查看其客户的财务数据(这些客户也是平台的租户)。
- 企业集团:母公司租户的管理员需要查看子公司租户的数据。
跨租户授权的架构选择:
- 委托授权(Delegated Access):租户 A 在界面上选择”允许租户 B 的用户 X 访问我的 Y 数据”。生成一条跨租户的 relation tuple。
- 父租户模型:在租户之间建立父子关系,父租户的管理员自动获得子租户的指定权限。
- 代理账号(Service Account per Tenant):为第三方创建目标租户下的服务账号,限定权限范围。
四、小结
多租户权限不是”单租户权限 × N”。它是一个新的维度,会和你已有的 RBAC、组织树、Zanzibar 风格的资源级权限互相交叉:
- 首先选好租户隔离模型(单 DB + tenant_id 是多数 SaaS 的起点)。
- 四层权限架构(平台级→功能级→数据级→资源级)作为设计蓝图,每层有独立的实现方式。
- 组织树缓存是性能关键——预计算用户的”有效组织范围”。
- ID 被遗忘的 WHERE tenant_id 是跨租户数据泄露的首要原因——从第 1 天就在 App 层建立防呆机制。
上一篇:OPA、Cedar 与策略引擎落地 下一篇:API Gateway、BFF 与边界认证授权
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【身份与访问控制工程】IAM 全景:为什么这是高价值赛道
从 2020 年 SolarWinds 到 2024 年 Okta 支持系统泄露,身份基础设施的安全失败反复证明一件事:IAM 不是 IT 支撑系统,而是安全架构的承重墙。本文建立现代 IAM 的全景地图——从认证协议、令牌体系、权限模型到身份治理与平台选型,给出 5 个贯穿全系列的核心问题。
【身份与访问控制工程】RBAC、ABAC、ReBAC:权限模型怎么选
RBAC 简单但会角色爆炸,ABAC 灵活但策略管理失控时更可怕,ReBAC 表达力强但引入了图遍历的性能约束。三种模型不是'选一个升级另一个'的线性关系,而是在表达能力、管理成本和性能三者之间做工程权衡。本文从每种模型的本质数据结构出发,拆解选型框架。
【身份与访问控制工程】Zanzibar 风格权限系统:Google 的全球授权引擎
Google Zanzibar 论文在 2019 年发布后,引发了开源授权系统的一波重新设计:Auth0 FGA、SpiceDB、Permify、Ory Keto——全都基于 Zanzibar 的'关系图+命名空间配置'模型。但论文本身只讲了 What,没深入 Why。本文从 Zanzibar 的 relation tuple 模型、namespace config 的语义、consistency 模型(Zookie)和工程权衡出发,拆解为什么 Zanzibar 的设计决策是这样的,以及你自己实现时要面对什么。
【身份与访问控制工程】OPA、Cedar 与策略引擎落地
OPA 是 CNCF 的策略引擎标准答案,Rego 是它的策略语言;Cedar 是 AWS 开源的新竞争者,基于 Rust 的 WASM 编译执行、语法更接近 SQL。两者在架构模式(sidecar vs 中心化)、策略语言设计哲学和性能特征上有根本差异。本文从策略引擎的架构模式出发,拆解 OPA Rego 的核心语义与性能限制、Cedar 的设计取舍,以及策略即代码(Policy as Code)在 CI/CD 中的落地。