Spring Security – Filter chains and request authorization

The important components

WebSecurityConfigurer instances

When we enable Spring Security in a Spring application, we benefit automatically from one WebSecurityConfigurer instance or multiple of them if we included other spring dependencies that require them such as oauth2 deps
Each WebSecurityConfigurer instance defines ,among other things, the request authorization rules and a security filter chain.
And each security filter chain is composed of a list of filters such as BasicAuthenticationFilter, AnonymousAuthenticationFilter, SessionManagementFilter, FilterSecurityInterceptor.
Some of these filters are added by default (provided by WebSecurityConfigurerAdapter for example) and others are added explicitly or implicitly.

WebSecurityConfigurer instance are ordered in a specific order. As well as, in the frame of each WebSecurityConfigurerAdapter instance : filters of the chain and request authorization rules are also ordered in a specific order.
That matters, we will see why later.

Request authorization declarations

The request authorizations are configured in the WebSecurityConfigurerAdapter.configure(HttpSecurity http) method that we override. For example :

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
      http.antMatcher("/**")
          .authorizeRequests().antMatchers("/hello.html", "/login")
          .permitAll()
       // ...
   }
    // ...
 }

Filters declarations

Default filters

For example, when the WebSecurityConfigurerAdapter constructor is invoked, an HttpSecurity object is instantiated with some default filters :

protected final HttpSecurity getHttp() throws Exception {
	if (http != null) {
		return http;
	}
 
	DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
			.postProcess(new DefaultAuthenticationEventPublisher());
	localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
 
	AuthenticationManager authenticationManager = authenticationManager();
	authenticationBuilder.parentAuthenticationManager(authenticationManager);
	authenticationBuilder.authenticationEventPublisher(eventPublisher);
	Map<Class<?>, Object> sharedObjects = createSharedObjects();
 
	http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
			sharedObjects);
	if (!disableDefaults) {
		// @formatter:off
		http
			.csrf().and()
			.addFilter(new WebAsyncManagerIntegrationFilter())
			.exceptionHandling().and()
			.headers().and()
			.sessionManagement().and()
			.securityContext().and()
			.requestCache().and()
			.anonymous().and()
			.servletApi().and()
			.apply(new DefaultLoginPageConfigurer<>()).and()
			.logout();
		// @formatter:on
		ClassLoader classLoader = this.context.getClassLoader();
		List<AbstractHttpConfigurer> defaultHttpConfigurers =
				SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
 
		for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
			http.apply(configurer);
		}
	}
//...
}

Implicit filters

Invoking some HttpSecurity instance methods creates under the hood some filters. For example HttpSecurity.anonymous() applies a AnonymousConfigurer instance that adds a AnonymousAuthenticationFilter instance.

public AnonymousConfigurer<HttpSecurity> anonymous() throws Exception {
		return getOrApply(new AnonymousConfigurer<>());
}

Explicit filters

Some filters can be explicitly added in the chain and in a specific order. 
For example, we can add a SpnegoAuthenticationProcessingFilter instance after BasicAuthenticationFilter such as :

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.addFilterBefore(new SpnegoAuthenticationProcessingFilter(),
						BasicAuthenticationFilter.class);
                //..
	}

So how all these things work ?

Matching the incoming request to a WebSecurityConfigurerAdapter instance

When a request is received by the web application, Spring Security tries to find which WebSecurityConfigurerAdapter instance will be used to process the request.
Spring Security starts with the first (whereas the order notion) WebSecurityConfigurerAdapter instance.
It tries to do a match between the incoming request and the request authorization rules of that instance. It tries rules, one after the other, in the order in which these are declared.
If no rule matches with the request, Spring Security goes on with the next WebSecurityConfigurerAdapter instance.
As soon as a rules matches with the request, the WebSecurityConfigurerAdapter instance is selected to address the request. That has as consequence that filters of the chain associated to the matched WebSecurityConfigurerAdapter instance are invoked and it also means that next WebSecurityConfigurerAdapter instances are never invoked to process that request.

Filter chain processing after request matching with a WebSecurityConfigurerAdapter instance

Filter are invoked, one after the other, according to their declaration or their default order.
Filters examine the request and according to its value, they enrich or don’t the current request or response object. After its logic, filters can either delegate to the next filter or terminate the filter chain process.

Concrete approaches for developpers

Several approaches are possible.
Some very used are :
– declaring a single WebSecurityConfigurerAdapter class and so define a monolithic request rules mapping and using same filters for any request
– declaring a WebSecurityConfigurerAdapter class by concerns and so define a request rules mapping and using specific filters by concerns.

The first way is suitable for very simple rules and filters.
The second way is suitable for cases where we notice that rules and filters depend on a specific concern and so we want to make things clear and flexible by separating them into distinct component.
That approaches has also the benefit from reducing checks in filters before processing it.

Monolithic declaration example

We will focus on configure(HttpSecurity http).

package com.example.demo;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
@Configuration
@EnableWebSecurity(debug = false)
public class WebSecurityConfigMonolithic extends WebSecurityConfigurerAdapter {
 
