For the latest stable version, please use Spring Security 6.2.4!

Authorization Migrations

The following steps relate to changes around how authorization is performed.

Use AuthorizationManager for Method Security

Method Security has been simplified through the AuthorizationManager API and direct use of Spring AOP.

Should you run into trouble with making these changes, note that @EnableGlobalMethodSecurity, while deprecated, will not be removed in 6.0, allowing you to opt out by sticking with the old annotation.

Replace global method security with method security

@EnableGlobalMethodSecurity and <global-method-security> are deprecated in favor of @EnableMethodSecurity and <method-security>, respectively. The new annotation and XML element activate Spring’s pre-post annotations by default and use AuthorizationManager internally.

This means that the following two listings are functionally equivalent:

  • Java

  • Kotlin

  • Xml

@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableGlobalMethodSecurity(prePostEnabled = true)
<global-method-security pre-post-enabled="true"/>

and:

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity
@EnableMethodSecurity
<method-security/>

For applications not using the pre-post annotations, make sure to turn it off to avoid activating unwanted behavior.

For example, a listing like:

  • Java

  • Kotlin

  • Xml

@EnableGlobalMethodSecurity(securedEnabled = true)
@EnableGlobalMethodSecurity(securedEnabled = true)
<global-method-security secured-enabled="true"/>

should change to:

  • Java

  • Kotlin

  • Xml

@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false)
<method-security secured-enabled="true" pre-post-enabled="false"/>

Change the order value in @EnableTransactionManagement

@EnableTransactionManagement and @EnableGlobalMethodSecurity have the same order value, Integer.MAX_VALUE. This means that their order in the Spring AOP Advisor chain relative to each other is undefined.

This is often fine since most method security expressions don’t require an open transaction to function correctly; however, historically it was sometimes necessary to ensure one happens before the other by setting their order values.

@EnableMethodSecurity does not have an order value since it publishes multiple interceptors. Indeed, it cannot attempt backward-compatibility with @EnableTransactionManagement since it cannot set all the interceptors to be in the same advisor chain location.

Instead, the values for the @EnableMethodSecurity interceptors are based off of an offset of 0. The @PreFilter interceptor has an order of 100; @PostAuthorize, 200; and so on.

So, if after updating you find that your method security expressions are not working due to not having an open transaction, please change your transaction annotation definition from the following:

  • Java

  • Kotlin

  • Xml

@EnableTransactionManagement
@EnableTransactionManagement
<tx:annotation-driven ref="txManager"/>

to:

  • Java

  • Kotlin

  • Xml

@EnableTransactionManagement(order = 0)
@EnableTransactionManagement(order = 0)
<tx:annotation-driven ref="txManager" order="0"/>

In this way, the transaction AOP advice will be placed before Spring Security’s advice and the transaction will be open when your authorization SpEL expressions are evaluated.

Use a Custom @Bean instead of subclassing DefaultMethodSecurityExpressionHandler

As a performance optimization, a new method was introduced to MethodSecurityExpressionHandler that takes a Supplier<Authentication> instead of an Authentication.

This allows Spring Security to defer the lookup of the Authentication, and is taken advantage of automatically when you use @EnableMethodSecurity instead of @EnableGlobalMethodSecurity.

However, let’s say that your code extends DefaultMethodSecurityExpressionHandler and overrides createSecurityExpressionRoot(Authentication, MethodInvocation) to return a custom SecurityExpressionRoot instance. This will no longer work because the arrangement that @EnableMethodSecurity sets up calls createEvaluationContext(Supplier<Authentication>, MethodInvocation) instead.

Happily, such a level of customization is often unnecessary. Instead, you can create a custom bean with the authorization methods that you need.

For example, let’s say you are wanting a custom evaluation of @PostAuthorize("hasAuthority('ADMIN')"). You can create a custom @Bean like this one:

  • Java

  • Kotlin

class MyAuthorizer {
	boolean isAdmin(MethodSecurityExpressionOperations root) {
		boolean decision = root.hasAuthority("ADMIN");
		// custom work ...
        return decision;
	}
}
class MyAuthorizer {
	fun isAdmin(val root: MethodSecurityExpressionOperations): boolean {
		val decision = root.hasAuthority("ADMIN");
		// custom work ...
        return decision;
	}
}

and then refer to it in the annotation like so:

  • Java

  • Kotlin

@PreAuthorize("@authz.isAdmin(#root)")
@PreAuthorize("@authz.isAdmin(#root)")

I’d still prefer to subclass DefaultMethodSecurityExpressionHandler

If you must continue subclassing DefaultMethodSecurityExpressionHandler, you can still do so. Instead, override the createEvaluationContext(Supplier<Authentication>, MethodInvocation) method like so:

  • Java

  • Kotlin

