必须用filter而非interceptor,因其在servlet容器最外层运行,覆盖全部http流量,确保租户id在请求入口就提取并绑定到threadlocal,且finally兜底清理防污染;数据隔离需配合mybatis-plus的tenantlineinnerinterceptor等持久层机制。

多租户权限出问题,往往不是权限逻辑本身错了,而是租户上下文没立住——Filter 没生效、上下文没传下去、数据查询没隔离,三者缺一不可。用好 Tenant Filter 是最直接、最可控的破局点。
为什么必须用 Filter 而不是拦截器?
HandlerInterceptor 只拦 @Controller 方法,静态资源、Actuator 端点、文件上传、WebSocket 握手等统统绕过。而租户识别必须在请求入口就完成,否则后续所有环节(安全校验、数据过滤、日志埋点)都可能错乱。
Filter(尤其是 OncePerRequestFilter)运行在 Servlet 容器最外层,覆盖全部 HTTP 流量。TenantContextWebFilter 就是靠它,在 DispatcherServlet 之前就把 tenant-id 从 Header 或 JWT 中提取出来,塞进 ThreadLocal,确保整个请求链路都能拿到正确租户 ID。
Tenant Filter 的三个硬性动作
一个可靠的 Tenant Filter 必须做完以下三件事,缺一不可:
-
提取租户标识:优先从
tenant-id或X-Tenant-ID请求头读取;无头时尝试解析 JWT 的tenant_id声明;极少数回调场景(如支付通知)需手动查库补全 -
绑定到线程上下文:调用
TenantContextHolder.setTenantId(tenantId),底层是 ThreadLocal,保证同一线程内任意位置可取 -
兜底清理:在
finally块中执行TenantContextHolder.clear(),防止线程复用导致上下文污染
数据层自动隔离怎么配?
Filter 只负责“设上下文”,数据隔离要靠持久层配合:
-
MyBatis-Plus 方案:启用
TenantLineInnerInterceptor,配置tenantIdColumn = "tenant_id",它会在所有 INSERT/UPDATE/SELECT/DELETE SQL 自动追加AND tenant_id = ? -
JPA/EF Core 方案:在
OnModelCreating中对实体调用HasQueryFilter(p => p.TenantId == _currentTenantId),依赖 DI 注入的租户 ID -
需要跳过隔离的场景:比如租户管理后台查所有租户列表,或系统级统计报表,在 Mapper 方法上加
@InterceptorIgnore(tenantLine = "true")即可
常见踩坑点
权限乱,90% 出现在这几个地方:
- Filter Order 设置太靠后,被 Security 过滤器链提前拦截,根本没机会执行
- 异步线程(@Async、CompletableFuture、线程池)里取不到 ThreadLocal 值,必须手动传递租户 ID
- 数据库表漏加
tenant_id字段,或字段名不统一(比如有的叫tenant_code),导致过滤失效 - 未给
tenant_id字段建索引,大租户数据量下查询性能断崖下跌











