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