How do I add a filter to spring-security based on a @Profile?

From the article about Spring Security architecture:

Spring Security is installed as a single Filter in the chain, and its concerete type is FilterChainProxy, for reasons that will become apparent soon. In a Spring Boot app the security filter is a @Bean in the ApplicationContext, and it is installed by default so that it is applied to every request.

There can be multiple filter chains all managed by Spring Security in the same top level FilterChainProxy and all unknown to the container. The Spring Security filter contains a list of filter chains, and dispatches a request to the first chain that matches it.

Note also that:

The fact that all filters internal to Spring Security are unknown to the container is important, especially in a Spring Boot application, where all @Beans of type Filter are registered automatically with the container by default. So if you want to add a custom filter to the security chain, you need to either not make it a @Bean or wrap it in a FilterRegistrationBean that explicitly disables the container registration.

So, when you define a filter as a Spring bean, it is registered with the servlet container automatically, but not with the Spring Security filter chain. That is why you need to add it to the Spring Security chain explicitly using addFilter method. You also need to disable auto-registration in the servlet container or the filter will be called twice.

See also:

  • 75.3 Add a Servlet, Filter, or Listener to an Application
  • What is implication of adding @Component to custom Spring Security filter

As for profiles there are at least two ways to do what you need:

  1. Extend AbstractHttpConfigurer and move common security configuration there. After that create a separate security configuration for each profile:

    @Configuration
    @EnableWebSecurity(debug = true)
    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
    public class SecurityConfiguration {
    
        /**
         * Development security configuration.
         */
        @Profile("dev")
        @Configuration
        public static class DevSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
            @Bean
            public FilterRegistrationBean userIdAuthenticationFilter() {
                // ...
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.apply(commonSecurityConfiguration())
                        .and().addFilter(userIdAuthenticationFilter().getFilter());
            }
        }
    
        /**
         * Production security configuration.
         */
        @Profile("!dev")
        @Order(1)
        @Configuration
        public static class ProdSecurityConfiguration extends WebSecurityConfigurerAdapter {
    
            @Bean
            public FilterRegistrationBean jwtAuthenticationFilter() {
                // ...
            }
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.apply(commonSecurityConfiguration())
                        .and().addFilter(jwtAuthenticationFilter().getFilter());
            }
        }
    
    
        /**
         * Common security configuration reused by all profiles.
         */
        public static class CommonSecurityConfiguration
                extends AbstractHttpConfigurer<CommonSecurityConfiguration, HttpSecurity> {
    
            @Override
            public void init(HttpSecurity http) throws Exception {
                // Your basic configuration here:
                // http.cors().and()
                // ...
            }
    
            public static CommonSecurityConfiguration commonSecurityConfiguration() {
                return new CommonSecurityConfiguration();
            }
        }
    }
    

    See also the example in the documentation.

  2. Inject the Environment object and check the current profile:

    @Configuration
    @EnableWebSecurity(debug = true)
    @EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
    public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
        private final Environment environment;
    
        public SecurityConfiguration(Environment environment) {
            this.environment = environment;
        }
    
        @Profile("dev")
        @Bean
        public FilterRegistrationBean userIdAuthenticationFilter() {
            // ...
        }
    
        @Profile("!dev")
        @Bean
        public FilterRegistrationBean jwtAuthenticationFilter() {
            // ...
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // Your basic configuration here:
            // http.cors().and()
            // ...
    
            if (environment.acceptsProfiles("dev")) {
                http.addFilter(userIdAuthenticationFilter().getFilter());
            } else {
                http.addFilter(jwtAuthenticationFilter().getFilter());
            }
        }
    }
    

    Alternatively you can use application property instead of profile for this.