数据隔离:一行 workspace_id 与四道防线

一条查询少写 where,导出 CSV 就混进别家的项目名。本文在 20 个团队的规模下逐项对比行级、schema、独立库三种隔离方案,给出四道防线、8 条 RLS 策略、27 条越权用例与 20 套迁移的完整复算口径。

导出 CSV 里的三行,来自另一个团队。

一、导出文件里多了三行

一位做项目管理的独立开发者,给 20 个团队开了工作区。某天一位客户在导出 CSV 里看到了另一个团队的项目名,虽然只有三行,但足够让他确认这不是显示错误。

出问题的是一条「最近项目」查询:新同事在仓储层写查询时漏了 where workspace_id = ?,本地测试只有一个租户的数据,所以怎么点都正常;上线后,这条查询把所有团队的项目按更新时间排了一遍。

导出接口复用了这条查询,于是把别家的项目名、负责人和截止日期一起写进了 CSV。客户没有恶意,也没有改任何参数,只是点了一次导出。

以下情境为教学假设,人物、引语和数字不代表经核实的真实案例。

隔离不是「记得加 where」,而是让漏写 where 的查询跑不起来。 只要正确性依赖每个人的记性,早晚会有一次遗漏。

这类事故的共同点是「默认全量」:查询方法不要求租户参数,表结构里 workspace_id 可空,ORM 的默认查询不带作用域。每一处默认都在鼓励漏写。

修复不能停在补上那一个 where。要问的是:还有多少条查询用了同样的写法?有没有办法让下一条查询不可能漏?这两个问题分别对应测试与结构。

这一篇讲四道防线:应用层作用域、数据库行级安全策略、集成测试、导出与删除按租户。四道防线各自会失效,所以需要叠加,而不是选一道最强的。

二、三种隔离方案怎么选

三种主流隔离方案没有绝对优劣,只有与租户规模、运维能力和合规要求是否匹配。

方案隔离强度运维成本迁移难度适合几家租户
行级 workspace_id中:靠查询与策略保证低:一套库、一套迁移低:加列与索引即可几十到几千家
schema per tenant中高:连接切换与迁移隔离中:N 套 schema、N 次迁移中:跨 schema 变更需编排几十到几百家
database per tenant高:故障与备份物理隔离高:N 套实例、监控与备份高:跨库统计与迁移复杂几家到几十家,客单价高

20 个团队选行级。 不是因为行级最安全,而是因为它在当前规模下,用一套迁移、一套监控、8 个额外索引,换到了足够好的隔离与可验证性。

隔离强度要按「最坏情况」评估。行级方案的最坏情况是一条查询漏了作用域;schema 方案的最坏情况是连接切错 schema;独立库方案的最坏情况是跨库任务连错实例。三种都会出错,区别在于错误的影响面与发现难度。

运维成本不只是服务器费用。schema 方案每次发布要跑 20 次迁移,任何一次失败都要单独回滚;独立库方案的备份、监控、连接池、慢查询都要乘以租户数。一个人的团队要先算人力,再算机器。

迁移难度决定你能不能改主意。行级方案从加一列开始,后续补索引与策略都是增量;schema 与独立库方案一旦租户数量上去,再合并回单库几乎是一次重写。

「适合几家租户」不是拍脑袋。行级适合几十到几千家;schema 适合需要按租户做结构差异、但租户数可控的场景;独立库适合客单价足够高、合同明确要求物理隔离的客户。

合规要求会改变答案。如果客户合同写明「数据不得与其他客户共存于同一数据库」,行级方案再省事也不能用;如果没有这条,行级方案的性价比明显更高。

三、四道防线与各自的失效条件

四道防线各自挡不同的失败,也各自有失效条件。

防线挡什么失效条件
应用层作用域业务查询漏写租户条件有人绕过仓储层直接写 SQL,或作用域参数可被请求覆盖
数据库行级安全策略(RLS)应用层漏过滤时的兜底连接使用表所有者或超级用户,或策略写成 using (true)
集成测试回归时发现跨租户读写测试数据只有一个租户,或只测详情不测列表与批量
导出与删除按租户大批量操作带走或删掉别家数据导出复用未加作用域的查询,删除按 id 列表逐条执行

四道防线里,RLS 是唯一不依赖应用代码正确性的那道。 但它只有在连接角色、策略与测试都正确时才成立,配置错一次就等于没有。

应用层作用域是第一道,也是最容易被绕过的一道。仓储层要求每个方法显式接收 workspace_id,并在拼查询时强制注入;业务代码不能自己拼 SQL,也不能从请求体读取租户 id。

RLS 是第二道。8 张业务表各启用行级安全,策略统一写成 workspace_id = current_setting('app.workspace_id')::uuid,连接时由应用设置该变量。策略不要按角色分支,角色判断留在应用层,数据库只负责租户边界。

RLS 的失效条件很具体:迁移脚本用表所有者执行、连接池复用了上一个请求的会话变量、策略被写成 using (true)、或者某些表忘了启用。任何一条都让第二道防线变成装饰。

集成测试是第三道。测试库至少要有两个租户,每个租户各有读写数据;用例覆盖详情、列表、导出、批量、删除。只有一个租户的测试库,永远测不出跨租户泄露。

导出与删除是第四道,也是影响面最大的操作。导出必须走带作用域的查询,并在文件生成后校验行数与租户;删除必须按租户条件执行,不能先查 id 再逐条删。

四道防线的顺序不是执行顺序,而是失效顺序。应用层先失效,RLS 兜底;RLS 配置错了,测试兜底;测试没覆盖到,导出与删除的租户校验兜底。每一道都要假设前一道已经失效。

