4. 多租户环境下的租户隔离与数据边界

多租户是 SaaS 的基本经济模型:一套应用实例、一套运维体系,服务成百上千个客户。但“共用”与“隔离”天然矛盾——租户 A 与租户 B 的数据可能落在同一张表、同一个索引、同一个对象存储桶,甚至同一块缓存里。租户隔离要解决的核心问题不是“能不能共用”,而是“共用之后,如何保证任何一条读写路径都不会越过租户边界”。

一、租户隔离模型:从共享到独占的连续谱

业界通常把隔离模型分为三类,它们不是互斥选项,而是同一系统内可按租户等级混用的连续谱。

  • 共享数据库、共享 Schema:所有租户数据存于同一批表,靠 tenant_id 列区分。成本最低、扩展性最好,但隔离强度最弱,任何一条漏写 WHERE tenant_id = ? 的 SQL 都可能造成跨租户泄露。
  • 共享数据库、独立 Schema:每个租户一个 Schema(如 PostgreSQL 的 tenant_001.orders),连接时切换 search_path。隔离强度中等,迁移与备份可按租户粒度进行,但 Schema 数量膨胀会带来元数据压力。
  • 独立数据库/独立实例:每租户一套库甚至一套实例。隔离最强,适合金融、医疗等强合规租户,但成本与运维复杂度最高。

选择模型时,建议按“数据敏感度 × 租户付费等级 × 合规要求”三维打分。例如免费版租户走共享 Schema,企业版走独立 Schema,受监管行业客户走独立数据库。这种分层策略既控制了成本,又让高价值租户获得可承诺的隔离等级。

二、逻辑隔离与物理隔离的边界

逻辑隔离依赖软件层的过滤条件、权限校验和命名空间,物理隔离依赖独立的存储、网络或进程资源。二者并非二选一:一个系统往往在数据层做逻辑隔离,在计算层做物理隔离(如每租户独立容器),在存储层做逻辑隔离但加密密钥独立。

需要特别注意的是,逻辑隔离的失效点往往不在主查询,而在“旁路路径”:

  • 缓存键未带租户前缀,导致 A 租户读到 B 租户的缓存对象;
  • 异步任务从队列取消息时未校验 tenant_id,批处理误扫全表;
  • 全文检索索引未按租户分区,搜索结果跨租户返回;
  • 对象存储的预签名 URL 未绑定租户路径,被猜测或重放。

因此,隔离设计必须覆盖“主链路 + 旁路链路”,把租户上下文作为贯穿请求全生命周期的强制参数,而不是某个 DAO 层的可选条件。

三、数据泄露风险与典型失效模式

多租户数据泄露通常不是单点漏洞,而是“上下文丢失 + 过滤缺失 + 权限过宽”三者叠加。常见失效模式包括:

  1. 租户上下文丢失:请求进入异步线程池后,ThreadLocal 中的 tenant_id 未传递,下游查询退化为全租户扫描。
  2. 越权访问(IDOR):接口只校验“资源存在”,未校验“资源属于当前租户”,攻击者遍历 ID 即可读取他人数据。
  3. 聚合查询泄露:报表或统计接口未按租户聚合,返回全局计数,间接暴露其他租户的业务规模。
  4. 备份与导出泄露:运维导出全库备份后未按租户脱敏,或导出文件被跨租户下载。
租户隔离的验证不能只测“正常路径能否读到自己的数据”,更要测“异常路径能否读到别人的数据”。后者才是隔离是否真正生效的判据。

四、隔离策略设计与验证任务

一个可落地的隔离策略应包含四层控制:入口鉴权、上下文传播、数据访问强制过滤、出口审计。入口层从 JWT 或会话中解析 tenant_id 并写入请求上下文;传播层确保同步调用、异步任务、消息消费都能携带该上下文;数据访问层通过 ORM 拦截器或行级安全(RLS)自动追加租户条件;出口层记录跨租户访问尝试并告警。

以 PostgreSQL 行级安全为例,可在共享 Schema 模型下用策略强制隔离:

ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

CREATE POLICY tenant_isolation ON orders
  USING (tenant_id = current_setting('app.current_tenant')::uuid);

-- 每个请求开始时设置当前租户
SET app.current_tenant = '3f2a...c9';

这样即使应用层漏写过滤条件,数据库也会拒绝返回其他租户的行。RLS 是逻辑隔离中性价比很高的“兜底防线”,但它要求连接池在每次借出连接时正确设置会话变量,否则会串租户。

验证任务建议设计成可执行的测试用例集,而不是人工点检。下面是一份可直接参考的隔离验证清单:

验证项构造输入预期结果
主查询隔离租户 A 令牌查询订单列表仅返回 A 的订单,无 B 记录
IDOR 越权租户 A 令牌请求 B 的订单 ID返回 404/403,不返回数据
缓存键隔离A、B 先后请求同一资源 ID各自命中自己的缓存,无交叉
异步任务隔离触发 A 的导出任务导出文件仅含 A 的数据
聚合接口隔离A 请求统计接口计数仅覆盖 A 的租户范围
RLS 兜底绕过应用层直接执行无过滤 SQL数据库仅返回当前租户行

执行时,建议用两个真实租户账号交叉验证:先用 A 的令牌访问 B 的资源,再用 B 的令牌访问 A 的资源,并检查日志中是否产生跨租户告警。若所有用例均通过,且 RLS 兜底生效,才能认为隔离策略在“主链路 + 旁路链路”上闭环。

多租户隔离控制链与旁路泄露点 租户 A tenant_id = A 租户 B tenant_id = B 入口鉴权 解析 JWT 写入上下文 上下文传播 同步 / 异步 消息 / 任务 强制过滤 ORM 拦截 + RLS tenant_id 条件 数据存储 共享表 / 独立 Schema 行级安全策略 旁路泄露点(隔离失效高发区) 缓存键缺租户前缀 A 读到 B 的缓存 异步任务丢上下文 批处理全表扫描 检索索引未分区 搜索结果跨租户 预签名 URL 未绑定 对象存储越权下载 聚合接口未过滤 全局计数泄露规模 备份导出未脱敏 跨租户文件泄露 出口审计 跨租户访问告警 审计日志留存 异常 ID 遍历检测 导出行为监控 RLS 拒绝记录

最后要强调:隔离策略不是一次性配置,而是持续验证的工程能力。建议把上述验证清单纳入 CI,每次涉及数据访问层、缓存层、异步任务的变更都自动跑一遍跨租户用例。只有让“越界即失败”成为默认行为,多租户的数据边界才真正可信。