  @Autowired
  private BCryptPasswordEncoder passwordEncoder;
 
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("admin").password(passwordEncoder.encode("123")).roles("USER","ADMIN").and()
        .withUser("foouser").password(passwordEncoder.encode("123")).roles("USER");
  }
 
  @Override
  protected void configure(HttpSecurity http) throws Exception { // @formatter:off
    http.csrf().disable()
    .antMatcher("/**")
        .authorizeRequests().antMatchers("/public.html", "/login")
        .permitAll()
 
        .and()
        .formLogin().permitAll()
 
        .and()
        .authorizeRequests()
        .antMatchers("/admin/**","/h2-console/**").hasRole("ADMIN")
        .anyRequest().hasRole("USER");
  }
}

Explanations : http.antMatcher("/**") is very important.
That method allows configuring the HttpSecurity to only be invoked when matching the provided ant pattern. We can see that as the request scope of our WebSecurityConfig class.
By specifying /**, we require to use that config class for any incoming request.
The remaining is self explanatory :
– The url "/public.html", "/login" are allowed for anyone
– The formlogin feature (provided by Spring Security) is allowed for anyone too
– The url "/admin/**", "/h2-console/**" are allowed only for authenticated users that own the ADMIN role
– All other urls are allowed only for authenticated users that own the USER role

An important remark :
About the last rule : .anyRequest().hasRole("USER");, an important thing is that it doesn’t override previous declared rules. For example .authorizeRequests().antMatchers("/public.html", "/login").permitAll() that is declared before remaining valid. Why ?
Because as said previously, rules order matter since rules are executed one after the other and as soon as a rule matches, the others are not applied.
As a consequence, .anyRequest() has to be specified at the right place.
Invoking it and then defining an ant matcher makes no sense since we will never happen at that matcher because of the .anyRequest() catch-all declared before.

About that misuse, Spring detects that at startup.
Reverse the order of these rules such as :

        .and()
        .authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().hasRole("USER")
 
        .and()
        .authorizeRequests().antMatchers("/public.html", "/login", "/h2-console/**")
        .permitAll();

At the startup, Spring will fail to start and throws an exception such as :

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'springSecurityFilterChain' defined in class path resource [org/springframework/security/config/annotation/web/configuration/WebSecurityConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [javax.servlet.Filter]: Factory method 'springSecurityFilterChain' threw exception; nested exception is java.lang.IllegalStateException: Can't configure antMatchers after anyRequest
	at org.springframework.beans.factory.suppo

Multiple declarations example

As for the monolithic example, we will focus on configure(HttpSecurity http).
The following declaration brings exactly the same rules than in the previous case.
The single difference is that these are defined in two distinct WebSecurityConfig instances : an instance handles h2 console urls accesses while the other one handles all urls.

package com.example.demo;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 
@Configuration
@Order(10)
@EnableWebSecurity(debug = false)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
  @Autowired
  private BCryptPasswordEncoder passwordEncoder;
 
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication()
        .withUser("admin").password(passwordEncoder.encode("123")).roles("USER","ADMIN").and()
        .withUser("foouser").password(passwordEncoder .encode("123")).roles("USER");
  }
 
  @Configuration
  @Order(9)
  public static class WebSecurityConfigH2 extends WebSecurityConfigurerAdapter {
 
    @Override
    protected void configure(HttpSecurity http) throws Exception { // @formatter:off
      http.csrf().disable()
          .antMatcher("/h2-console/**")
          .authorizeRequests()
          .anyRequest().hasRole("ADMIN").
              and().headers().frameOptions().disable()
          .and()
          .formLogin().permitAll();
    }
  }
 
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/**")
        .authorizeRequests().antMatchers("/public.html", "/login").permitAll()
 
        .and()
        .formLogin().permitAll()
 
        .and()
        .authorizeRequests()
        .antMatchers("/admin/**").hasRole("ADMIN")
        .anyRequest().hasRole("USER");
  }
 
}

Important remarks :
– Each configuration class defines a distinct scope for request matching : the outer class catches any url : .antMatcher("/**") while the inner class handles only urls for h2-console prefix : .antMatcher("/h2-console/**").
That is an example of concern separations.
– for any request, we want that Spring security tests a matching with the WebSecurityConfigH2 instance before WebSecurityConfig because WebSecurityConfig matches to any urls. It means that any rule that accepts any request will make a match while we would like that some specific urls to be handled by the WebSecurityConfigH2 mapping.
So we have to define an higher precedence for WebSecurityConfigH2 than WebSecurityConfig.
We do it by annotating WebSecurityConfigH2 with @Order(9) and WebSecurityConfig with @Order(10) . The lower values have higher priorities.
.formLogin().permitAll(); has to be declared in each configuration if we want that the form be enabled and available whatever the WebSecurityConfig instance that has processed the request.

Ce contenu a été publié dans Non classé. Vous pouvez le mettre en favoris avec ce permalien.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *