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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 17:45:46