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 层的可选条件。
三、数据泄露风险与典型失效模式
多租户数据泄露通常不是单点漏洞,而是“上下文丢失 + 过滤缺失 + 权限过宽”三者叠加。常见失效模式包括:
- 租户上下文丢失:请求进入异步线程池后,ThreadLocal 中的
tenant_id未传递,下游查询退化为全租户扫描。 - 越权访问(IDOR):接口只校验“资源存在”,未校验“资源属于当前租户”,攻击者遍历 ID 即可读取他人数据。
- 聚合查询泄露:报表或统计接口未按租户聚合,返回全局计数,间接暴露其他租户的业务规模。
- 备份与导出泄露:运维导出全库备份后未按租户脱敏,或导出文件被跨租户下载。
四、隔离策略设计与验证任务
一个可落地的隔离策略应包含四层控制:入口鉴权、上下文传播、数据访问强制过滤、出口审计。入口层从 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 兜底生效,才能认为隔离策略在“主链路 + 旁路链路”上闭环。
最后要强调:隔离策略不是一次性配置,而是持续验证的工程能力。建议把上述验证清单纳入 CI,每次涉及数据访问层、缓存层、异步任务的变更都自动跑一遍跨租户用例。只有让“越界即失败”成为默认行为,多租户的数据边界才真正可信。