如何通过MyBatis拦截器在执行SQL前自动注入多租户物理隔离ID实现数据隔离?

2026-04-24 17:182阅读0评论SEO资源
  • 内容介绍
  • 相关推荐

本文共计704个文字,预计阅读时间需要3分钟。

如何通过MyBatis拦截器在执行SQL前自动注入多租户物理隔离ID实现数据隔离?

直接使用MyBatis原生+Interceptor手动解析SQL并注入租户条件,风险极高、维护成本巨大。MyBatis-Plus内置的 TenantLineInnerInterceptor已覆盖所有标准DML场景(SELECT、INSERT、UPDATE、DELETE),经过大量SaaS生产环境验证,无需自行构建轮子。

TenantLineInnerInterceptor 怎么正确配置 tenant_id 注入逻辑

核心是实现 TenantLineHandler 接口,重点不在“怎么写”,而在“怎么取”和“怎么防错”:

  • getTenantId() 必须返回 Expression 类型,不能直接 return 字符串或数字;常见错误是返回 new StringValue("xxx") 却没处理 null 场景——一旦 TenantContext.getCurrentTenantId() 为 null,整个 SQL 会崩掉
  • getTenantIdColumn() 值必须与数据库表中真实字段名完全一致(大小写敏感),比如 MySQL 表定义是 tenant_id,就不能写成 TENANT_IDtenantId
  • ignoreTable(String tableName) 要显式排除公共表,例如 "sys_dict""sys_config";漏掉会导致这些表被错误加上 WHERE tenant_id = ?,查不到全局数据

为什么 INSERT 语句能自动补 tenant_id 字段

插件不是简单拼字符串,而是用 JSQLParser 解析 AST 后,在 INSERT 的 column list 和 values list 中双向注入:

INSERT INTO user (name, email) VALUES (?, ?) → INSERT INTO user (name, email, tenant_id) VALUES (?, ?, ?)

但前提是你的实体类(如 User)里必须声明 tenant_id 字段,并加 @TableField(fill = FieldFill.INSERT) 注解,否则 MP 不知道该往哪填值。漏掉这个注解,INSERT 成功但数据无租户归属,等于裸奔。

租户 ID 来源必须绑定请求生命周期,不能靠静态变量

最常踩的坑:在 getTenantId() 里直接调用 SecurityContextHolder.getContext().getAuthentication() 或硬编码,导致异步线程、定时任务、单元测试中取不到值,或跨请求污染。

  • 务必用 ThreadLocal 封装 TenantContext,并在 Web 层用 OncePerRequestFilter 或 Spring MVC HandlerInterceptor 提前解析并 set
  • 解析来源优先级建议:Header X-Tenant-ID > JWT payload 中的 tenant_id > 子域名(如 tenant1.example.com)> fallback 到默认租户(需明确日志告警)
  • 每次请求结束必须调用 TenantContext.clear(),否则 Tomcat 线程复用时会把上一个租户 ID 带进下一个请求

真正难的不是配置拦截器,而是确保租户上下文在所有执行路径中不丢失——包括 Dubbo 远程调用、RabbitMQ 消费、Scheduled 定时任务。这些地方都需要手动透传或重置 TenantContext,框架不会自动帮你做。

本文共计704个文字,预计阅读时间需要3分钟。

如何通过MyBatis拦截器在执行SQL前自动注入多租户物理隔离ID实现数据隔离?

直接使用MyBatis原生+Interceptor手动解析SQL并注入租户条件,风险极高、维护成本巨大。MyBatis-Plus内置的 TenantLineInnerInterceptor已覆盖所有标准DML场景(SELECT、INSERT、UPDATE、DELETE),经过大量SaaS生产环境验证,无需自行构建轮子。

TenantLineInnerInterceptor 怎么正确配置 tenant_id 注入逻辑

核心是实现 TenantLineHandler 接口,重点不在“怎么写”,而在“怎么取”和“怎么防错”:

  • getTenantId() 必须返回 Expression 类型,不能直接 return 字符串或数字;常见错误是返回 new StringValue("xxx") 却没处理 null 场景——一旦 TenantContext.getCurrentTenantId() 为 null,整个 SQL 会崩掉
  • getTenantIdColumn() 值必须与数据库表中真实字段名完全一致(大小写敏感),比如 MySQL 表定义是 tenant_id,就不能写成 TENANT_IDtenantId
  • ignoreTable(String tableName) 要显式排除公共表,例如 "sys_dict""sys_config";漏掉会导致这些表被错误加上 WHERE tenant_id = ?,查不到全局数据

为什么 INSERT 语句能自动补 tenant_id 字段

插件不是简单拼字符串,而是用 JSQLParser 解析 AST 后,在 INSERT 的 column list 和 values list 中双向注入:

INSERT INTO user (name, email) VALUES (?, ?) → INSERT INTO user (name, email, tenant_id) VALUES (?, ?, ?)

但前提是你的实体类(如 User)里必须声明 tenant_id 字段,并加 @TableField(fill = FieldFill.INSERT) 注解,否则 MP 不知道该往哪填值。漏掉这个注解,INSERT 成功但数据无租户归属,等于裸奔。

租户 ID 来源必须绑定请求生命周期,不能靠静态变量

最常踩的坑:在 getTenantId() 里直接调用 SecurityContextHolder.getContext().getAuthentication() 或硬编码,导致异步线程、定时任务、单元测试中取不到值,或跨请求污染。

  • 务必用 ThreadLocal 封装 TenantContext,并在 Web 层用 OncePerRequestFilter 或 Spring MVC HandlerInterceptor 提前解析并 set
  • 解析来源优先级建议:Header X-Tenant-ID > JWT payload 中的 tenant_id > 子域名(如 tenant1.example.com)> fallback 到默认租户(需明确日志告警)
  • 每次请求结束必须调用 TenantContext.clear(),否则 Tomcat 线程复用时会把上一个租户 ID 带进下一个请求

真正难的不是配置拦截器,而是确保租户上下文在所有执行路径中不丢失——包括 Dubbo 远程调用、RabbitMQ 消费、Scheduled 定时任务。这些地方都需要手动透传或重置 TenantContext,框架不会自动帮你做。