This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Security 6.1.12!

Authorize HttpServletRequests with AuthorizationFilter

This section builds on Servlet Architecture and Implementation by digging deeper into how authorization works within Servlet-based applications.

AuthorizationFilter supersedes FilterSecurityInterceptor. To remain backward compatible, FilterSecurityInterceptor remains the default. This section discusses how AuthorizationFilter works and how to override the default configuration.

The AuthorizationFilter provides authorization for HttpServletRequests. It is inserted into the FilterChainProxy as one of the Security Filters.

You can override the default when you declare a SecurityFilterChain. Instead of using authorizeRequests, use authorizeHttpRequests, like so:

Use authorizeHttpRequests
  • Java

@Bean
SecurityFilterChain web(HttpSecurity http) throws AuthenticationException {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().authenticated();
        )
        // ...

    return http.build();
}

This improves on authorizeRequests in a number of ways:

  1. Uses the simplified AuthorizationManager API instead of metadata sources, config attributes, decision managers, and voters. This simplifies reuse and customization.

  2. Delays Authentication lookup. Instead of the authentication needing to be looked up for every request, it will only look it up in requests where an authorization decision requires authentication.

  3. Bean-based configuration support.

When authorizeHttpRequests is used instead of authorizeRequests, then AuthorizationFilter is used instead of FilterSecurityInterceptor.

authorizationfilter
Figure 1. Authorize HttpServletRequest
  • number 1 First, the AuthorizationFilter obtains an Authentication from the SecurityContextHolder. It wraps this in an Supplier in order to delay lookup.

  • number 2 Second, it passes the Supplier<Authentication> and the HttpServletRequest to the AuthorizationManager.

    • number 3 If authorization is denied, an AccessDeniedException is thrown. In this case the ExceptionTranslationFilter handles the AccessDeniedException.

    • number 4 If access is granted, AuthorizationFilter continues with the FilterChain which allows the application to process normally.

We can configure Spring Security to have different rules by adding more rules in order of precedence.

Authorize Requests
  • Java

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
	http
		// ...
		.authorizeHttpRequests(authorize -> authorize                                  (1)
			.requestMatchers("/resources/**", "/signup", "/about").permitAll()         (2)
			.requestMatchers("/admin/**").hasRole("ADMIN")                             (3)
			.requestMatchers("/db/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') and hasRole('DBA')"))   (4)
			// .requestMatchers("/db/**").access(AuthorizationManagers.allOf(AuthorityAuthorizationManager.hasRole("ADMIN"), AuthorityAuthorizationManager.hasRole("DBA")))   (5)
			.anyRequest().denyAll()                                                (6)
		);

	return http.build();
}
1 There are multiple authorization rules specified. Each rule is considered in the order they were declared.
2 We specified multiple URL patterns that any user can access. Specifically, any user can access a request if the URL starts with "/resources/", equals "/signup", or equals "/about".
3 Any URL that starts with "/admin/" will be restricted to users who have the role "ROLE_ADMIN". You will notice that since we are invoking the hasRole method we do not need to specify the "ROLE_" prefix.
4 Any URL that starts with "/db/" requires the user to have both "ROLE_ADMIN" and "ROLE_DBA". You will notice that since we are using the hasRole expression we do not need to specify the "ROLE_" prefix.
5 The same rule from 4, could be written by combining multiple AuthorizationManager.
6 Any URL that has not already been matched on is denied access. This is a good strategy if you do not want to accidentally forget to update your authorization rules.

You can take a bean-based approach by constructing your own RequestMatcherDelegatingAuthorizationManager like so:

Configure RequestMatcherDelegatingAuthorizationManager
  • Java

@Bean
SecurityFilterChain web(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
        throws AuthenticationException {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().access(access)
        )
        // ...

    return http.build();
}