@Component
class MyExpressionHandler extends DefaultMethodSecurityExpressionHandler {
    @Override
    public EvaluationContext createEvaluationContext(
            Supplier<Authentication> authentication, MethodInvocation mi) {
		StandardEvaluationContext context = (StandardEvaluationContext) super.createEvaluationContext(authentication, mi);
        MySecurityExpressionRoot root = new MySecurityExpressionRoot(authentication, invocation);
	    root.setPermissionEvaluator(getPermissionEvaluator());
	    root.setTrustResolver(new AuthenticationTrustResolverImpl());
        root.setRoleHierarchy(getRoleHierarchy());
        context.setRootObject(root);
        return context;
    }
}
@Component
class MyExpressionHandler: DefaultMethodSecurityExpressionHandler {
    override fun createEvaluationContext(val authentication: Supplier<Authentication>,
        val mi: MethodInvocation): EvaluationContext {
		val context = super.createEvaluationContext(authentication, mi) as StandardEvaluationContext;
        val root = new MySecurityExpressionRoot(authentication, invocation);
	    root.setPermissionEvaluator(getPermissionEvaluator());
	    root.setTrustResolver(new AuthenticationTrustResolverImpl());
        root.setRoleHierarchy(getRoleHierarchy());
        context.setRootObject(root);
        return context;
    }
}

Opt-out Steps

If you need to opt-out of these changes, you can use @EnableGlobalMethodSecurity instead of @EnableMethodSecurity

Publish a MethodSecurityExpressionHandler instead of a PermissionEvaluator

@EnableMethodSecurity does not pick up a PermissionEvaluator. This helps keep its API simple.

If you have a custom PermissionEvaluator @Bean, please change it from:

  • Java

  • Kotlin

@Bean
static PermissionEvaluator permissionEvaluator() {
	// ... your evaluator
}
companion object {
	@Bean
	fun permissionEvaluator(): PermissionEvaluator {
		// ... your evaluator
	}
}

to:

  • Java

  • Kotlin

@Bean
static MethodSecurityExpressionHandler expressionHandler() {
	var expressionHandler = new DefaultMethodSecurityExpressionHandler();
	expressionHandler.setPermissionEvaluator(myPermissionEvaluator);
	return expressionHandler;
}
companion object {
	@Bean
	fun expressionHandler(): MethodSecurityExpressionHandler {
		val expressionHandler = DefaultMethodSecurityExpressionHandler
		expressionHandler.setPermissionEvaluator(myPermissionEvaluator)
		return expressionHandler
	}
}

Replace any custom method-security AccessDecisionManagers

Your application may have a custom AccessDecisionManager or AccessDecisionVoter arrangement. The preparation strategy will depend on your reason for each arrangement. Read on to find the best match for your situation.

I use UnanimousBased

If your application uses UnanimousBased with the default voters, you likely need do nothing since unanimous-based is the default behavior with @EnableMethodSecurity.

However, if you do discover that you cannot accept the default authorization managers, you can use AuthorizationManagers.allOf to compose your own arrangement.

Note that there is a difference with allOf, which is that if all delegates abstain then it grants authorization. If you must deny authorization when all delegates abstain, please implement a composite AuthorizationManager that takes the set of delegate AuthorizationManagers into account.

Having done that, please follow the details in the reference manual for adding a custom AuthorizationManager.

I use AffirmativeBased

If your application uses AffirmativeBased, then you can construct an equivalent AuthorizationManager, like so:

  • Java

  • Kotlin

AuthorizationManager<MethodInvocation> authorization = AuthorizationManagers.anyOf(
		// ... your list of authorization managers
)
val authorization = AuthorizationManagers.anyOf(
		// ... your list of authorization managers
)

Once you have implemented AuthorizationManager, please follow the details in the reference manual for adding a custom AuthorizationManager.

I use ConsensusBased

There is no framework-provided equivalent for ConsensusBased. In that case, please implement a composite AuthorizationManager that takes the set of delegate AuthorizationManagers into account.

Once you have implemented AuthorizationManager, please follow the details in the reference manual for adding a custom AuthorizationManager.

I use a custom AccessDecisionVoter

You should either change the class to implement AuthorizationManager or create an adapter.

Without knowing what your custom voter is doing, it is impossible to recommend a general-purpose solution. By way of example, though, here is what adapting SecurityMetadataSource and AccessDecisionVoter for @PreAuthorize would look like:

  • Java

public final class PreAuthorizeAuthorizationManagerAdapter implements AuthorizationManager<MethodInvocation> {
    private final SecurityMetadataSource metadata;
    private final AccessDecisionVoter voter;

