概述
多数租户泄漏并不复杂:新接口写了 SELECT * FROM invoices WHERE id = $1,id 来自 URL,租户过滤只存在于 UI 或部分路由使用的中间件里。修复必须是结构性的:每个访问租户数据的查询都要受租户约束,最好由数据库在查询遗忘条件时仍然强制执行。
步骤
- 建立两个租户与一个归属第一个租户的对象。
- 用第二个租户的用户访问详情、导出、更新等所有接受 id 的路径,断言 404 且数据未被修改。
- 增加行级安全作为第二层防护:
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant_id', true)::uuid);
- 在每个事务开始时设置租户,禁止使用会话级
SET:
await client.query('BEGIN');
await client.query("SELECT set_config('app.tenant_id', $1, true)", [tenantId]);
- 验证缺少上下文时读返回零行、写触发策略违规,而不是放行。
验证
跨租户用户对详情、PDF 导出、更新接口均应得到 404,发票金额保持 4200。直接以应用角色执行 SELECT * FROM invoices 只能看到当前租户的行。清空上下文后插入应报 new row violates row-level security policy,同一连接的下一个事务不应继承之前的值。
注意事项
- 表所有者默认绕过 RLS,必须使用非所有者角色并开启 FORCE。
- 连接池会复用连接,
SET LOCAL才能把设置限制在事务内。 - 策略中的缺失值应导致零行,而不是允许全部;写入策略还需要
WITH CHECK。 - 难以猜测的 id 不是授权手段,不能替代租户过滤。
- 启用 RLS 后要检查大表执行计划,
tenant_id应位于复合索引首列。