@Bean
AuthorizationManager<RequestAuthorizationContext> requestMatcherAuthorizationManager(HandlerMappingIntrospector introspector) {
    MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
    RequestMatcher permitAll =
            new AndRequestMatcher(
                    mvcMatcherBuilder.pattern("/resources/**"),
                    mvcMatcherBuilder.pattern("/signup"),
                    mvcMatcherBuilder.pattern("/about"));
    RequestMatcher admin = mvcMatcherBuilder.pattern("/admin/**");
    RequestMatcher db = mvcMatcherBuilder.pattern("/db/**");
    RequestMatcher any = AnyRequestMatcher.INSTANCE;
    AuthorizationManager<HttpServletRequest> manager = RequestMatcherDelegatingAuthorizationManager.builder()
            .add(permitAll, (context) -> new AuthorizationDecision(true))
            .add(admin, AuthorityAuthorizationManager.hasRole("ADMIN"))
            .add(db, AuthorityAuthorizationManager.hasRole("DBA"))
            .add(any, new AuthenticatedAuthorizationManager())
            .build();
    return (context) -> manager.check(context.getRequest());
}

You can also wire your own custom authorization managers for any request matcher.

Here is an example of mapping a custom authorization manager to the my/authorized/endpoint:

Custom Authorization Manager
  • Java

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .requestMatchers("/my/authorized/endpoint").access(new CustomAuthorizationManager());
        )
        // ...

    return http.build();
}

Or you can provide it for all requests as seen below:

Custom Authorization Manager for All Requests
  • Java

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().access(new CustomAuthorizationManager());
        )
        // ...

    return http.build();
}

By default, the AuthorizationFilter does not apply to DispatcherType.ERROR and DispatcherType.ASYNC. We can configure Spring Security to apply the authorization rules to all dispatcher types by using the shouldFilterAllDispatcherTypes method:

Set shouldFilterAllDispatcherTypes to true
  • Java

  • Kotlin

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .shouldFilterAllDispatcherTypes(true)
            .anyRequest.authenticated()
        )
        // ...

    return http.build();
}
@Bean
open fun web(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            shouldFilterAllDispatcherTypes = true
            authorize(anyRequest, authenticated)
        }
    }
    return http.build()
}

Now with the authorization rules applying to all dispatcher types, you have more control of the authorization on them. For example, you may want to configure shouldFilterAllDispatcherTypes to true but not apply authorization on requests with dispatcher type ASYNC or FORWARD.

Permit ASYNC and FORWARD dispatcher type
  • Java

  • Kotlin

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .shouldFilterAllDispatcherTypes(true)
            .dispatcherTypeMatchers(DispatcherType.ASYNC, DispatcherType.FORWARD).permitAll()
            .anyRequest().authenticated()
        )
        // ...

    return http.build();
}
@Bean
open fun web(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            shouldFilterAllDispatcherTypes = true
            authorize(DispatcherTypeRequestMatcher(DispatcherType.ASYNC, DispatcherType.FORWARD), permitAll)
            authorize(anyRequest, authenticated)
        }
    }
    return http.build()
}

You can also customize it to require a specific role for a dispatcher type:

Require ADMIN for Dispatcher Type ERROR
  • Java

  • Kotlin

@Bean
SecurityFilterChain web(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .shouldFilterAllDispatcherTypes(true)
            .dispatcherTypeMatchers(DispatcherType.ERROR).hasRole("ADMIN")
            .anyRequest().authenticated()
        )
        // ...

    return http.build();
}
@Bean
open fun web(http: HttpSecurity): SecurityFilterChain {
    http {
        authorizeHttpRequests {
            shouldFilterAllDispatcherTypes = true
            authorize(DispatcherTypeRequestMatcher(DispatcherType.ERROR), hasRole("ADMIN"))
            authorize(anyRequest, authenticated)
        }
    }
    return http.build()
}

Request Matchers

The RequestMatcher interface is used to determine if a request matches a given rule. We use securityMatchers to determine if a given HttpSecurity should be applied to a given request. The same way, we can use requestMatchers to determine the authorization rules that we should apply to a given request. Look at the following example:

  • Java

  • Kotlin

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.securityMatcher("/api/**")                            (1)
			.authorizeHttpRequests(authorize -> authorize
				.requestMatchers("/user/**").hasRole("USER")       (2)
				.requestMatchers("/admin/**").hasRole("ADMIN")     (3)
				.anyRequest().authenticated()                      (4)
			)
			.formLogin(withDefaults());
		return http.build();
	}
}
@Configuration
@EnableWebSecurity
open class SecurityConfig {

