ON子句漏写tenant_id会导致租户数据泄露,因JOIN不继承主表租户条件且优化器不下推过滤,可能引发笛卡尔积与跨租户数据暴露。

为什么 ON 子句漏写 tenant_id 会导致租户数据泄露
多租户场景下,LEFT JOIN sys_user u ON u.id = o.user_id 这类写法看似正常,但只要 sys_user 表没在 ON 或 WHERE 中显式限定 tenant_id,就可能让 A 租户查到 B 租户的用户数据。根本原因是:JOIN 不自动继承主表的租户过滤条件,数据库优化器也不会把外层 WHERE o.tenant_id = 't123' 下推到右表上——尤其当右表无索引支撑时,会退化为笛卡尔积再过滤。
MySQL / PostgreSQL 中 ON 子句加 tenant_id 的实操写法
必须让关联双方都带上租户约束,且字段名、类型、NULL 性保持一致,否则索引失效或隐式转换拖慢查询。
- 标准写法(推荐):
LEFT JOIN sys_user u ON u.id = o.user_id AND u.tenant_id = o.tenant_id - 若
sys_user表没有tenant_id字段,说明设计已违规——所有含租户语义的表都必须有该字段并建复合索引 - PostgreSQL 注意:如果用视图封装了租户过滤(如
v_orders),嵌套 JOIN 时仍需在ON中重复u.tenant_id = current_setting('app.tenant_id'),否则视图的 WHERE 可能被优化器忽略 - MySQL 用户变量方式不适用 ON 子句——
@tenant_id在 JOIN 条件中不可用,必须提前在应用层注入为字面量
容易踩的坑:索引、NULL 和连接池残留值
即使语法写对了,以下三点任一缺失都会让 tenant_id 在 JOIN 中形同虚设:
-
sys_user表必须有(tenant_id, id)联合索引,否则AND u.tenant_id = o.tenant_id无法走索引,JOIN 效率断崖下跌 - 若
o.tenant_id允许为 NULL,而u.tenant_id是 NOT NULL,则u.tenant_id = o.tenant_id在 NULL 时恒为 FALSE,导致 LEFT JOIN 变成 INNER JOIN,业务逻辑错乱 - 连接池复用时,上一个请求遗留的
SET @tenant_id = 't456'可能污染当前查询——务必配置connection-init-sql = SET @tenant_id = NULL(MySQL)或RESET app.tenant_id(PostgreSQL)
比 ON 更可靠的做法:优先用行级安全策略(RLS)替代手工拼条件
手动维护每个 JOIN 的 tenant_id 条件极易遗漏,尤其在动态 SQL 或 MyBatis @SelectProvider 场景下。PostgreSQL 原生 RLS 才是真正兜底方案:
- 启用后,所有访问
sys_user表的语句(无论是否 JOIN、是否走视图)都会自动追加tenant_id = current_setting('app.tenant_id') - 执行
ALTER TABLE sys_user ENABLE ROW LEVEL SECURITY后,必须配策略:CREATE POLICY tenant_isolation ON sys_user USING (tenant_id = current_setting('app.tenant_id', true)::uuid) - 关键点:RLS 策略对函数、视图、触发器全部生效;而 ON 子句只管这一次 JOIN,漏一处就全盘失效
真正难的不是写对 ON 条件,而是确保它在每一个 JOIN、每一个子查询、每一个连接生命周期里都不被绕过——这已经超出 SQL 语法层面,进入权限体系和部署规范的范畴。

