    public PreAuthorizeAuthorizationManagerAdapter(MethodSecurityExpressionHandler expressionHandler) {
        ExpressionBasedAnnotationAttributeFactory attributeFactory =
                new ExpressionBasedAnnotationAttributeFactory(expressionHandler);
        this.metadata = new PrePostAnnotationSecurityMetadataSource(attributeFactory);
        ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
        expressionAdvice.setExpressionHandler(expressionHandler);
        this.voter = new PreInvocationAuthorizationAdviceVoter(expressionAdvice);
    }

    public AuthorizationDecision check(Supplier<Authentication> authentication, MethodInvocation invocation) {
        List<ConfigAttribute> attributes = this.metadata.getAttributes(invocation, AopUtils.getTargetClass(invocation.getThis()));
        int decision = this.voter.vote(authentication.get(), invocation, attributes);
        if (decision == ACCESS_GRANTED) {
            return new AuthorizationDecision(true);
        }
        if (decision == ACCESS_DENIED) {
            return new AuthorizationDecision(false);
        }
        return null; // abstain
    }
}

Once you have implemented AuthorizationManager, please follow the details in the reference manual for adding a custom AuthorizationManager.

I use AfterInvocationManager or AfterInvocationProvider

AfterInvocationManager and AfterInvocationProvider make an authorization decision about an invocation’s result. For example, in the case of method invocation, these make an authorization decision about a method’s return value.

In Spring Security 3.0, authorization decision-making was standardized into the @PostAuthorize and @PostFilter annotations. @PostAuthorize is for deciding whether the return value as a whole was permitted to be returned. @PostFilter is for filtering individual entries from a returned collection, array, or stream.

These two annotations should serve most needs, and you are encouraged to migrate to one or both of them since AfterInvocationProvider and AfterInvocationManager are now deprecated.

If you’ve implemented your own AfterInvocationManager or AfterInvocationProvider, you should first ask yourself what it is trying to do. If it is trying to authorize the return type, consider implementing AuthorizationManager<MethodInvocationResult> and using AfterMethodAuthorizationManagerInterceptor. Or publishing a custom bean and using @PostAuthorize("@myBean.authorize(#root)").

If it is trying to filter, then consider publishing a custom bean and using @PostFilter("@mybean.authorize(#root)"). Or, if needed, you can implement your own MethodInterceptor, taking a look at PostFilterAuthorizationMethodInterceptor and PrePostMethodSecurityConfiguration for an example.

I use RunAsManager

There is currently no replacement for RunAsManager though one is being considered.

It is quite straightforward to adapt a RunAsManager, though, to the AuthorizationManager API, if needed.

Here is some pseudocode to get you started:

  • Java

public final class RunAsAuthorizationManagerAdapter<T> implements AuthorizationManager<T> {
	private final RunAsManager runAs = new RunAsManagerImpl();
	private final SecurityMetadataSource metadata;
    private final AuthorizationManager<T> authorization;

    // ... constructor

    public AuthorizationDecision check(Supplier<Authentication> authentication, T object) {
		Supplier<Authentication> wrapped = (auth) -> {
			List<ConfigAttribute> attributes = this.metadata.getAttributes(object);
			return this.runAs.buildRunAs(auth, object, attributes);
		};
		return this.authorization.check(wrapped, object);
    }
}

Once you have implemented AuthorizationManager, please follow the details in the reference manual for adding a custom AuthorizationManager.

Check for AnnotationConfigurationExceptions

@EnableMethodSecurity and <method-security> activate stricter enforcement of Spring Security’s non-repeatable or otherwise incompatible annotations. If after moving to either you see AnnotationConfigurationExceptions in your logs, follow the instructions in the exception message to clean up your application’s method security annotation usage.

Use AuthorizationManager for Message Security

Message Security has been improved through the AuthorizationManager API and direct use of Spring AOP.

Should you run into trouble with making these changes, you can follow the opt out steps at the end of this section.

Ensure all messages have defined authorization rules

The now-deprecated message security support permits all messages by default. The new support has the stronger default of denying all messages.

To prepare for this, ensure that authorization rules exist are declared for every request.

For example, an application configuration like:

  • Java

  • Kotlin

  • Xml

@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
    messages
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN");
}
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
    messages
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
}
<websocket-message-broker>
    <intercept-message pattern="/user/queue/errors" access="permitAll"/>
    <intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/>
</websocket-message-broker>

should change to:

  • Java

  • Kotlin

  • Xml

@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
    messages
        .simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll()
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        .anyMessage().denyAll();
}
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
    messages
        .simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll()
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        .anyMessage().denyAll()
}
<websocket-message-broker>
    <intercept-message type="CONNECT" access="permitAll"/>
    <intercept-message type="DISCONNECT" access="permitAll"/>
    <intercept-message type="UNSUBSCRIBE" access="permitAll"/>
    <intercept-message pattern="/user/queue/errors" access="permitAll"/>
    <intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/>
    <intercept-message pattern="/**" access="denyAll"/>
</websocket-message-broker>

