java每日精进 4.26【多租户之过滤器及请求处理流程】
一月没更,立誓以后断更三天我就是狗!!!!!!!!
研究多租户框架中一条请求的处理全流程
@RestController
@RequestMapping("/users")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/save")public boolean save(@RequestBody User user) {return userService.save(user);}@GetMapping("/get")public List<User> getAll() {return userService.list();}@GetMapping("/getById")public User getById(@RequestParam Long id) {return userService.getById(id);}
}
以getById接口为例
1. 请求接收
- 客户端请求:假设客户端发送以下 GET 请求:
-
GET /users/getById?id=123 HTTP/1.1 Host: localhost:8080 tenant-id: 1 Authorization: Bearer <token>
- Tomcat 接收:Spring Boot 嵌入的 Tomcat 服务器监听端口(默认 8080),接收请求并解析 HTTP 报文。
- DispatcherServlet:Tomcat 将请求交给 Spring 的 DispatcherServlet,它负责路由和处理所有 HTTP 请求。
2. 执行顺序
-
TenantContextWebFilter (Order = WebFilterOrderEnum.TENANT_CONTEXT_FILTER)
-
Spring Security 过滤器链:
-
DisableEncodeUrlFilter
-
WebAsyncManagerIntegrationFilter
-
SecurityContextPersistenceFilter
-
HeaderWriterFilter
-
LogoutFilter
-
RequestCacheAwareFilter
-
SecurityContextHolderAwareRequestFilter
-
AnonymousAuthenticationFilter
-
SessionManagementFilter
-
-
TenantSecurityWebFilter (继承自 ApiRequestFilter)
-
ExceptionTranslationFilter
-
FilterSecurityInterceptor
阶段一:TenantContextWebFilter 处理
public class TenantContextWebFilter extends OncePerRequestFilter {protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {System.out.println("+++++tenant-id:"+request.getHeader("tenant-id")+"+++++");// 从请求头获取租户IDLong tenantId = WebFrameworkUtils.getTenantId(request);if (tenantId != null) {TenantContextHolder.setTenantId(tenantId); // 设置到线程局部变量}try {chain.doFilter(request, response); // 继续过滤器链} finally {TenantContextHolder.clear(); // 清理线程局部变量}}
}
-
从请求头中提取
tenant-id
并设置到TenantContextHolder
线程局部变量中 -
确保请求处理完成后清理线程局部变量,避免内存泄漏
阶段二:Spring Security 基础过滤器
这些过滤器主要处理安全相关的基础功能:
-
SecurityContextPersistenceFilter
: 从Session中加载安全上下文 -
AnonymousAuthenticationFilter
: 如果用户未认证,创建一个匿名身份 -
其他过滤器处理会话、头信息等基础安全功能
+-------------------------------+
| DisableEncodeUrlFilter |
| 禁用URL编码过滤器:用于禁用对URL的编码处理 |
+-------------------------------+|V
+-------------------------------+
| WebAsyncManagerIntegrationFilter |
| 异步管理器集成过滤器:将Spring的异步管理与安全上下文集成 |
+-------------------------------+|V
+-------------------------------+
| SecurityContextPersistenceFilter |
| 安全上下文持久化过滤器:在请求处理前后管理安全上下文的持久化 |
+-------------------------------+|V
+-------------------------------+
| HeaderWriterFilter |
| 头部写入过滤器:在响应中添加安全相关的HTTP头部信息 |
+-------------------------------+|V
+-------------------------------+
| LogoutFilter |
| 注销过滤器:处理用户的注销请求,清除相关的安全信息 |
+-------------------------------+|V
+-------------------------------+
| RequestCacheAwareFilter |
| 请求缓存感知过滤器:处理请求的缓存,用于重定向后恢复请求 |
+-------------------------------+|V
+-------------------------------+
| SecurityContextHolderAwareRequestFilter |
| 安全上下文持有者感知请求过滤器:包装请求对象,使其能访问安全上下文 |
+-------------------------------+|V
+-------------------------------+
| AnonymousAuthenticationFilter |
| 匿名认证过滤器:在没有认证信息时,为请求提供匿名身份 |
+-------------------------------+|V
+-------------------------------+
| SessionManagementFilter |
| 会话管理过滤器:管理用户会话,处理会话过期等问题 |
+-------------------------------+|V
+-------------------------------+
| ExceptionTranslationFilter |
| 异常转换过滤器:将安全异常转换为Spring Security能处理的异常 |
+-------------------------------+|V
+-------------------------------+
| FilterSecurityInterceptor |
| 过滤器安全拦截器:对请求进行最终的安全检查和授权控制 |
+-------------------------------+
阶段三:TenantSecurityWebFilter 处理
作用:
-
检查登录用户是否有权限访问请求的租户
-
对于非忽略URL,必须包含有效的租户ID
-
校验租户状态(是否禁用、过期等)
public class TenantSecurityWebFilter extends ApiRequestFilter {protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {Long tenantId = TenantContextHolder.getTenantId();// 1. 检查登录用户租户权限User user = SecurityFrameworkUtils.getLoginUser();if (user != null) {if (tenantId == null) {// 使用登录用户的租户IDtenantId = user.getTenantId();TenantContextHolder.setTenantId(tenantId);} else if (!user.getTenantId().equals(tenantId)) {// 租户不匹配,越权访问log.error("越权访问");ServletUtils.writeJSON(response, CommonResult.error(...));return;}}// 2. 非忽略URL需校验租户if (!isIgnoreUrl(request)) {if (tenantId == null) {// 未传递租户IDlog.error("未传递租户编号");ServletUtils.writeJSON(response, CommonResult.error(...));return;}// 3. 校验租户状态try {tenantFrameworkService.validTenant(tenantId);} catch (Throwable ex) {CommonResult<?> result = globalExceptionHandler.allExceptionHandler(request, ex);ServletUtils.writeJSON(response, result);return;}} else {// 忽略URL处理if (tenantId == null) {TenantContextHolder.setIgnore(true); // 标记忽略租户}}chain.doFilter(request, response);}
}
阶段四:Controller 处理
-
MyBatis-Plus 会调用
TenantLineInnerInterceptor
-
拦截器检查
TenantDatabaseInterceptor
的规则:-
检查表是否在忽略列表中
-
如果没有忽略,自动添加租户条件
WHERE tenant_id = ?
-
1.多租户配置类YudaoTenantAutoConfiguration:
@AutoConfiguration
@ConditionalOnProperty(prefix = "moyun.tenant", value = "enable", matchIfMissing = true)
@EnableConfigurationProperties(TenantProperties.class)
public class YudaoTenantAutoConfiguration {@Beanpublic TenantLineInnerInterceptor tenantLineInnerInterceptor(TenantProperties properties) {return new TenantLineInnerInterceptor(new TenantDatabaseInterceptor(properties));}@Beanpublic FilterRegistrationBean<TenantContextWebFilter> tenantContextWebFilter() {FilterRegistrationBean<TenantContextWebFilter> registrationBean = new FilterRegistrationBean<>();registrationBean.setFilter(new TenantContextWebFilter());registrationBean.setOrder(WebFilterOrderEnum.TENANT_CONTEXT_FILTER);return registrationBean;}
}
作用:
-
条件化装配多租户相关组件
-
注册两个核心组件:
-
TenantLineInnerInterceptor
: MyBatis Plus SQL拦截器,自动拼接租户条件或判断是否跳过租户判断拼接; -
TenantContextWebFilter
: 租户上下文过滤器,将租户id设置到上下文;
-
2. TenantContextHolder (租户上下文持有者)
/*** 多租户上下文 Holder** @author 芋道源码*/
public class TenantContextHolder {/*** 当前租户编号*/private static final ThreadLocal<Long> TENANT_ID = new TransmittableThreadLocal<>();/*** 是否忽略租户*/private static final ThreadLocal<Boolean> IGNORE = new TransmittableThreadLocal<>();/*** 获得租户编号** @return 租户编号*/public static Long getTenantId() {return TENANT_ID.get();}/*** 获得租户编号。如果不存在,则抛出 NullPointerException 异常** @return 租户编号*/public static Long getRequiredTenantId() {Long tenantId = getTenantId();if (tenantId == null) {throw new NullPointerException("TenantContextHolder 不存在租户编号!可参考文档:"+ DocumentEnum.TENANT.getUrl());}return tenantId;}public static void setTenantId(Long tenantId) {TENANT_ID.set(tenantId);}public static void setIgnore(Boolean ignore) {IGNORE.set(ignore);}/*** 当前是否忽略租户** @return 是否忽略*/public static boolean isIgnore() {return Boolean.TRUE.equals(IGNORE.get());}public static void clear() {TENANT_ID.remove();IGNORE.remove();}}
TenantContextHolder
是一个用于管理多租户系统中租户信息的工具类,它使用ThreadLocal
来存储当前线程的租户上下文信息。主要作用
存储当前租户ID:在同一个线程中共享租户ID
控制租户过滤:可以临时忽略租户过滤条件
线程安全:使用
TransmittableThreadLocal
保证线程安全且支持线程池场景核心功能说明
getTenantId()
: 获取当前租户ID(可能为null)
getRequiredTenantId()
: 获取当前租户ID(如果不存在则抛出异常)
setTenantId(Long)
: 设置当前租户ID
isIgnore()
: 检查是否忽略租户过滤
setIgnore(Boolean)
: 设置是否忽略租户过滤
clear()
: 清理当前线程的租户上下文为什么用 TransmittableThreadLocal?
与普通ThreadLocal的区别:
特性 ThreadLocal TransmittableThreadLocal 线程隔离 ✅ ✅ 线程池支持 ❌(会丢失上下文) ✅(自动传递) 异步任务支持 ❌ ✅ 性能开销 低 略高(需注册TTL修饰的线程池) 实际工作流程示例
// 主线程设置值 TenantContextHolder.setTenantId(1L); // TENANT_ID线程变量=1 TenantContextHolder.setIgnore(false); // IGNORE线程变量=false// 当新线程/线程池任务执行时: // 普通ThreadLocal会丢失这两个值 // TransmittableThreadLocal会自动传递这两个值// 子线程中仍然可以获取: Long tenantId = TenantContextHolder.getTenantId(); // 得到1 boolean ignore = TenantContextHolder.isIgnore(); // 得到false为什么要这样设计?
线程安全:避免使用全局变量导致的多线程冲突
调用链透明:在方法调用链中无需显式传递tenantId参数
灵活控制:可以通过IGNORE临时绕过租户隔离
兼容异步:支持线程池、异步任务等复杂场景
注意事项
必须成对使用:set后必须clear/remove,否则会导致:
内存泄漏(特别是线程池场景)
租户信息错乱(后续请求可能读到错误的tenantId)
默认值规则:
TENANT_ID.get() 未设置时返回null
IGNORE.get() 未设置时返回null,isIgnore()做了null安全处理(返回false)
性能影响:
TransmittableThreadLocal比普通ThreadLocal稍慢
在极高并发场景需要考虑性能损耗
3. TenantDatabaseInterceptor (数据库拦截器)
-
实现MyBatis Plus的多租户SQL改写
-
根据配置决定哪些表需要忽略租户条件
public class TenantDatabaseInterceptor implements TenantLineHandler {private final Set<String> ignoreTables = new HashSet<>();@Overridepublic Expression getTenantId() {return new LongValue(TenantContextHolder.getRequiredTenantId());}@Overridepublic boolean ignoreTable(String tableName) {return TenantContextHolder.isIgnore() || ignoreTables.contains(SqlParserUtils.removeWrapperSymbol(tableName));}
}
阶段五:响应返回
-
响应通过过滤器链反向返回
-
TenantContextWebFilter
的finally
块清理租户上下文
完整请求处理流程(以/users/getById为例)
阶段一:过滤器初始化
-
YudaoTenantAutoConfiguration 被Spring加载
-
创建
TenantLineInnerInterceptor
并注册到MyBatis -
注册
TenantContextWebFilter
到Servlet容器
-
阶段二:请求处理
-
TenantContextWebFilter
-
从请求头提取
tenant-id
-
设置到
TenantContextHolder
-
System.out.println("+++++tenant-id:"+request.getHeader("tenant-id")+"+++++");
-
-
Spring Security 过滤器链
-
执行基础安全处理(认证、会话管理等)
-
-
TenantSecurityWebFilter
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {// 1. 检查登录用户权限// 2. 校验租户ID是否存在(非忽略URL)// 3. 通过tenantFrameworkService校验租户状态 }
-
Controller处理
@GetMapping("/getById") public User getById(@RequestParam Long id) {return userService.getById(id); }
-
MyBatis Plus拦截器
-
TenantLineInnerInterceptor
拦截SQL执行 -
调用
TenantDatabaseInterceptor
:-
检查
ignoreTable()
决定是否添加租户条件 -
如需添加,调用
getTenantId()
获取当前租户ID
-
-
-
响应返回
-
过滤器链反向执行
-
TenantContextWebFilter
清理线程局部变量
-
关键顺序解析
-
租户上下文设置必须最先执行
-
TenantContextWebFilter
设置了最高优先级(最早执行) -
确保后续过滤器能获取正确的租户信息
-
-
安全校验在上下文设置之后
-
TenantSecurityWebFilter
依赖已设置的租户上下文 -
在Spring Security基础过滤器之后执行
-
-
SQL拦截最后执行
-
MyBatis拦截器不是Servlet Filter
-
在Service层执行SQL时触发
-
HTTP Request│▼
TenantContextWebFilter (设置租户上下文)│▼
Spring Security Filters│▼
TenantSecurityWebFilter (租户权限校验)│▼
Controller│▼
Service ───┐│ │▼ ▼
Mapper → TenantLineInnerInterceptor (SQL改写)│▼
DB