如何让UserDetailsService获取过滤器包装后的HttpServletRequest?
解决自定义UserDetailsService获取包装后HttpServletRequest的问题
我之前也踩过一模一样的坑,核心问题其实是过滤器执行顺序和请求上下文传递的问题,下面一步步给你讲清楚怎么搞定:
为什么注入的是原生Request?
默认情况下,Spring Security的过滤器链会优先于我们自定义的过滤器执行(如果没手动调整顺序的话)。而UserDetailsService是在Spring Security的认证流程中被调用的,这时候你的包装过滤器还没来得及处理请求,自然拿到的是原生对象。另外,如果过滤器没有正确把包装后的请求传递给后续的过滤器链,也会导致这个问题。
解决方案步骤
1. 实现自定义HttpServletRequestWrapper
先写一个包装类,完成子域名解析并重写getHeader()方法:
public class SubdomainAwareRequestWrapper extends HttpServletRequestWrapper { private final String subdomain; public SubdomainAwareRequestWrapper(HttpServletRequest request) { super(request); // 这里写你的子域名解析逻辑,比如从ServerName提取 String serverName = request.getServerName(); this.subdomain = extractSubdomain(serverName); } private String extractSubdomain(String serverName) { if (serverName == null || !serverName.contains(".")) { return ""; } String[] parts = serverName.split("\\."); // 假设域名格式是 sub.domain.com,取第一个部分作为子域名 return parts.length > 2 ? parts[0] : ""; } @Override public String getHeader(String name) { // 自定义头返回子域名,也可以根据需求增强原有头的逻辑 if ("X-Custom-Subdomain".equals(name)) { return subdomain; } return super.getHeader(name); } // 如果需要,还可以重写getHeaders()、getParameter()等方法 }
2. 编写自定义过滤器并传递包装后的请求
这个过滤器的核心是把包装后的请求传递给后续的过滤器链,确保整个请求上下文里都是包装后的对象:
@Component public class SubdomainFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 包装原始请求 SubdomainAwareRequestWrapper wrappedRequest = new SubdomainAwareRequestWrapper(request); // 关键:把包装后的请求传给下一个过滤器,不能传原始request! filterChain.doFilter(wrappedRequest, response); } }
3. 调整过滤器顺序,确保在Spring Security之前执行
这是最关键的一步!必须让你的包装过滤器在Spring Security的认证过滤器之前运行,这样当Security调用UserDetailsService时,请求已经被包装好了。
方式一:用FilterRegistrationBean配置顺序
@Configuration public class FilterConfig { @Bean public FilterRegistrationBean<SubdomainFilter> subdomainFilterRegistration(SubdomainFilter subdomainFilter) { FilterRegistrationBean<SubdomainFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(subdomainFilter); // 设置顺序比Spring Security默认过滤器小,确保先执行 registrationBean.setOrder(SecurityProperties.DEFAULT_FILTER_ORDER - 1); return registrationBean; } }
方式二:直接加到SecurityFilterChain中
如果你用的是Spring Security 5.4+的SecurityFilterChain配置,可以直接把过滤器加到Security链的最前面:
@Configuration @EnableWebSecurity public class SecurityConfig { private final SubdomainFilter subdomainFilter; // 构造注入自定义过滤器 public SecurityConfig(SubdomainFilter subdomainFilter) { this.subdomainFilter = subdomainFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 把自定义过滤器加到UsernamePasswordAuthenticationFilter之前(Security认证的核心过滤器) .addFilterBefore(subdomainFilter, UsernamePasswordAuthenticationFilter.class) // 你的其他Security配置,比如授权规则、登录方式等 .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) .formLogin(form -> form.permitAll()); return http.build(); } }
4. 在UserDetailsService中注入并使用包装后的Request
现在你直接@Autowired就能拿到包装后的请求了:
@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private HttpServletRequest request; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 测试获取自定义头,应该能拿到解析后的子域名 String subdomain = request.getHeader("X-Custom-Subdomain"); // 这里可以根据子域名和用户名查询对应的用户信息 // 比如从不同的租户数据源中获取用户 return User.withUsername(username) .password("{noop}your-password") .roles("USER") .build(); } }
额外注意事项
- 如果你的应用有异步请求,需要配置
RequestContextHolder的请求属性传播,比如在异步方法上加上@Async的同时,配置TaskExecutor来继承请求上下文。 - 测试的时候可以在过滤器里打个日志,确认请求确实被包装后再进入Security链。
内容的提问来源于stack exchange,提问作者Tinus Tate
相关产品推荐
相关产品推荐

