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!
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.
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.
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:
As for profiles there are at least two ways to do what you need:
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With