Add @EnableWebSocketSecurity

If you want to have CSRF disabled and you are using Java configuration, the migration steps are slightly different. Instead of using @EnableWebSocketSecurity, you will override the appropriate methods in WebSocketMessageBrokerConfigurer yourself. Please see the reference manual for details about this step.

If you are using Java Configuration, add @EnableWebSocketSecurity to your application.

For example, you can add it to your websocket security configuration class, like so:

  • Java

  • Kotlin

@EnableWebSocketSecurity
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
	// ...
}
@EnableWebSocketSecurity
@Configuration
class WebSocketSecurityConfig: AbstractSecurityWebSocketMessageBrokerConfigurer() {
	// ...
}

This will make a prototype instance of MessageMatcherDelegatingAuthorizationManager.Builder available to encourage configuration by composition instead of extension.

Use an AuthorizationManager<Message<?>> instance

To start using AuthorizationManager, you can set the use-authorization-manager attribute in XML or you can publish an AuthorizationManager<Message<?>> @Bean in Java.

For example, the following application configuration:

  • Java

  • Kotlin

  • Xml

@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
    messages
        .simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll()
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        .anyMessage().denyAll();
}
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
    messages
        .simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll()
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        .anyMessage().denyAll()
}
<websocket-message-broker>
    <intercept-message type="CONNECT" access="permitAll"/>
    <intercept-message type="DISCONNECT" access="permitAll"/>
    <intercept-message type="UNSUBSCRIBE" access="permitAll"/>
    <intercept-message pattern="/user/queue/errors" access="permitAll"/>
    <intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/>
    <intercept-message pattern="/**" access="denyAll"/>
</websocket-message-broker>

changes to:

  • Java

  • Kotlin

  • Xml

@Bean
AuthorizationManager<Message<?>> messageSecurity(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
    messages
        .simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll()
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        .anyMessage().denyAll();
	return messages.build();
}
@Bean
fun messageSecurity(val messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
    messages
        .simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll()
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        .anyMessage().denyAll()
    return messages.build()
}
<websocket-message-broker use-authorization-manager="true">
    <intercept-message type="CONNECT" access="permitAll"/>
    <intercept-message type="DISCONNECT" access="permitAll"/>
    <intercept-message type="UNSUBSCRIBE" access="permitAll"/>
    <intercept-message pattern="/user/queue/errors" access="permitAll"/>
    <intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/>
    <intercept-message pattern="/**" access="denyAll"/>
</websocket-message-broker>

Stop Implementing AbstractSecurityWebSocketMessageBrokerConfigurer

If you are using Java configuration, you can now simply extend WebSocketMessageBrokerConfigurer.

For example, if your class that extends AbstractSecurityWebSocketMessageBrokerConfigurer is called WebSocketSecurityConfig, then:

  • Java

  • Kotlin

@EnableWebSocketSecurity
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
	// ...
}
@EnableWebSocketSecurity
@Configuration
class WebSocketSecurityConfig: AbstractSecurityWebSocketMessageBrokerConfigurer() {
	// ...
}

changes to:

  • Java

  • Kotlin

@EnableWebSocketSecurity
@Configuration
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
	// ...
}
@EnableWebSocketSecurity
@Configuration
class WebSocketSecurityConfig: WebSocketMessageBrokerConfigurer {
	// ...
}

Opt-out Steps

In case you had trouble, take a look at these scenarios for optimal opt out behavior:

I cannot declare an authorization rule for all requests

If you are having trouble setting an anyRequest authorization rule of denyAll, please use permitAll instead, like so:

  • Java

  • Kotlin

  • Xml

@Bean
AuthorizationManager<Message<?>> messageSecurity(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
    messages
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        // ...
        .anyMessage().permitAll();
	return messages.build();
}
@Bean
fun messageSecurity(val messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
    messages
        .simpDestMatchers("/user/queue/errors").permitAll()
        .simpDestMatchers("/admin/**").hasRole("ADMIN")
        // ...
        .anyMessage().permitAll();
    return messages.build()
}
<websocket-message-broker use-authorization-manager="true">
    <intercept-message pattern="/user/queue/errors" access="permitAll"/>
    <intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/>
    <!-- ... -->
    <intercept-message pattern="/**" access="permitAll"/>
</websocket-message-broker>

I cannot get CSRF working, need some other AbstractSecurityWebSocketMessageBrokerConfigurer feature, or am having trouble with AuthorizationManager

In the case of Java, you may continue using AbstractMessageSecurityWebSocketMessageBrokerConfigurer. Even though it is deprecated, it will not be removed in 6.0.

In the case of XML, you can opt out of AuthorizationManager by setting use-authorization-manager="false":

Xml
<websocket-message-broker>
    <intercept-message pattern="/user/queue/errors" access="permitAll"/>
    <intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/>
