Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I am attempting to define two different beans (both extending AbstractPreAuthenticatedProcessingFilter): one for grabbing a header off of the request (such as USER_ID) while a "development" profile is active, and a second to grab a JWT off of the request header while the "development" profile is not active. (Conceptually though, I'm really just trying to programmatically register filters based on the existence of the bean itself) At the moment, I'm not even trying to use profiles, since I'm having an issue with getting the headers to automatically be registered in the appropriate filter chain.

The app uses Spring-Boot 2.0.0.RELEASE, configured to use an embedded Tomcat, and the service is annotated as a @RestController. Below is a simplified version of my SecurityConfig class:

@Configuration
@EnableWebSecurity(debug=true)
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {


@Bean
public UserIdAuthenticationFilter UserIdAuthenticationFilter() throws Exception {
    ...
}

@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
    ...
}


@Override
protected void configure(HttpSecurity http) throws Exception {

    http
        .cors().and()
        .csrf().disable()
        .headers().frameOptions().sameOrigin()
        .and()
        //.addFilter(jwtAuthenticationFilter())
        //.addFilter(UserIdAuthenticationFilter())
        .exceptionHandling()
        .and().authorizeRequests()
        .antMatchers("/", "/index.html", "/css/**", "/images/**", "/js/**").permitAll()
        .anyRequest()
        .authenticated()
        .antMatchers("/**").permitAll()
    ;


 }

}

As you can see, I've defined my filter beans, and they work when appliced to the correct chain... The problem I'm seeing is that spring appears to register to the filters somewhere, but when I make a call to service endpoint, it never calls the filter code I added.

Log output from running the app that seemingly indicates that the filters are being located...

 2018-04-04 09:43:02.907  INFO 7717 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
 2018-04-04 09:43:02.907  INFO 7717 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
 2018-04-04 09:43:02.907  INFO 7717 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
 2018-04-04 09:43:02.908  INFO 7717 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
 2018-04-04 09:43:02.908  INFO 7717 --- [ost-startStop-1] .s.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
 2018-04-04 09:43:02.908  INFO 7717 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpTraceFilter' to: [/*]
 2018-04-04 09:43:02.908  INFO 7717 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'webMvcMetricsFilter' to: [/*]
 2018-04-04 09:43:02.908  INFO 7717 --- [ LOOK HERE ] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'userIdAuthenticationFilter' to: [/*]
 2018-04-04 09:43:02.908  INFO 7717 --- [ LOOK HERE ] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'jwtAuthenticationFilter' to: [/*]
 2018-04-04 09:43:02.909  INFO 7717 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
 2018-04-04 09:43:02.922 DEBUG 7717 --- [ost-startStop-1] g.n.a.f.w.c.a.f.JwtAuthenticationFilter  : Initializing filter 'jwtAuthenticationFilter'
 2018-04-04 09:43:02.922 DEBUG 7717 --- [ost-startStop-1] g.n.a.f.w.c.a.f.JwtAuthenticationFilter  : Filter 'jwtAuthenticationFilter' configured successfully
 2018-04-04 09:43:02.922 DEBUG 7717 --- [ost-startStop-1] c.a.f.UserIdAuthenticationFilter : Initializing filter 'userIdAuthenticationFilter'
 2018-04-04 09:43:02.922 DEBUG 7717 --- [ost-startStop-1] c.a.f.UserIdAuthenticationFilter : Filter 'userIdAuthenticationFilter' configured successfully

Now that the app is running, when I try to access the service I see spring dump the following output (with debug on):

 Security filter chain: [
   WebAsyncManagerIntegrationFilter
   SecurityContextPersistenceFilter
   HeaderWriterFilter
   CorsFilter
   LogoutFilter
   RequestCacheAwareFilter
   SecurityContextHolderAwareRequestFilter
   AnonymousAuthenticationFilter
   SessionManagementFilter
   ExceptionTranslationFilter
   FilterSecurityInterceptor
   ]

Given that output, it indicates to me that the security filter chain does not have my filters applied.

Perhaps more telling is that if you'll notice in my configuration code snippet there are two lines commented out (this is where I was manually adding the filter(s) before I tried to get fancy with the profile based detection). If I uncomment the line that adds the JWT filter (aka. manually registering the filter, rather than rely on detection), everything seems to work as intended. Looking at the debug output, I now see the following when calling the endpoint after manually adding the filter (notice that the JwtAuthenticationFilter is now present in the security filter chain):

 Security filter chain: [
     WebAsyncManagerIntegrationFilter
     SecurityContextPersistenceFilter
     HeaderWriterFilter
     CorsFilter
     LogoutFilter
     JwtAuthenticationFilter <-----
     RequestCacheAwareFilter
     SecurityContextHolderAwareRequestFilter
     AnonymousAuthenticationFilter
     SessionManagementFilter
     ExceptionTranslationFilter
     FilterSecurityInterceptor
   ]

There are really two questions I'm asking... I'm obviously not understanding the various filter chains that spring-boot/spring-security sets up, so what is the difference between the beans being registered during the application start up in the normal filter chain, vs the beans being registered with the spring-security filter chain? Can someone point out what I'm doing wrong and why?

What is the correct way to register these "optional" beans? (Optional by virtue of possibly not being there dependent on the active profile.)

Thanks!

like image 569
jholder Avatar asked Apr 04 '18 15:04

jholder


People also ask

How do you add filters to spring boot security?

There are a couple of possible methods: addFilterBefore(filter, class) adds a filter before the position of the specified filter class. addFilterAfter(filter, class) adds a filter after the position of the specified filter class. addFilterAt(filter, class) adds a filter at the location of the specified filter class.

Can you add custom filters in Spring Security filter chain?

Spring security provides few options to register the custom filter. We can use one of them based on our requirement. addFilterAfter(filter, class)–Adds a filter after the position of the specified filter class. addFilterBefore(filter, class)–Filter before the position of the specified filter class.


1 Answers

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.

like image 137
Eien Avatar answered Sep 29 '22 15:09

Eien