javaspring-bootspring-security

Update spring-boot 3.4.1 Spring Security Error: A filter chain that matches any request has already been configured


I have two security configurations in two libs

First one is for authentication:

    @Bean
    @Order(10)
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers(createAntRequestMatchers(whitelist))
                                .permitAll().anyRequest()
                                .authenticated()
                )
                .oauth2ResourceServer( ...)
        return http.build();
    }

Second one adds some resource filter:

    @Bean
    @Order(100)
    public SecurityFilterChain filterChain(HttpSecurity http, ResourceFilter resourceFilter) throws Exception {
        return      http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers(createAntRequestMatchers(whitelist))
                                .permitAll().anyRequest()
                                .authenticated()
                ).addFilterAfter(resourceFilter, SessionManagementFilter.class).build();
    }   

It worked perfect until spring-boot 3.3.? After update to spring-boot 3.4.1 spring context don't startet anymore with error message

A filter chain that matches any request [DefaultSecurityFilterChain defined as 'filterChain' in ... has already been configured, which means that this filter chain ... will never get invoked. Please use HttpSecurity#securityMatcher to ensure that there is only one filter chain configured for 'any request' and that the 'any request' filter chain is published last.

After I add in each configuration requestMatcher (all requests)

http.securityMatcher("/**").authorizeHttpRequests(...

it works as expected. But if I read spring-security issue comments https://github.com/spring-projects/spring-security/issues/15220 I have a doubts about my solution.

What do you mean?

I adapt my code acording @Roar S. suggestion

    @Bean
    @Order(10)
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.securityMatcher("/**")
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .requestMatchers(createAntRequestMatchers(whitelist))
                                .permitAll().anyRequest()
                                .authenticated()
                )
                .oauth2ResourceServer( ...)
        return http.build();
    }

---------

    @Bean
    @Order(100)
    public SecurityFilterChain filterChain(HttpSecurity http, ResourceFilter resourceFilter) throws Exception {
        return http.securityMatcher("/**")
        .addFilterAfter(resourceFilter, SessionManagementFilter.class).build();
    }   


It works, but .securityMatcher("/**") looks suspicious. And without .securityMatcher("/**") it doesn't start


Solution

  • Update: OP mentioned in a comment that the first SecurityFilterChain is shared across multiple applications and cannot be modified. Since the issue involves simply adding a filter that needs to execute after the shared SecurityFilterChain, we can address it using FilterRegistrationBean instead of using two security chains. The following code is based on this answer.

    LoggingFilter is the same as in my original answer.

    import org.springframework.boot.autoconfigure.security.SecurityProperties;
    import org.springframework.boot.web.servlet.FilterRegistrationBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class FilterConfig {
    
        @Bean
        public FilterRegistrationBean<LoggingFilter> afterAuthFilterRegistrationBean(
                SecurityProperties securityProperties) {
            
            var filterRegistrationBean = new FilterRegistrationBean<LoggingFilter>();
    
            // a filter that extends OncePerRequestFilter
            filterRegistrationBean.setFilter(new LoggingFilter());
    
            // this needs to be a number greater than than spring.security.filter.order
            filterRegistrationBean.setOrder(securityProperties.getFilter().getOrder() + 1);
            return filterRegistrationBean;
        }
    }
    

    Original answer

    OP has separated security configuration into two chains under the assumption, I believe, that the principal becomes available only after a security chain is fully executed. However, the principal is populated and available after the BearerTokenAuthenticationFilter has completed. Therefore, the two chains in the question can be merged into one.

    This behavior can be verified by adding the following logging filter to the chain with:

    .addFilterAfter(new LoggingFilter(), BearerTokenAuthenticationFilter.class)
    

    Here is the logging filter implementation:

        private static class LoggingFilter extends OncePerRequestFilter {
    
            @Override
            protected void doFilterInternal(@NonNull HttpServletRequest request,
                                            @NonNull HttpServletResponse response,
                                            @NonNull FilterChain filterChain) throws ServletException, IOException {
    
                var authentication = SecurityContextHolder.getContext().getAuthentication();
                if (authentication != null) {
                    LOG.info("Logged in as: {}", authentication.getName());
                    LOG.info("Authorities: {}",
                            authentication.getAuthorities().stream()
                                    .map(GrantedAuthority::getAuthority)
                                    .collect(Collectors.joining(", "))
                    );
                } else {
                    LOG.info("No user");
                }
    
                filterChain.doFilter(request, response);
            }
        }