</websocket-message-broker>

to:

Xml
<websocket-message-broker use-authorization-manager="false">
    <intercept-message pattern="/user/queue/errors" access="permitAll"/>
    <intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/>
</websocket-message-broker>

Use AuthorizationManager for Request Security

Should you run into trouble with making these changes, you can follow the opt out steps at the end of this section.

Ensure that all requests have defined authorization rules

In Spring Security 5.8 and earlier, requests with no authorization rule are permitted by default. It is a stronger security position to deny by default, thus requiring that authorization rules be clearly defined for every endpoint. As such, in 6.0, Spring Security by default denies any request that is missing an authorization rule.

The simplest way to prepare for this change is to introduce an appropriate anyRequest rule as the last authorization rule. The recommendation is denyAll since that is the implied 6.0 default.

You may already have an anyRequest rule defined that you are happy with in which case this step can be skipped.

Adding denyAll to the end looks like changing:

  • Java

  • Kotlin

  • Xml

http
    .authorizeRequests((authorize) -> authorize
        .filterSecurityInterceptorOncePerRequest(true)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
    )
    // ...
http {
    authorizeRequests {
        filterSecurityInterceptorOncePerRequest = true
        authorize("/app/**", hasRole("APP"))
        // ...
    }
}
<http once-per-request="true">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
</http>

to:

  • Java

  • Kotlin

  • Xml

http
    .authorizeRequests((authorize) -> authorize
        .filterSecurityInterceptorOncePerRequest(true)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeRequests {
        filterSecurityInterceptorOncePerRequest = true
        authorize("/app/**", hasRole("APP"))
        // ...
        authorize(anyRequest, denyAll)
    }
}
<http once-per-request="true">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
    <intercept-url pattern="/**" access="denyAll"/>
</http>

If you have already migrated to authorizeHttpRequests, the recommended change is the same.

Switch to AuthorizationManager

To opt in to using AuthorizationManager, you can use authorizeHttpRequests or use-authorization-manager for Java or XML, respectively.

Change:

  • Java

  • Kotlin

  • Xml

http
    .authorizeRequests((authorize) -> authorize
        .filterSecurityInterceptorOncePerRequest(true)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeRequests {
        filterSecurityInterceptorOncePerRequest = true
        authorize("/app/**", hasRole("APP"))
        // ...
        authorize(anyRequest, denyAll)
    }
}
<http once-per-request="true">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
    <intercept-url pattern="/**" access="denyAll"/>
</http>

to:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize
        .shouldFilterAllDispatcherTypes(false)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeHttpRequests {
        shouldFilterAllDispatcherTypes = false
        authorize("/app/**", hasRole("APP"))
        // ...
        authorize(anyRequest, denyAll)
    }
}
<http filter-all-dispatcher-types="false" use-authorization-manager="true">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
    <intercept-url pattern="/**" access="denyAll"/>
</http>

Migrate hasIpAddress to access(AuthorizationManager)

hasIpAddress has no DSL equivalent in authorizeHttpRequests.

As such, you need to change any called to hasIpAddress to using an AuthorizationManager.

First, construct an IpAddressMatcher like so:

Java
IpAddressMatcher hasIpAddress = new IpAddressMatcher("127.0.0.1");

And then change from this:

Java
http
    .authorizeRequests((authorize) -> authorize
        .mvcMatchers("/app/**").hasIpAddress("127.0.0.1")
        // ...
        .anyRequest().denyAll()
    )
    // ...

to this:

