很多 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、角色、成员状态和资源隔离,改造成本会非常高。

更多推荐