多租户 SaaS 权限怎么设计?从组织、角色到资源隔离
很多 SaaS 系统一开始只有个人用户。
数据库表里有一个 user_id,查询数据时加上:
WHERE user_id = ?
基本就能满足需求。
但当系统开始支持企业版、团队协作、成员管理后,权限模型就会变复杂。
例如:
一个企业有多个成员
不同成员权限不同
管理员可以管理组织
普通成员只能查看自己的内容
某些记录属于整个团队
某些数据只属于个人
这时如果继续只靠 user_id 判断,很快就会出现权限混乱。
多租户 SaaS 权限设计的核心,是先区分几个概念:
用户是谁
他属于哪个租户
他在租户里是什么角色
他能访问哪些资源
一、什么是多租户?
多租户可以简单理解为:一套系统服务多个企业或组织,但不同组织之间的数据必须隔离。
例如:
A 公司使用同一个 SaaS 系统
B 公司也使用同一个 SaaS 系统
它们可以共用同一套代码、同一套服务,甚至同一个数据库集群。
但 A 公司不能看到 B 公司的数据。
因此,系统中通常会引入一个核心字段:
tenant_id
也可以叫:
organization_id
team_id
workspace_id
company_id
具体命名不重要,重要的是它代表“数据属于哪个组织”。
二、基础表结构怎么设计?
一个简单的多租户模型可以包含三张核心表。
用户表:
CREATE TABLE user (
id BIGINT PRIMARY KEY,
email VARCHAR(128) NOT NULL,
name VARCHAR(64),
created_at DATETIME NOT NULL
);
租户表:
CREATE TABLE tenant (
id BIGINT PRIMARY KEY,
name VARCHAR(128) NOT NULL,
plan_type VARCHAR(32),
created_at DATETIME NOT NULL
);
成员关系表:
CREATE TABLE tenant_member (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
role VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_tenant_user (tenant_id, user_id)
);
这里不要把 tenant_id 直接写死在用户表里。
因为一个用户未来可能属于多个组织。
例如:
用户自己有个人账号
同时是 A 公司成员
又被邀请进 B 公司项目
用成员关系表会更灵活。
三、角色怎么设计?
最常见的是 RBAC,也就是基于角色的访问控制。
基础角色可以设计为:
OWNER:组织拥有者
ADMIN:管理员
MEMBER:普通成员
VIEWER:只读成员
不同角色拥有不同能力:
| 权限动作 | OWNER | ADMIN | MEMBER | VIEWER |
|---|---|---|---|---|
| 管理成员 | 是 | 是 | 否 | 否 |
| 修改套餐 | 是 | 否 | 否 | 否 |
| 创建任务 | 是 | 是 | 是 | 否 |
| 查看团队记录 | 是 | 是 | 按规则 | 按规则 |
| 删除组织 | 是 | 否 | 否 | 否 |
一开始不建议把角色设计得太细。
如果上来就有十几个角色,后期维护会很麻烦。
更推荐先用少量稳定角色覆盖主要场景,再在复杂业务中补充具体权限点。
四、权限判断不要只看角色
很多系统会写成:
if (member.getRole().equals("ADMIN")) {
allow();
}
这样虽然简单,但不够灵活。
更好的方式是判断某个用户是否拥有某个动作权限。
例如:
permissionService.check(
userId,
tenantId,
"record:delete"
);
权限点可以设计成:
member:invite
member:remove
record:view
record:delete
task:create
billing:manage
setting:update
这样业务代码不需要关心用户是管理员还是拥有者,只需要关心他能不能执行当前动作。
角色和权限点之间的对应关系,可以放在配置中。
五、数据隔离必须放在后端
前端可以隐藏按钮,但不能作为权限安全边界。
例如普通成员不应该看到“删除团队记录”按钮。
但即使按钮被隐藏,用户仍然可能通过接口请求:
DELETE /api/records/10001
所以后端必须检查:
当前用户是否属于该租户
当前资源是否属于该租户
当前用户是否有删除权限
例如查询记录时:
SELECT *
FROM translation_record
WHERE id = ?
AND tenant_id = ?;
不要只写:
WHERE id = ?
否则只要猜到资源 ID,就可能越权访问其他组织的数据。
六、资源表都要带 tenant_id 吗?
多数业务表都建议带上 tenant_id。
例如:
CREATE TABLE translation_record (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
created_by BIGINT NOT NULL,
title VARCHAR(128),
created_at DATETIME NOT NULL
);
这样做有几个好处:
查询时可以直接按租户过滤
更容易建立联合索引
方便做数据归档和迁移
权限判断更明确
常见索引可以这样建:
CREATE INDEX idx_tenant_created
ON translation_record(tenant_id, created_at);
这样查询某个组织下的记录列表时,性能会更稳定。
七、个人资源和团队资源要分清
SaaS 产品里经常同时存在两类资源:
个人资源
团队资源
例如同言翻译(Transync AI)这类实时翻译产品,用户可能既有个人翻译记录,也可能在企业组织下产生会议记录、翻译任务、会议总结和成员使用数据。
这时就要明确:
个人记录:只有创建者能看
团队记录:组织内有权限的人能看
企业使用明细:管理员或拥有者能看
可以在资源表中增加:
owner_type
owner_id
tenant_id
created_by
例如:
owner_type = PERSONAL
owner_id = user_id
owner_type = TENANT
owner_id = tenant_id
这样系统可以同时支持个人版和企业版,而不是为两套业务复制两套表。
八、邀请成员也要考虑状态
成员邀请通常不只是插入一条成员记录。
更完整的状态包括:
INVITED:已邀请,未接受
ACTIVE:已加入
REMOVED:已移除
EXPIRED:邀请过期
成员表可以增加 status 字段。
用户真正登录并接受邀请后,再变成 ACTIVE。
权限判断时,只允许 ACTIVE 成员访问组织资源。
if (!member.getStatus().equals("ACTIVE")) {
throw new ForbiddenException();
}
这样可以避免“邀请了但未加入”的账号提前拥有权限。
九、权限系统要记录操作日志
企业版功能通常需要审计。
例如:
谁邀请了成员
谁删除了记录
谁修改了组织设置
谁查看了使用明细
谁调整了套餐或席位
可以建立操作日志表:
CREATE TABLE audit_log (
id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
operator_id BIGINT NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(64),
target_id BIGINT,
created_at DATETIME NOT NULL
);
操作日志的价值不只是在出问题时追责,也能帮助管理员理解团队内部发生了什么。
十、多租户权限检查清单
设计 SaaS 权限时,可以检查:
1. 是否区分 user_id 和 tenant_id?
2. 一个用户是否可以加入多个组织?
3. 成员关系是否有状态?
4. 是否定义了清晰的角色?
5. 角色是否映射到具体权限点?
6. 后端是否校验资源所属租户?
7. 业务表是否包含 tenant_id?
8. 个人资源和团队资源是否分开建模?
9. 管理员和普通成员的数据范围是否不同?
10. 是否记录关键操作日志?
总结
多租户 SaaS 权限设计,不能只靠一个 user_id 字段解决。
更稳妥的模型通常包括:
用户表
租户表
成员关系表
角色与权限点
资源 tenant_id 隔离
个人资源与团队资源区分
操作审计日志
对于个人版产品来说,权限模型可以简单一些。
但一旦产品进入企业版、团队协作、组织管理阶段,就应该尽早建立多租户模型。
否则后期再补 tenant_id、角色、成员状态和资源隔离,改造成本会非常高。

更多推荐


所有评论(0)