Java
http
    .authorizeHttpRequests((authorize) -> authorize
        .requestMatchers("/app/**").access((authentication, context) ->
            new AuthorizationDecision(hasIpAddress.matches(context.getRequest()))
        // ...
        .anyRequest().denyAll()
    )
    // ...
Securing by IP Address is quite fragile to begin with. For that reason, there are no plans to port this support over to authorizeHttpRequests.

Migrate SpEL expressions to AuthorizationManager

For authorization rules, Java tends to be easier to test and maintain than SpEL. As such, authorizeHttpRequests does not have a method for declaring a String SpEL.

Instead, you can implement your own AuthorizationManager implementation or use WebExpressionAuthorizationManager.

For completeness, both options will be demonstrated.

First, if you have the following SpEL:

  • Java

  • Kotlin

http
    .authorizeRequests((authorize) -> authorize
        .filterSecurityInterceptorOncePerRequest(true)
        .mvcMatchers("/complicated/**").access("hasRole('ADMIN') || hasAuthority('SCOPE_read')")
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeRequests {
        filterSecurityInterceptorOncePerRequest = true
        authorize("/complicated/**", access("hasRole('ADMIN') || hasAuthority('SCOPE_read')"))
        // ...
        authorize(anyRequest, denyAll)
    }
}

Then you can compose your own AuthorizationManager with Spring Security authorization primitives like so:

  • Java

  • Kotlin

http
    .authorizeHttpRequests((authorize) -> authorize
        .shouldFilterAllDispatcherTypes(false)
        .mvcMatchers("/complicated/**").access(anyOf(hasRole("ADMIN"), hasAuthority("SCOPE_read"))
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeHttpRequests {
        shouldFilterAllDispatcherTypes = false
        authorize("/complicated/**", access(anyOf(hasRole("ADMIN"), hasAuthority("SCOPE_read"))
        // ...
        authorize(anyRequest, denyAll)
    }
}

Or you can use WebExpressionAuthorizationManager in the following way:

  • Java

  • Kotlin

http
    .authorizeRequests((authorize) -> authorize
        .filterSecurityInterceptorOncePerRequest(true)
        .mvcMatchers("/complicated/**").access(
			new WebExpressionAuthorizationManager("hasRole('ADMIN') || hasAuthority('SCOPE_read')")
        )
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeRequests {
        filterSecurityInterceptorOncePerRequest = true
        authorize("/complicated/**", access(
            WebExpressionAuthorizationManager("hasRole('ADMIN') || hasAuthority('SCOPE_read')"))
        )
        // ...
        authorize(anyRequest, denyAll)
    }
}

Switch to filter all dispatcher types

Spring Security 5.8 and earlier only perform authorization once per request. This means that dispatcher types like FORWARD and INCLUDE that run after REQUEST are not secured by default.

It’s recommended that Spring Security secure all dispatch types. As such, in 6.0, Spring Security changes this default.

So, finally, change your authorization rules to filter all dispatcher types.

To do this, you should change:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize
        .shouldFilterAllDispatcherTypes(false)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeHttpRequests {
        shouldFilterAllDispatcherTypes = false
        authorize("/app/**", hasRole("APP"))
        // ...
        authorize(anyRequest, denyAll)
    }
}
<http filter-all-dispatcher-types="false" use-authorization-manager="true">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
    <intercept-url pattern="/**" access="denyAll"/>
</http>

to:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize
        .shouldFilterAllDispatcherTypes(true)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeHttpRequests {
        shouldFilterAllDispatcherTypes = true
        authorize("/app/**", hasRole("APP"))
        // ...
        authorize(anyRequest, denyAll)
    }
}
<http filter-all-dispatcher-types="true" use-authorization-manager="true">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
    <intercept-url pattern="/**" access="denyAll"/>
</http>

And, the FilterChainProxy should be registered for all dispatcher types as well. If you are using Spring Boot, you have to change the spring.security.filter.dispatcher-types property to include all dispatcher types:

  • application.properties

spring.security.filter.dispatcher-types=request,async,error,forward,include

If you are using the AbstractSecurityWebApplicationInitializer you should override the getSecurityDispatcherTypes method and return all dispatcher types:

  • Java

import org.springframework.security.web.context.*;

public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {

    @Override
    protected EnumSet<DispatcherType> getSecurityDispatcherTypes() {
        return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR, DispatcherType.ASYNC,
                DispatcherType.FORWARD, DispatcherType.INCLUDE);
    }

}

Permit FORWARD when using Spring MVC

If you are using Spring MVC to resolve view names, you will need to permit FORWARD requests. This is because when Spring MVC detects a mapping between view name and the actual views, it will perform a forward to the view. As we saw on the previous section, Spring Security 6.0 will apply authorization to FORWARD requests by default.

Consider the following common configuration:

  • Java

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .shouldFilterAllDispatcherTypes(true)
            .requestMatchers("/").authenticated()
            .anyRequest().denyAll()
        )
        .formLogin((form) -> form
            .loginPage("/login")
            .permitAll()
        ));
    return http.build();
}

and one of the following equivalents MVC view mapping configurations:

  • Java

@Controller
public class MyController {

    @GetMapping("/login")
    public String login() {
        return "login";
    }

}
  • Java

@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/login").setViewName("login");
    }

}

With either configuration, when there is a request to /login, Spring MVC will perform a forward to the view login, which, with the default configuration, is under src/main/resources/templates/login.html path. The security configuration permits requests to /login but every other request will be denied, including the FORWARD request to the view under /templates/login.html.

To fix this, you should configure Spring Security to permit FORWARD requests:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize
        .shouldFilterAllDispatcherTypes(true)
        .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll()
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeHttpRequests {
        shouldFilterAllDispatcherTypes = true
        authorize(DispatcherTypeRequestMatcher(DispatcherType.FORWARD), permitAll)
        authorize(anyRequest, denyAll)
    }
}
<http filter-all-dispatcher-types="true" use-authorization-manager="true">
    <intercept-url request-matcher-ref="forwardRequestMatcher" access="permitAll()" />
    <!-- ... -->
    <intercept-url pattern="/**" access="denyAll"/>
