Spring Security表单认证后RequestContextHolder.getRequestAttributes()返回空问题
我之前也碰到过一模一样的问题——搭完Spring Security基础表单认证后,RequestContextHolder.getRequestAttributes()突然返回null,想从登录请求里拿租户ID切换数据库都卡壳了。别急,咱们一步步拆解解决:
为什么RequestContextHolder会丢请求属性?核心原因是Spring Security的表单认证流程中,部分认证逻辑可能脱离了原始请求的线程上下文绑定——比如默认过滤器链处理认证时,线程切换导致请求上下文没被正确传递,或者自定义认证逻辑没维护好上下文。
1. 先把租户ID从登录请求里捞出来存好
既然RequestContextHolder靠不住,咱们用ThreadLocal来临时存登录时的租户ID,这玩意儿在当前线程里绝对不会丢。先整个工具类:
public class TenantContextHolder { private static final ThreadLocal<String> tenantIdHolder = new ThreadLocal<>(); public static void setTenantId(String tenantId) { tenantIdHolder.set(tenantId); } public static String getTenantId() { return tenantIdHolder.get(); } public static void clear() { tenantIdHolder.remove(); } }
然后自定义一个认证过滤器,替换Spring Security默认的UsernamePasswordAuthenticationFilter,在认证前先把租户ID从请求里拿出来存到TenantContextHolder:
public class TenantAwareAuthenticationFilter extends UsernamePasswordAuthenticationFilter { @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 从登录表单参数里捞租户ID(如果是放请求头就改request.getHeader("tenantId")) String tenantId = request.getParameter("tenantId"); TenantContextHolder.setTenantId(tenantId); // 继续走默认的账号密码认证逻辑 return super.attemptAuthentication(request, response); } }
接下来在Security配置里替换掉默认过滤器:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .formLogin(form -> form .loginProcessingUrl("/login") .usernameParameter("username") .passwordParameter("password") .successHandler(customSuccessHandler()) .failureHandler(customFailureHandler()) ) // 替换默认的认证过滤器 .addFilterAt(tenantAwareAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public TenantAwareAuthenticationFilter tenantAwareAuthenticationFilter() { TenantAwareAuthenticationFilter filter = new TenantAwareAuthenticationFilter(); filter.setAuthenticationManager(authenticationManagerBean().getObject()); return filter; } // 其他配置... }
2. 改造你的CurrentTenantIdentifierResolverImpl
现在让租户解析器优先从TenantContextHolder拿ID,实在不行再fallback到RequestContextHolder(非登录场景用):
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver { @Override public String resolveCurrentTenantIdentifier() { // 先拿登录时存在ThreadLocal里的租户ID String tenantId = TenantContextHolder.getTenantId(); if (tenantId != null) { return tenantId; } // 非登录请求,尝试从RequestContextHolder拿(可选,根据你的业务场景调整) RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); if (requestAttributes != null) { HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest(); tenantId = request.getParameter("tenantId"); if (tenantId != null) { return tenantId; } } // 给个默认租户ID兜底 return "default"; } @Override public boolean validateExistingCurrentSessions() { return true; } }
3. 别忘了清理ThreadLocal,避免内存泄漏
认证不管成功失败,都要把ThreadLocal里的租户ID清掉,不然线程复用的时候会串数据:
// 认证成功处理器 public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 可选:把租户ID存到SecurityContext里,后续请求直接从这里拿 SecurityContextHolder.getContext().setAuthentication(new TenantAwareAuth(authentication, TenantContextHolder.getTenantId())); // 清理ThreadLocal TenantContextHolder.clear(); // 跳转或返回响应 response.sendRedirect("/dashboard"); } } // 认证失败处理器 public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // 失败也要清 TenantContextHolder.clear(); // 处理失败逻辑 response.sendRedirect("/login?error"); } } // 自定义Authentication,用来携带租户ID class TenantAwareAuth extends UsernamePasswordAuthenticationToken { private final String tenantId; public TenantAwareAuth(Authentication auth, String tenantId) { super(auth.getPrincipal(), auth.getCredentials(), auth.getAuthorities()); this.tenantId = tenantId; } public String getTenantId() { return tenantId; } }
额外提醒
如果你的应用用到了异步处理,要确保ThreadLocal的租户ID能传递到子线程——可以给异步线程池配置上下文继承,或者在异步方法里手动传递。
这样改完,不仅解决了RequestContextHolder返回null的问题,还能稳稳拿到登录请求里的租户ID来切换数据库了。
内容的提问来源于stack exchange,提问作者guidev

