You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring中自定义Filter无法获取UserDetails的问题求助

我明白你现在卡在哪了——你的TenantFilter总是抢在Spring Security的认证过滤器前面执行,导致SecurityContext还没被填充,根本拿不到用户的principal信息对吧?咱们来一步步把这个问题解决掉。

问题根源

你之前用FilterRegistrationBean注册过滤器的方式,本质是把TenantFilter放到了Spring Security过滤器链的外部。不管你设置多大的order值,外部过滤器都会优先于Spring Security的内置过滤器链执行,这就是为什么你在过滤器里拿不到认证信息,但控制器里却能拿到的原因——控制器是在整个过滤器链执行完之后才处理请求的。

解决方案:把过滤器整合到Spring Security链中

不要用FilterRegistrationBean注册你的租户过滤器,而是直接把它加到Spring Security的内置过滤器链里,指定它在认证完成之后执行。

步骤1:修改TenantFilter,去掉自动注册

先把@Component注解去掉,避免Spring自动把它注册成全局过滤器:

// 移除@Component注解,我们将通过Security配置手动注册
public class TenantFilter implements Filter {
    @Autowired
    private TenantStore tenantStore;
    @Autowired
    private UserService userService;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        User user = null;
        
        // 增加空指针判断,避免NPE
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.getPrincipal() instanceof User) {
            user = (User) auth.getPrincipal();
        }

        String tenantId = user != null ? user.getSchool().getCode() : "";
        try {
            this.tenantStore.setTenantId(tenantId);
            chain.doFilter(servletRequest, servletResponse);
        } finally {
            // 必须清理ThreadLocal,避免线程复用导致的租户信息污染
            this.tenantStore.clear();
        }
    }

    @Override
    public void destroy() {}
}

步骤2:修改Spring Security配置,添加租户过滤器

创建或修改你的Security配置类,用addFilterAfter方法把TenantFilter加到认证过滤器之后:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            // 这里放你的其他安全配置,比如授权规则、csrf设置等
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            // 关键:把TenantFilter放到用户名密码认证过滤器之后执行
            // 如果用的是JWT/OAuth2,换成对应的认证过滤器类,比如JwtAuthenticationTokenFilter
            .addFilterAfter(tenantFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    // 手动注册TenantFilter的Bean
    @Bean
    public TenantFilter tenantFilter() {
        return new TenantFilter();
    }

    // 你的其他Security相关Bean,比如UserDetailsService、PasswordEncoder等
}

步骤3:精简TenantFilterConfig

现在不需要FilterRegistrationBean了,删掉相关配置,只保留租户存储的Bean定义:

@Configuration
public class TenantFilterConfig {

    @Bean(destroyMethod = "destroy")
    public ThreadLocalTargetSource threadLocalTenantStore() {
        ThreadLocalTargetSource result = new ThreadLocalTargetSource();
        result.setTargetBeanName("tenantStore");
        return result;
    }

    @Primary
    @Bean(name = "proxiedThreadLocalTargetSource")
    public ProxyFactoryBean proxiedThreadLocalTargetSource(ThreadLocalTargetSource threadLocalTargetSource) {
        ProxyFactoryBean result = new ProxyFactoryBean();
        result.setTargetSource(threadLocalTargetSource);
        return result;
    }

    @Bean(name = "tenantStore")
    @Scope(scopeName = "prototype")
    public TenantStore tenantStore() {
        return new TenantStore();
    }
}

额外注意事项

  • 如果你的项目用的是JWT、OAuth2等非表单登录的认证方式,一定要把UsernamePasswordAuthenticationFilter.class换成对应的认证过滤器类,比如JwtAuthenticationTokenFilter或者OAuth2AuthenticationProcessingFilter,确保租户过滤器在认证完成后执行。
  • 始终在finally块中清理ThreadLocal里的租户信息,这能避免线程池复用导致的租户信息串用问题。

内容的提问来源于stack exchange,提问作者Jordan Mackie

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:35:50