</http>

<bean name="forwardRequestMatcher" class="org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher">
    <constructor-arg value="FORWARD"/>
</bean>

Replace any custom filter-security AccessDecisionManagers

Your application may have a custom AccessDecisionManager or AccessDecisionVoter arrangement. The preparation strategy will depend on your reason for each arrangement. Read on to find the best match for your situation.

I use UnanimousBased

If your application uses UnanimousBased, you should first adapt or replace any AccessDecisionVoters and then you can construct an AuthorizationManager like so:

  • Java

  • Kotlin

  • Xml

@Bean
AuthorizationManager<RequestAuthorizationContext> requestAuthorization() {
    PolicyAuthorizationManager policy = ...;
    LocalAuthorizationManager local = ...;
    return AuthorizationManagers.allOf(policy, local);
}
@Bean
fun requestAuthorization(): AuthorizationManager<RequestAuthorizationContext> {
    val policy: PolicyAuthorizationManager = ...
    val local: LocalAuthorizationManager = ...
    return AuthorizationManagers.allOf(policy, local)
}
<bean id="requestAuthorization" class="org.springframework.security.authorization.AuthorizationManagers"
        factory-method="allOf">
    <constructor-arg>
        <util:list>
            <bean class="my.PolicyAuthorizationManager"/>
            <bean class="my.LocalAuthorizationManager"/>
        </util:list>
    </constructor-arg>
</bean>

then, wire it into the DSL like so:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize.anyRequest().access(requestAuthorization))
    // ...
http {
    authorizeHttpRequests {
        authorize(anyRequest, requestAuthorization)
    }
    // ...
}
<http authorization-manager-ref="requestAuthorization"/>

authorizeHttpRequests is designed so that you can apply a custom AuthorizationManager to any url pattern. See the reference for more details.

I use AffirmativeBased

If your application uses AffirmativeBased, then you can construct an equivalent AuthorizationManager, like so:

  • Java

  • Kotlin

  • Xml

@Bean
AuthorizationManager<RequestAuthorizationContext> requestAuthorization() {
    PolicyAuthorizationManager policy = ...;
    LocalAuthorizationManager local = ...;
    return AuthorizationManagers.anyOf(policy, local);
}
@Bean
fun requestAuthorization(): AuthorizationManager<RequestAuthorizationContext> {
    val policy: PolicyAuthorizationManager = ...
    val local: LocalAuthorizationManager = ...
    return AuthorizationManagers.anyOf(policy, local)
}
<bean id="requestAuthorization" class="org.springframework.security.authorization.AuthorizationManagers"
        factory-method="anyOf">
    <constructor-arg>
        <util:list>
            <bean class="my.PolicyAuthorizationManager"/>
            <bean class="my.LocalAuthorizationManager"/>
        </util:list>
    </constructor-arg>
</bean>

then, wire it into the DSL like so:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize.anyRequest().access(requestAuthorization))
    // ...
http {
    authorizeHttpRequests {
        authorize(anyRequest, requestAuthorization)
    }
    // ...
}
<http authorization-manager-ref="requestAuthorization"/>

authorizeHttpRequests is designed so that you can apply a custom AuthorizationManager to any url pattern. See the reference for more details.

I use ConsensusBased

There is no framework-provided equivalent for ConsensusBased. In that case, please implement a composite AuthorizationManager that takes the set of delegate AuthorizationManagers into account.

Once you have implemented AuthorizationManager, please follow the details in the reference manual for adding a custom AuthorizationManager.

I use a custom AccessDecisionVoter

You should either change the class to implement AuthorizationManager or create an adapter.

Without knowing what your custom voter is doing, it is impossible to recommend a general-purpose solution. By way of example, though, here is what adapting SecurityMetadataSource and AccessDecisionVoter for anyRequest().authenticated() would look like:

  • Java

public final class AnyRequestAuthenticatedAuthorizationManagerAdapter implements AuthorizationManager<RequestAuthorizationContext> {
    private final SecurityMetadataSource metadata;
    private final AccessDecisionVoter voter;

    public PreAuthorizeAuthorizationManagerAdapter(SecurityExpressionHandler expressionHandler) {
        Map<RequestMatcher, List<ConfigAttribute>> requestMap = Collections.singletonMap(
                AnyRequestMatcher.INSTANCE, Collections.singletonList(new SecurityConfig("authenticated")));
        this.metadata = new DefaultFilterInvocationSecurityMetadataSource(requestMap);
        WebExpressionVoter voter = new WebExpressionVoter();
        voter.setExpressionHandler(expressionHandler);
        this.voter = voter;
    }

    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext context) {
        List<ConfigAttribute> attributes = this.metadata.getAttributes(context);
        int decision = this.voter.vote(authentication.get(), invocation, attributes);
        if (decision == ACCESS_GRANTED) {
            return new AuthorizationDecision(true);
        }
        if (decision == ACCESS_DENIED) {
            return new AuthorizationDecision(false);
        }
        return null; // abstain
    }
}

