Spring Security多过滤器链:如何阻止非对应端点过滤器执行?
我有两组端点:/api/** 和 /ui/**,每组采用独立JWT认证(密钥不同),配置代码如下:
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class SecurityConfig { @Order(1) @Configuration public static class SecurityConfigApi { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private UnauthorizedHandler unauthorizedHandler; @Autowired private PasswordEncoder passwordEncoder; @Bean public AuthTokenFilterApi authenticationJwtTokenFilterApi() { return new AuthTokenFilterApi(); } @Bean public DaoAuthenticationProvider authenticationProviderApi() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder); return authProvider; } @Bean public AuthenticationManager authenticationManagerApi( AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChainApi(HttpSecurity http) throws Exception { http.antMatcher("/api/**") .cors() .and() .csrf() .disable() .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/api/**").authenticated(); http.addFilterBefore(authenticationJwtTokenFilterApi(), UsernamePasswordAuthenticationFilter.class); return http.build(); } } @Order(2) @Configuration public static class UiSecurityConfig { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private UnauthorizedHandler unauthorizedHandler; @Autowired private PasswordEncoder passwordEncoder; @Bean public AuthTokenFilter authenticationJwtTokenFilterUi() { return new AuthTokenFilter(); } @Bean public DaoAuthenticationProvider authenticationProviderUi() { DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); authProvider.setUserDetailsService(userDetailsService); authProvider.setPasswordEncoder(passwordEncoder); return authProvider; } @Bean public AuthenticationManager authenticationManagerUi( AuthenticationConfiguration authConfig) throws Exception { return authConfig.getAuthenticationManager(); } @Bean public SecurityFilterChain filterChainUi(HttpSecurity http) throws Exception { http.antMatcher("/ui/**") .cors() .and() .csrf() .disable() .exceptionHandling() .authenticationEntryPoint(unauthorizedHandler) .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/ui/auth/login") .permitAll() .antMatchers("/ui/auth/signup") .hasRole("ADMIN") .antMatchers("/ui/auth/refresh-token") .hasAnyRole(); http.addFilterBefore(authenticationJwtTokenFilterUi(), UsernamePasswordAuthenticationFilter.class); return http.build(); } } }
过滤器示例(另一过滤器逻辑类似):
public class AuthTokenFilterApi extends OncePerRequestFilter { private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilterApi.class); @Autowired private JwtUtilsApi jwtUtils; @Autowired private CustomUserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = jwtUtils.getJwtFromHttpRequest(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String username = jwtUtils.getUserNameFromJwtToken(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { logger.error("Cannot set user authentication.", e); } filterChain.doFilter(request, response); } }
目前认证功能正常,但发现认证完成后,另一组端点的过滤器仍会执行。请问这是Spring Security的正常行为吗?有没有办法阻止这种情况?我原以为不同的安全上下文不会出现这种情况,但查看FilterChain时总能看到两个过滤器。
这不是Spring Security的正常行为,问题根源在这里
你定义的AuthTokenFilterApi和AuthTokenFilter被Spring Boot自动注册成了全局Servlet过滤器,而不是仅属于对应的SecurityFilterChain。也就是说,不管请求匹配哪个SecurityFilterChain,这两个全局过滤器都会被执行一遍,这就是你看到的现象。
Spring Boot会自动把所有实现Filter接口的Bean注册为全局过滤器,哪怕你已经在SecurityFilterChain里添加过它们。
解决方法有三种,按需选择:
1. 取消过滤器的@Bean定义,局部创建
把配置类里的过滤器@Bean删掉,直接在对应的filterChain方法里new过滤器实例。这样过滤器不会被注册为全局Bean,只会属于当前SecurityFilterChain。
修改SecurityConfigApi里的filterChainApi方法:
@Bean public SecurityFilterChain filterChainApi(HttpSecurity http) throws Exception { // ... 其他配置不变 // 直接new过滤器,不用@Bean注入 http.addFilterBefore(new AuthTokenFilterApi(), UsernamePasswordAuthenticationFilter.class); return http.build(); }
同理修改UiSecurityConfig里的filterChainUi方法。
2. 保留@Bean,但阻止全局注册
如果需要保留过滤器的Bean身份(比如要注入其他依赖),可以用FilterRegistrationBean来控制,禁止它们被全局注册:
在每个配置类里添加:
@Bean public FilterRegistrationBean<AuthTokenFilterApi> disableAuthTokenFilterApi() { FilterRegistrationBean<AuthTokenFilterApi> registration = new FilterRegistrationBean<>(authenticationJwtTokenFilterApi()); registration.setEnabled(false); return registration; }
UiSecurityConfig里同理添加对应的FilterRegistrationBean来禁用全局注册。
3. 在过滤器内部判断请求路径,非目标路径直接跳过
在过滤器的doFilterInternal方法开头,先判断请求路径是否属于当前过滤器的处理范围,不符合就直接放行,不执行后续逻辑:
比如在AuthTokenFilterApi里添加:
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 只处理/api/**开头的请求,其他直接跳过 if (!request.getRequestURI().startsWith("/api/")) { filterChain.doFilter(request, response); return; } // ... 原有的认证逻辑 }
AuthTokenFilter里同理添加判断,只处理/ui/**的请求。
内容的提问来源于stack exchange,提问作者Ventrue

