多租户 SaaS 隔离与权限架构¶
要点卡¶
30 秒回答
多租户不是给表加 tenant_id,而是让可信租户身份贯穿 API、任务、数据库、缓存、MQ、
对象存储、搜索、向量检索、日志和备份。隔离强度按合规、噪声、故障域和成本选择共享表、
schema/库或独立部署;应用 fail-closed scope、复合键和 PostgreSQL RLS 形成纵深防御,
同时注意 owner/BYPASSRLS、连接池上下文和数据库之外的资源仍需各自授权。
3 分钟展开
- tenant 来自已验证 identity 与服务端 membership,不接受 body/query 覆盖;跨租户运营走独立 break-glass 审批和审计。
- 共享表的主键/唯一键/外键和常用索引通常都带
tenant_id,防止业务约束和查询遗漏形成跨租户引用。 - PostgreSQL RLS 默认不约束 superuser、
BYPASSRLS,table owner 通常也绕过;必要时FORCE ROW LEVEL SECURITY, 应用角色不得拥有绕过能力,pooled connection 在事务内SET LOCAL并测试上下文清理。 - 每租户限 QPS、并发、存储、token/tool 成本;大租户可渐进迁移到 silo,控制面维护路由、配置和迁移状态。
| 记忆槽 | 内容 |
|---|---|
| 三个不变量 | tenant 只能来自可信身份;缺 tenant 必须 fail closed;所有共享资源都要纳入隔离 |
| 手画图 | identity → tenant context → app scope + RLS → cache/MQ/search/storage |
| 项目落点 | OctoAgentFlow 讲 persona/memory/tool/credential namespace 和配额;只按真实实现程度表述 |
| 一个取舍 | pool 模型成本低但爆破半径和 noisy neighbor 风险高;silo 隔离强但迁移与运维成本高 |
错误表达
- ❌ “有 RLS 就不会串租户;独立数据库一定完全隔离。”
- ✅ “RLS 有绕过角色和连接上下文边界;独立库仍要检查凭证、备份、网络和运维控制面是否共享。”
自测追问:使用 pgx 连接池时,tenant session variable 为什么可能泄漏到下一个请求,如何避免?
10 分钟版(原理 + 图示)¶
flowchart TB
Req[HTTP 请求] --> GW[网关解析 tenant]
GW --> Svc[Go 服务]
Svc --> Ctx[context 注入 TenantID]
Ctx --> ORM[GORM Scopes]
ORM --> DB[(DB)]
subgraph models[隔离模型]
M1[共享表 + tenant_id]
M2[schema per tenant]
M3[DB per tenant]
end
隔离模型对比
| 模型 | 成本 | 隔离 | 适用 |
|---|---|---|---|
| 共享表 + tenant_id | 低 | 逻辑隔离、共享故障域 | 大量相似租户且合规允许 |
| Schema / 库 per tenant | 中 | 更强数据边界 | 需独立迁移/备份或 noisy-neighbor 治理 |
| 部署 per tenant | 高 | 可分离计算、网络与运维边界 | 合规/性能/故障域要求值得承担成本 |
权限层次
- 租户级:tenant A 看不见 tenant B 数据
- 租户内 RBAC:admin / member / read-only
- 资源级:项目、订单、知识库文档 ACL
Go 实现要点
type tenantKey struct{}
func WithTenant(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, tenantKey{}, id)
}
func TenantScope(db *gorm.DB, ctx context.Context) (*gorm.DB, error) {
tid, ok := ctx.Value(tenantKey{}).(string)
if !ok || tid == "" {
return nil, ErrMissingTenant // 缺 tenant 必须 fail closed
}
return db.Where("tenant_id = ?", tid), nil
}
中间件:从已完成 iss/aud/签名校验的身份中解析 tenant,并再次验证用户对 tenant 的 membership;禁止客户端 body 覆盖。仅靠开发者记得调用 scope 容易漏,关键表还应使用 RLS、带 tenant_id 的唯一键/外键与跨租户测试。
若 PostgreSQL policy 读取 current_setting('app.tenant_id'),应在显式事务中用 SET LOCAL
绑定,并确保应用角色不是 table owner/superuser/BYPASSRLS;FORCE ROW LEVEL SECURITY
只处理 table owner 的常见绕过,不能约束 superuser/BYPASSRLS。连接池上的 session-level
SET 若未可靠 reset,可能把前一个请求的 tenant 带给后一个请求。
生产场景¶
- 大客户要独立库:绞杀迁移 tenant 数据;连接池按 tenant 路由(见连接池题)
- AI 知识库 SaaS:向量检索必须 filter
tenant_id(S-AI-02 RAG) - 配额 noisy neighbor:单 tenant QPS/存储/cpu 限流
- 异步与外围资源:job payload、cache key、topic/consumer authorization、object prefix、search alias/filter、日志查询和导出都必须携带并验证 tenant,不能只保护主数据库
排查与工具¶
- 渗透测试:横向越权用例自动化
- 审计日志:
who + tenant + action + resource - 集成测试:双 tenant 并发写读
架构取舍¶
| 早期全共享 | 过早独立库 |
|---|---|
| 快 | 运维爆炸 |
演进路径:可从共享表演进到部分租户 silo 的混合模式;比例由合规、噪声、成本与迁移能力决定。
深挖问答¶
- 子域名 tenant 解析? →
acme.app.com→ tenant=acme;通配证书 + 网关路由。 - 跨 tenant 运营后台? → 超级管理员 break-glass + 全审计。
- 缓存隔离? → key 必须含 tenant 防碰撞,还要限制租户配额、保护管理命令和凭证;key 前缀本身不是访问控制。
- MQ 隔离? → 可按监管/规模选择独立 topic/cluster,或共享 topic 携带 tenant;消费者仍必须授权和校验,客户端 header filter 不是安全边界。
反模式与事故¶
- 忘记 scope → IDOR 看他人订单,架构师责任
- tenant_id 来自 query 参数 → 伪造
- 宣称“独立库”却共享高权限凭证、备份和运维边界 → 实际隔离强度低于承诺
代码示例¶
Gin 中间件注入 tenant 后,c.Request = c.Request.WithContext(WithTenant(...))。