Once you have implemented AuthorizationManager, please follow the details in the reference manual for adding a custom AuthorizationManager.

Replace hasRole with hasAuthority if using GrantedAuthorityDefaults

Currently, the hasRole method inside authorizeHttpRequests does not support the GrantedAuthorityDefaults bean like the authorizeRequests does. Therefore, if you are using GrantedAuthorityDefaults to change the prefix of your roles, you will need to use hasAuthority instead of hasRole.

For example, you will have to change from:

authorizeRequests with custom role prefix
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeRequests((authorize) -> authorize
            .anyRequest().hasRole("ADMIN")
        );
    return http.build();
}

@Bean
public GrantedAuthorityDefaults grantedAuthorityDefaults() {
    return new GrantedAuthorityDefaults("MYPREFIX_");
}

to:

authorizeHttpRequests with hasAuthority and custom role prefix
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests((authorize) -> authorize
            .anyRequest().hasAuthority("MYPREFIX_ADMIN")
        );
    return http.build();
}

This should be supported in the future, see gh-13227 for more details.

Opt-out Steps

In case you had trouble, take a look at these scenarios for optimal opt out behavior:

I cannot secure all dispatcher types

If you cannot secure all dispatcher types, first try and declare which dispatcher types should not require authorization like so:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize
        .shouldFilterAllDispatcherTypes(true)
        .dispatcherTypeMatchers(FORWARD, INCLUDE).permitAll()
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
        .anyRequest().denyAll()
    )
    // ...
http {
    authorizeHttpRequests {
        shouldFilterAllDispatcherTypes = true
        authorize(DispatcherTypeRequestMatcher(FORWARD, INCLUDE), permitAll)
        authorize("/app/**", hasRole("APP"))
        // ...
        authorize(anyRequest, denyAll)
    }
}
<http filter-all-dispatcher-types="true" use-authorization-manager="true">
    <intercept-url request-matcher-ref="dispatchers"/>
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
    <intercept-url pattern="/**" access="denyAll"/>
</http>

<bean id="dispatchers" class="org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher">
    <constructor-arg>
        <util:list value-type="javax.servlet.DispatcherType">
            <value>FORWARD</value>
            <value>INCLUDE</value>
        </util:list>
    </constructor-arg>
</bean>

Or, if that doesn’t work, then you can explicitly opt out of the behavior by setting filter-all-dispatcher-types and filterAllDispatcherTypes to false:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpRequests((authorize) -> authorize
        .filterAllDispatcherTypes(false)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
    )
    // ...
http {
    authorizeHttpRequests {
        filterAllDispatcherTypes = false
        authorize("/messages/**", hasRole("APP"))
        // ...
    }
}
<http filter-all-dispatcher-types="false" use-authorization-manager="true">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
</http>

or, if you are still using authorizeRequests or use-authorization-manager="false", set oncePerRequest to true:

  • Java

  • Kotlin

  • Xml

http
    .authorizeRequests((authorize) -> authorize
        .filterSecurityInterceptorOncePerRequest(true)
        .mvcMatchers("/app/**").hasRole("APP")
        // ...
    )
    // ...
http {
    authorizeRequests {
        filterSecurityInterceptorOncePerRequest = true
        authorize("/messages/**", hasRole("APP"))
        // ...
    }
}
<http once-per-request="true" use-authorization-manager="false">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
</http>

I cannot declare an authorization rule for all requests

If you are having trouble setting an anyRequest authorization rule of denyAll, please use permitAll instead, like so:

  • Java

  • Kotlin

  • Xml

http
    .authorizeHttpReqeusts((authorize) -> authorize
        .mvcMatchers("/app/*").hasRole("APP")
        // ...
        .anyRequest().permitAll()
    )
http {
    authorizeHttpRequests {
        authorize("/app*", hasRole("APP"))
        // ...
        authorize(anyRequest, permitAll)
    }
}
<http>
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
    <intercept-url pattern="/**" access="permitAll"/>
</http>

I cannot migrate my SpEL or my AccessDecisionManager

If you are having trouble with SpEL, AccessDecisionManager, or there is some other feature that you are needing to keep using in <http> or authorizeRequests, try the following.

First, if you still need authorizeRequests, you are welcome to keep using it. Even though it is deprecated, it is not removed in 6.0.

Second, if you still need your custom access-decision-manager-ref or have some other reason to opt out of AuthorizationManager, do:

Xml
<http use-authorization-manager="false">
    <intercept-url pattern="/app/*" access="hasRole('APP')"/>
    <!-- ... -->
</http>