租户Tenant、角色Role、用户User:SaaS权限设计的核心三元组
导言:一个(希望)有用的比喻
想象一下你正在构建一个大型的、提供全方位服务的 办公大楼(SaaS 应用程序)。
-
租户 (Tenant):
- 是什么? 租户就是租用你大楼里一层或几层空间的公司。比如,“Acme 公司”租了10楼,“Globex 集团”租了11楼。
- 核心功能? 隔离。Acme公司的员工不能(也不应该)刷卡进入 Globex 集团的办公室。租户确保了数据的边界和隐私。
- 对应实体?
Company,Organization,Team。
-
用户 (User):
- 是什么? 用户是真正在大楼里工作的 具体的人。比如,Acme 公司的“张三”和 Globex 集团的“李四”。
- 核心功能? 认证 (Authentication)。他是谁?他得用他的工卡(用户名/密码/SSO)来证明“我是张三”。
- 对应实体?
User,Account,Profile。
-
角色 (Role):
- 是什么? 角色是用户的 职位或职责。比如,“Acme-前台”、“Acme-经理”或“Acme-IT管理员”。
- 核心功能? 授权 (Authorization)。这个角色决定了用户能做什么。
- 对应实体?
Role,Group。
这个比喻的核心关系:
用户 “张三” (User) 是 “Acme公司” (Tenant) 的一名员工,他的职位是 “经理” (Role)。因为他是“经理”,所以他被授予了一串 钥匙 (Permissions),这串钥匙可以打开他自己办公室的门、会议室的门和档案室的门。
重要的是:张三的“经理”钥匙,绝对打不开11楼Globex集团的任何一扇门。这就是租户隔离。
关系模型详解
三者之间的关系是权限管理系统的骨架。
1. 租户 (Tenant) - “数据和用户的容器”
租户是最高层级的隔离单位。在多租户设计中,系统中的几乎所有数据都必须有一个 TenantID 标签。
- 与用户的关系 (Tenant 1:N User):
- 一个租户(公司)通常拥有 多个 用户(员工)。
- 一个用户(员工)通常只属于 一个 租户(公司)。
- (注意:在某些复杂的B2B协作模型中,一个用户可能属于多个租户,但这会使设计复杂度急剧上升。对于大多数 SaaS,建议先从“用户单归属”开始。)
2. 用户 (User) - “系统操作的实体”
用户是发起所有操作的主体。
- 与角色的关系 (User M:N Role):
- 一个用户可以拥有 多个 角色。例如,张三既是“经理”(管理团队)又是“财务审批人”(审批报销)。
- 一个角色可以分配给 多个 用户。例如,“员工”这个角色会分配给公司里的所有人。
- 这是一个经典的多对多(M:N)关系,需要一个中间表(如
UserRoles)来连接。
3. 角色 (Role) - “权限的集合”
这是最容易被误解的地方。角色本身不是权限,角色是 权限的载体。
- 与权限的关系 (Role M:N Permission):
- 一个角色(如“经理”)可以包含 多个 权限(如
read_report,approve_leave,edit_team)。 - 一个权限(如
read_report)可以被分配给 多个 角色(“经理”和“分析师”可能都需要)。
- 一个角色(如“经理”)可以包含 多个 权限(如
三者的完整连接
- 用户 (User) 登录系统。
- 系统根据
UserID确认他是谁,并(在单租户模型中)加载他所属的 租户 (TenantID)。 - 系统根据
UserID和TenantID(或全局)查询他所拥有的 角色 (Roles)。 - 系统根据这些 角色 (Roles),汇总出他拥有的所有 权限 (Permissions) 列表。
- 当用户尝试执行一个操作(例如,访问
GET /api/v1/reports)时,系统会检查:- 授权检查: 用户的权限列表里是否包含
read_report这个权限? - 租户隔离检查: 他要访问的报告的
TenantID是否等于他自己所在的TenantID? - 两者都通过,才允许操作。
- 授权检查: 用户的权限列表里是否包含
核心数据模型 (E-R 示例)
为了实现上述关系,你的数据库表结构可能看起来像这样(以简化形式):
-- 租户表 (公司)
CREATE TABLE Tenants (
TenantID INT PRIMARY KEY,
TenantName VARCHAR(100)
-- ... 其他租户信息 (订阅级别、域名等)
);
-- 用户表 (员工)
CREATE TABLE Users (
UserID INT PRIMARY KEY,
TenantID INT, -- 关键外键,实现用户到租户的归属
Username VARCHAR(50),
PasswordHash VARCHAR(255),
FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID)
);
-- 角色表 (职位)
CREATE TABLE Roles (
RoleID INT PRIMARY KEY,
TenantID INT, -- !! 注意这个字段,见下文 "实践要点4"
RoleName VARCHAR(50)
-- TenantID 为 NULL 表示这是一个 "全局系统角色"
-- TenantID 不为 NULL 表示这是 "租户自定义角色"
);
-- 权限表 (功能点)
CREATE TABLE Permissions (
PermissionID INT PRIMARY KEY,
PermissionKey VARCHAR(100) UNIQUE -- 例如: "user:create", "report:read"
-- ... 权限的描述
);
-- 角色-权限 关联表 (M:N)
CREATE TABLE RolePermissions (
RoleID INT,
PermissionID INT,
PRIMARY KEY (RoleID, PermissionID),
FOREIGN KEY (RoleID) REFERENCES Roles(RoleID),
FOREIGN KEY (PermissionID) REFERENCES Permissions(PermissionID)
);
-- 用户-角色 关联表 (M:N)
CREATE TABLE UserRoles (
UserID INT,
RoleID INT,
PRIMARY KEY (UserID, RoleID),
FOREIGN KEY (UserID) REFERENCES Users(UserID),
FOREIGNKEY (RoleID) REFERENCES Roles(RoleID)
);
-- 业务数据表 (例如:发票)
CREATE TABLE Invoices (
InvoiceID INT PRIMARY KEY,
TenantID INT, -- !! 黄金法则:所有业务数据都必须有 TenantID
Amount DECIMAL(10, 2),
-- ...
FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID)
);
权限管理实践中的关键注意事项
理论很美好,实践(的坑)才更重要。
1. 黄金法则:数据的绝对隔离
永远,永远,永远不要相信你的应用程序逻辑。你必须在数据库层面强制执行租户隔离。
- 错误的做法:
SELECT * FROM Invoices WHERE InvoiceID = 123;(然后用代码检查if (invoice.TenantID == user.TenantID))。 - 正确的做法:
SELECT * FROM Invoices WHERE InvoiceID = 123 AND TenantID = [CurrentUser.TenantID];
所有的数据查询 必须 包含 WHERE TenantID = ? 子句。一个地方的疏忽就可能导致灾难性的数据泄露(Acme公司看到了Globex公司的财务报表)。
- 最佳实践: 使用 ORM 或数据库抽象层,自动在每个查询中注入当前的
TenantID。
2. “超级管理员” vs “租户管理员”
你必须区分两种“管理员”:
-
超级管理员 (Super Admin):
- 他是谁? 你(SaaS提供商)的员工。
- 租户? 他不属于任何租户(或者说
TenantID为NULL)。 - 权限? 创建/删除/禁用 租户,查看系统运营仪表盘,管理订阅。
- 绝对不能: 查看租户的 内部业务数据(除非是明确的客户支持)。
-
租户管理员 (Tenant Admin):
- 他是谁? 客户(Acme公司)的IT经理。
- 租户? 他属于
TenantID = Acme。 - 权限? 在 他自己的租户内 创建/删除/禁用 用户,分配 角色,重置密码。
- 绝对不能: 访问任何其他租户的信息。
3. 在令牌 (Token) 中包含核心信息
当用户登录后,你生成的JWT(JSON Web Token)或 Session 中应包含这“三元组”的关键信息,以避免在每次请求时都去查数据库。
一个典型的JWT Payload可能长这样:
{
"sub": "12345", // UserID
"tid": "acme-corp", // TenantID
"roles": ["manager", "finance_approver"], // 角色列表
"perms": ["user:create", "report:read", "invoice:approve"], // 权限列表 (更推荐)
"iat": 1516239022
}
注意: 在Token中直接包含
perms(权限列表)通常优于roles(角色列表)。因为后端服务只需检查“Token中是否包含report:read权限”,而无需再查一次数据库“manager角色到底有什么权限”。
4. 角色的设计:全局 vs. 租户自定义
这是一个关键的架构决策:
-
全局角色 (Global Roles):
- 设计:
Roles表中的TenantID字段为NULL。 - 含义: 角色(如 “Admin”, “Member”)由你(SaaS提供商)统一定义。所有租户看到的都是这套角色。
- 优点: 简单,易于管理,逻辑清晰。
- 缺点: 不灵活。Acme公司可能想要一个“仓库管理员”角色,而Globex公司不需要。
- 设计:
-
租户自定义角色 (Tenant-Specific Roles):
- 设计: 租户管理员可以在系统中创建他们自己的角色(
Roles表中TenantID为他们自己的ID)。 - 优点: 极其灵活,能满足大型企业的复杂需求。
- 缺点: 复杂度飙升。你的UI和后端逻辑必须支持“角色和权限的动态配置”。
- 设计: 租户管理员可以在系统中创建他们自己的角色(
-
推荐方案(混合):
- 提供 3-4 个 全局默认角色(如:管理员、编辑者、查看者)。
- 允许 租户管理员 基于这些默认角色 创建自己的自定义角色(通过复制和增删权限)。
5. RBAC, ABAC 还是其他?
-
RBAC (Role-Based Access Control):
- 你正在看的: 这整个文档讲的基本就是RBAC。
- 核心: “你能做什么” 取决于你的角色。
-
ABAC (Attribute-Based Access Control):
- 更进一步: “你能做什么” 不仅取决于你的角色,还取决于 其他属性(上下文)。
- 例子: “财务经理”(角色)只能在“工作日的9点到5点”(属性)并且“从公司IP地址”(属性)访问“金额低于1000元”(属性)的报销单。
- 建议: 除非你的业务场景极其复杂,否则 从RBAC开始。RBAC能解决95%的问题。
结语
理解租户、用户和角色是构建可扩展、安全的多租户应用的起点。
- 租户 (Tenant) 负责 数据隔离 (在哪里)。
- 用户 (User) 负责 身份认证 (你是谁)。
- 角色 (Role) 负责 操作授权 (你能做什么)。
始终牢记“黄金法则”(数据隔离),并在设计时 优先考虑“租户管理员”和“超级管理员” 的清晰分离。