四、复算:行级方案撑不撑得住

下面这组数字用来判断「行级方案是否撑得住」,不是性能基准。

业务表 = 8 张,每张 1 条 RLS 策略 → 8 条策略
越权用例 = 27 条(与 05 篇同口径:9 类场景 × 3 层)
单团队导出 = 3.2 万行;20 个团队合计 = 3.2 万 × 20 = 64 万行
行级方案额外索引 = 8 个(每张表一个 workspace_id 索引),写入成本约 +5%
schema 方案迁移 = 20 套 schema × 每次发布 = 20 次迁移

这些数字支持「行级够用」的结论,但不支持「行级永远够用」。 当单表行数、租户数或合规要求越过某个阈值,答案要重新算。

8 张表、8 条策略、8 个索引是一一对应的关系。策略数量少不代表简单,关键是每张表都要有,且每张表的策略条件一致;漏一张表,那张表就是缺口。

64 万行对一个 PostgreSQL 实例不算大。按每行 1 KB 估算约 640 MB,加上索引仍在单机舒适区;真正需要关注的是导出时的排序与内存,而不是存储。

写入成本约 +5% 是量级判断:每张表多一个 workspace_id 索引,插入与更新要多维护一个 B 树。对以读为主的项目管理类应用,这个代价可以接受;对高频写入的日志表,要单独评估。

schema 方案的 20 次迁移不是线性成本。每次发布要确认 20 套 schema 的版本一致,失败要单独回滚,漂移要单独检测;租户数翻倍,迁移次数也翻倍,而发布窗口不会翻倍。

27 条越权用例与 05 篇同口径,意味着两篇文章共用一套判定矩阵。05 篇验证「拦不拦得住」,本篇验证「即使拦漏了,数据层能不能兜住」;两套结果要能对上,不能各测各的。

复算的意义在于把「感觉够用」变成「知道边界在哪」。当团队数从 20 涨到 200,先重算导出行数与索引写入成本,再决定是继续行级还是拆库,而不是等事故发生。

五、行动清单与复算验收

清单按「先结构,再策略,最后验证」排序。

  • 8 张业务表全部有 workspace_id 列,且不允许为空
  • 所有业务查询经仓储层注入 workspace_id,业务代码不手写租户条件
  • 8 张表全部启用 RLS,策略条件统一,且没有 using (true)
  • 应用连接使用非表所有者角色,app.workspace_id 每个请求重设
  • 测试库至少两个租户,27 条越权用例合并前全绿
  • 导出按租户查询并校验行数,生成后抽查是否含其他租户数据
  • 删除按租户条件执行,禁止「先查 id 再逐条删」
  • 每季度复算一次表数、策略数、索引数与导出行数

验收标准:能证明一个团队永远查不到另一个团队的行:应用层、RLS、测试、导出删除四道防线各有一条可复现的证据;缺任何一道,都只能证明「目前没发现」。

使用边界与实操验收

本文讨论单库行级隔离的工程判据,不覆盖跨地域数据驻留、加密密钥隔离与合规审计。

隔离方案要跟客户合同一起读。 合同没写物理隔离,行级方案就够;合同写了,再省事的方案也不能用。

本文的 20 个团队、24 个工作区、3.2 万行导出都来自系列统一基线,不对应真实客户;四道防线的效果取决于你的测试数据与连接配置,不取决于本文的数字。

RLS 的语法与行为在不同数据库间差异很大。PostgreSQL 的策略、会话变量与绕过规则是本文的核对对象;MySQL、SQLite 或托管数据库的等价机制需要单独验证,不能直接照搬。

跨租户统计是行级方案的隐藏成本。单库聚合很便宜,但它也意味着任何一条分析查询都可能越过租户边界;分析账号应使用只读副本与脱敏视图,不要复用业务连接。

备份与恢复是隔离之外的另一条线。备份通常是整库的,恢复演练要验证「恢复后 RLS 仍然启用、workspace_id 数据仍在」,否则隔离在灾难恢复后可能悄悄失效。

如果只有几个高客单价客户、合同明确要求物理隔离,独立库方案反而更省心:故障、备份、删除都以库为单位,解释成本最低。方案选择要回到合同与人力,而不是技术偏好。

数据驻留与加密密钥隔离不在本文范围内;它们会进一步限制可选方案,尤其是当客户要求数据留在特定区域或自带密钥时,行级方案的「一套库」前提就不再成立。

关键参考与核验范围

核验日期:2026-10-25。以下链接用于核对行级安全策略的语法、越权分类与多租户存储方案,本文未逐条引用其内容。

引用口径。 文中 20 个团队、24 个工作区、8 张业务表、3.2 万行导出与 64 万行合计均为本系列统一的教学基线,不对应任何真实客户;27 条用例与 20 次迁移是复算口径,不是压测结果。

核验范围。 机制层面核对到 RLS 的启用方式、连接角色与策略条件;未核对任何云厂商的托管数据库定价,也未做行级与 schema 方案的实测性能对比。

如何自行复核。 建两个租户、各 3 条项目数据,用租户 A 的连接执行一条故意不带 where 的查询:有 RLS 时应只返回 3 行,没有 RLS 时会返回 6 行。再把连接切成表所有者重跑一次,确认绕过是否如预期发生,并据此检查生产连接角色。

系列导航

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「Indie」更多文章

  1. 开源项目验收:十项能力、四维评分与三种不该开源的项目
  2. 治理、fork 与退出:17 位贡献者、82% 的提交与一次改协议的代价
  3. 双许可与企业版:九家询价、两家成交与 4,800 美元