单用户应用有个令人安心的性质:授权检查失败时,泄露的只是一个人的数据。SaaS 没有这个性质。多租户系统里忘掉一个 WHERE 条件,你就得向三百个客户解释为什么第 301 个客户能看到他们的发票。多租户是 SaaS 经济模型成立的原因——第一千家客户的成本只是第十家的零头——也正是它让 SaaS 安全变得不同:每个 bug 的爆炸半径都要乘上你拥有的全部租户数。
这就是为什么这个系列从这里开始,先于 SSO,先于合规,先于渗透测试。租户隔离是其他一切 SaaS 安全控制默认依赖的地基。这个系列没有固定目录——一次写一篇,每篇挑当下最紧迫的那扇门。第一篇,是其他所有门都要倚靠的那扇。
心态:信任本身就是产品
SaaS 卖的不是功能,而是“客户的数据放在你这里比放在他们自己那里更安全”这个信念。本系列的每一项控制都能追溯到这个信念。而这个信念要求的第一件事,是在每个请求上都能干脆地回答一个问题:**这是谁的数据?**答案就是租户上下文(tenant context)——而纪律在于:它只能来自已验证的身份声明,永远不能来自客户端发给你的任何东西。
三种隔离模型(以及各自在哪里崩坏)
租户数据的分离有三种方式,在成本与隔离强度之间做取舍:
- 共享 schema + RLS——所有租户的行放在同样的表里,用
tenant_id列标记,由 Postgres 的行级安全(row-level security)强制边界。运行成本最低(一个连接池、一条迁移路径、一份备份),也是大多数 SaaS 的正确默认选择。风险恰好集中在你猜得到的地方:隔离的生死全系于 policy 本身。 - 每租户一个 schema——一个数据库,每个客户一个 Postgres schema。逻辑隔离更强、单租户备份干净,但迁移要跑 N 次,租户过了几百个之后连接管理会变得别扭。适合中端市场的合同要求;据报道运行成本比完全的数据库级隔离低 30–50%。
- 每租户一个数据库——物理隔离。最容易推理,最容易卖给受监管的买家(HIPAA、PCI、政府),也最贵:备份、监控、补丁、迁移全部按客户数线性增长。留给愿意为合同付钱买单的客户。
2026 年大多数成功 SaaS 的落点是混合模式:SMB 租户池化共享,企业租户切到独立基础设施——通常由第一份大到要求签 DPA、交基础设施图的合同触发。这条切分路径要在第一周就规划好,而不是等合同来的那个季度;每次企业租户迁移预算大约 3–6 个工程师周。
阻止泄密的四条规则
每一次跨租户事故复盘,最后都会落到同样几个缺失的控制上。在第一个付费客户之前把这四条全部落地:
- 每张表都有
tenant_id,没有例外——包括审计日志、后台任务 payload、分析表和“临时”表。用迁移钩子或 linter 强制,而不是靠记忆力。 - Postgres 行级安全——
ENABLE ROW LEVEL SECURITY加上每表一条 policy,每个事务开头SET LOCAL app.tenant_id。让数据库成为执行点:某人晚上十一点随手写的查询不可能泄密,因为过滤行的是 policy,不是开发者。默认拒绝。这就是为什么 RLS 悄悄成了 2026 年多租户新项目的默认方案:审计方越来越期待看到它,查询规划器已经成熟,连接池的故事也解决了。 - 租户声明只来自已验证的 token——从你刚刚验签过的 JWT 里提取
tenant_id,并在每个请求上重新检查。请求体、URL 和子域名都是攻击者可控的输入,不是身份。 - CI 里的跨租户测试——一套以租户 A 身份认证、尝试通过每一个端点读取租户 B 数据的测试,在每个 PR 上运行。正是这条控制,把规则 1–3 从意图变成了证据。
让这一切失效的陷阱
团队意外关掉自己隔离机制的四种典型方式:
- 在用户路径里用 service-role key——在 Supabase 风格的技术栈里,service key 完全绕过 RLS。只要有一个服务端路由用 service key 初始化客户端,你的 policy 就纯属装饰。规则:用户请求永远走租户作用域、遵守 RLS 的连接;service key 只出现在隔离的管理任务里。
- 从请求里取
tenant_id——接受 body 字段、查询参数或子域名里的租户标识并用它来限定查询范围。攻击者会枚举子域名,探测的正是这一点。 - 把子域名当隔离——
acme.yourapp.com是路由便利,不是安全边界。认证和授权仍然要基于声明(claim)来做,否则就等于没做。 - 所有租户共用一把 KMS key——一把 key 泄露,所有租户的数据都被解密。按租户分 key(或按租户做数据密钥封装)能把一次密钥泄露降级成单租户事故。
系列的下一站
下一扇门很可能是身份(identity)——某天你的第一个企业级意向客户回复“你们支持 SSO 吗?”,然后你发现 SAML、SCIM 开通(provisioning)和“Sign in with Google”是三个完全不同的东西,而企业买家真正想要的是其中两个。不过顺序允许变动——系列会按当下最紧迫的主题决定下一扇门。
动手练习
- 从你当前项目里挑一张表,手写它的 RLS policy:
ENABLE ROW LEVEL SECURITY、CREATE POLICY ... USING (tenant_id = current_setting('app.tenant_id')),然后在会话里SET LOCAL,观察错误租户的查询返回空。 - 写一个跨租户测试:为租户 A 和 B 造 fixtures,以 A 的身份认证,在三个端点上断言访问 B 的资源返回 404 或空。把它放进 CI。
- 在代码库里 grep
tenant_id(或org_id、account_id)进入查询的所有位置,逐一回溯到已验证的声明。任何回溯到请求参数的路径,都是一个赶在攻击者之前修掉算你赚的 bug。