    @Bean
    open fun web(http: HttpSecurity): SecurityFilterChain {
        http {
            securityMatcher("/api/**")                                           (1)
            authorizeHttpRequests {
                authorize("/user/**", hasRole("USER"))                           (2)
                authorize("/admin/**", hasRole("ADMIN"))                         (3)
                authorize(anyRequest, authenticated)                             (4)
            }
        }
        return http.build()
    }

}
1 Configure HttpSecurity to only be applied to URLs that start with /api/
2 Allow access to URLs that start with /user/ to users with the USER role
3 Allow access to URLs that start with /admin/ to users with the ADMIN role
4 Any other request that doesn’t match the rules above, will require authentication

The securityMatcher(s) and requestMatcher(s) methods will decide which RequestMatcher implementation fits best for your application: If Spring MVC is in the classpath, then MvcRequestMatcher will be used, otherwise, AntPathRequestMatcher will be used. You can read more about the Spring MVC integration here.

If you want to use a specific RequestMatcher, just pass an implementation to the securityMatcher and/or requestMatcher methods:

  • Java

  • Kotlin

import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; (1)
import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			.securityMatcher(antMatcher("/api/**"))                              (2)
			.authorizeHttpRequests(authorize -> authorize
				.requestMatchers(antMatcher("/user/**")).hasRole("USER")         (3)
				.requestMatchers(regexMatcher("/admin/.*")).hasRole("ADMIN")     (4)
				.requestMatchers(new MyCustomRequestMatcher()).hasRole("SUPERVISOR")     (5)
				.anyRequest().authenticated()
			)
			.formLogin(withDefaults());
		return http.build();
	}
}

public class MyCustomRequestMatcher implements RequestMatcher {

    @Override
    public boolean matches(HttpServletRequest request) {
        // ...
    }
}
import org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher (1)
import org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher

@Configuration
@EnableWebSecurity
open class SecurityConfig {

    @Bean
    open fun web(http: HttpSecurity): SecurityFilterChain {
        http {
            securityMatcher(antMatcher("/api/**"))                               (2)
            authorizeHttpRequests {
                authorize(antMatcher("/user/**"), hasRole("USER"))               (3)
                authorize(regexMatcher("/admin/**"), hasRole("ADMIN"))           (4)
                authorize(MyCustomRequestMatcher(), hasRole("SUPERVISOR"))       (5)
                authorize(anyRequest, authenticated)
            }
        }
        return http.build()
    }

}
1 Import the static factory methods from AntPathRequestMatcher and RegexRequestMatcher to create RequestMatcher instances.
2 Configure HttpSecurity to only be applied to URLs that start with /api/, using AntPathRequestMatcher
3 Allow access to URLs that start with /user/ to users with the USER role, using AntPathRequestMatcher
4 Allow access to URLs that start with /admin/ to users with the ADMIN role, using RegexRequestMatcher
5 Allow access to URLs that match the MyCustomRequestMatcher to users with the SUPERVISOR role, using a custom RequestMatcher

Expressions

It is recommended that you use type-safe authorization managers instead of SpEL. However, WebExpressionAuthorizationManager is available to help migrate legacy SpEL.

To use WebExpressionAuthorizationManager, you can construct one with the expression you are trying to migrate, like so:

  • Java

  • Kotlin

.requestMatchers("/test/**").access(new WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))
.requestMatchers("/test/**").access(WebExpressionAuthorizationManager("hasRole('ADMIN') && hasRole('USER')"))

If you are referring to a bean in your expression like so: @webSecurity.check(authentication, request), it’s recommended that you instead call the bean directly, which will look something like the following:

  • Java

  • Kotlin

.requestMatchers("/test/**").access((authentication, context) ->
    new AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))
.requestMatchers("/test/**").access((authentication, context): AuthorizationManager<RequestAuthorizationContext> ->
    AuthorizationDecision(webSecurity.check(authentication.get(), context.getRequest())))

For complex instructions that include bean references as well as other expressions, it is recommended that you change those to implement AuthorizationManager and refer to them by calling .access(AuthorizationManager).

If you are not able to do that, you can configure a DefaultHttpSecurityExpressionHandler with a bean resolver and supply that to WebExpressionAuthorizationManager#setExpressionhandler.