不能只靠应用层加where tenant_id = ?,因漏写、orm绕过、join遗漏、测试误用等易致数据泄露;应采用视图+连接池绑定+mybatis拦截器+权限管控的组合方案。

为什么不能只靠应用层加 WHERE tenant_id = ?
因为漏写、ORM 自动生成 SQL 绕过、JOIN 时遗漏、测试环境误用共享库——这些都会导致租户数据泄露。硬编码过滤条件在 DAO 层或 MyBatis 的 WHERE 里,等于把隔离责任全押在开发自觉性上。真实线上事故里,80% 的租户越界读取都源于某次重构删掉了某个 AND tenant_id = #{tenantId}。
推荐方案:MySQL 5.7+ 的行级安全策略(Row-Level Security)
MySQL 原生支持基于 SQL SECURITY DEFINER + INFORMATION_SCHEMA + 视图 + 系统变量的组合实现逻辑隔离,但真正稳定可控的是 8.0.22+ 的 CREATE POLICY(需开启 check_constraint 和 row_security)。不过目前生产主流仍是 5.7/8.0.19–8.0.21,所以更务实的做法是:
- 所有业务表必须有
tenant_id字段(NOT NULL,建议加联合索引如(tenant_id, id)) - 禁止直接操作基表,只允许通过带强制过滤的视图访问:
CREATE VIEW user_view AS SELECT * FROM user WHERE tenant_id = @current_tenant_id;
- 连接池初始化时执行
SET @current_tenant_id = ?,确保每次连接绑定唯一租户上下文 - 应用层在获取连接后,立刻校验该连接的
@current_tenant_id是否与当前请求一致(防止连接复用污染)
MyBatis 中如何避免手写 tenant_id 条件
手动在每个 <where></where> 里加 AND tenant_id = #{tenantId} 不仅重复,还会在 <foreach></foreach> 或动态 SQL 分支中被跳过。正确做法是:
- 用 MyBatis 的
Interceptor拦截Executor.update和Executor.query - 解析
MappedStatement.getBoundSql().getSql(),对非SELECT COUNT、非INSERT INTO ... SELECT类型的语句,自动注入AND tenant_id = ?到WHERE子句末尾(注意处理已有WHERE/AND/OR) - 拦截器中必须排除
tenant_id字段本身被更新的场景(如租户迁移),否则会误拦UPDATE user SET tenant_id = ? WHERE id = ? - 为防绕过,数据库用户权限应限制为仅能执行
SELECT/INSERT/UPDATE/DELETE,且禁止SHOW TABLES或直接查information_schema
跨租户管理操作(如后台审计)怎么安全放开
管理员需要查所有租户数据,但又不能给超级账号。可行路径只有两条:
- 单独建一个管理库视图,如
admin_user_all,定义为SELECT *, 'admin' as _access_mode FROM user,并限制该视图只能被特定 DB 用户(如admin@localhost)访问,且该用户无法连业务应用库 - 临时切换隔离策略:在管理接口中显式调用
SET @bypass_tenant_check = 1,并在 SQL 解析拦截器中识别该变量,跳过注入逻辑——但必须配合 IP 白名单 + JWT 权限二次校验,且日志中完整记录每次 bypass 的时间、用户、SQL 原文
别忘了:任何绕过都得留下可追溯痕迹。真正的隔离难点不在技术实现,而在让每个 SQL 执行路径都明确知道自己“属于谁”,以及“谁有权看别人”。











