You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1985 lines
58 KiB
1985 lines
58 KiB
= Authorization Migrations |
|
|
|
The following steps relate to changes around how authorization is performed. |
|
|
|
== Use `AuthorizationManager` for Method Security |
|
|
|
xref:servlet/authorization/method-security.adoc[Method Security] has been xref:servlet/authorization/method-security.adoc#jc-enable-method-security[simplified] through {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[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. |
|
|
|
[[servlet-replace-globalmethodsecurity-with-methodsecurity]] |
|
=== Replace xref:servlet/authorization/method-security.adoc#jc-enable-global-method-security[global method security] with xref:servlet/authorization/method-security.adoc#jc-enable-method-security[method security] |
|
|
|
{security-api-url}org/springframework/security/config/annotation/method/configuration/EnableGlobalMethodSecurity.html[`@EnableGlobalMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-global-method-security[`<global-method-security>`] are deprecated in favor of {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[`@EnableMethodSecurity`] and xref:servlet/appendix/namespace/method-security.adoc#nsa-method-security[`<method-security>`], respectively. |
|
The new annotation and XML element activate Spring's xref:servlet/authorization/method-security.adoc#jc-enable-method-security[pre-post annotations] by default and use `AuthorizationManager` internally. |
|
|
|
This means that the following two listings are functionally equivalent: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableGlobalMethodSecurity(prePostEnabled = true) |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableGlobalMethodSecurity(prePostEnabled = true) |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<global-method-security pre-post-enabled="true"/> |
|
---- |
|
====== |
|
|
|
and: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableGlobalMethodSecurity(securedEnabled = true) |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableGlobalMethodSecurity(securedEnabled = true) |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<global-method-security secured-enabled="true"/> |
|
---- |
|
====== |
|
|
|
should change to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false) |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = false) |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableTransactionManagement |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableTransactionManagement |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<tx:annotation-driven ref="txManager"/> |
|
---- |
|
====== |
|
|
|
to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableTransactionManagement(order = 0) |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableTransactionManagement(order = 0) |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
class MyAuthorizer { |
|
boolean isAdmin(MethodSecurityExpressionOperations root) { |
|
boolean decision = root.hasAuthority("ADMIN"); |
|
// custom work ... |
|
return decision; |
|
} |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@PreAuthorize("@authz.isAdmin(#root)") |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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; |
|
} |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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` |
|
|
|
[[servlet-replace-permissionevaluator-bean-with-methodsecurityexpression-handler]] |
|
=== 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 {security-api-url}org/springframework/security/access/PermissionEvaluator.html[`PermissionEvaluator`] `@Bean`, please change it from: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
static PermissionEvaluator permissionEvaluator() { |
|
// ... your evaluator |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
companion object { |
|
@Bean |
|
fun permissionEvaluator(): PermissionEvaluator { |
|
// ... your evaluator |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
static MethodSecurityExpressionHandler expressionHandler() { |
|
var expressionHandler = new DefaultMethodSecurityExpressionHandler(); |
|
expressionHandler.setPermissionEvaluator(myPermissionEvaluator); |
|
return expressionHandler; |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
companion object { |
|
@Bean |
|
fun expressionHandler(): MethodSecurityExpressionHandler { |
|
val expressionHandler = DefaultMethodSecurityExpressionHandler |
|
expressionHandler.setPermissionEvaluator(myPermissionEvaluator) |
|
return expressionHandler |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
=== Replace any custom method-security ``AccessDecisionManager``s |
|
|
|
Your application may have a custom {security-api-url}org/springframework/security/access/AccessDecisionManager.html[`AccessDecisionManager`] or {security-api-url}org/springframework/security/access/AccessDecisionVoter.html[`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 {security-api-url}org/springframework/security/access/vote/UnanimousBased.html[`UnanimousBased`] with the default voters, you likely need do nothing since unanimous-based is the default behavior with {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableMethodSecurity.html[`@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 {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`] that takes the set of delegate ``AuthorizationManager``s into account. |
|
|
|
Having done that, please follow the details in the reference manual for xref:servlet/authorization/method-security.adoc#jc-method-security-custom-authorization-manager[adding a custom `AuthorizationManager`]. |
|
|
|
==== I use `AffirmativeBased` |
|
|
|
If your application uses {security-api-url}org/springframework/security/access/vote/AffirmativeBased.html[`AffirmativeBased`], then you can construct an equivalent {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`], like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
AuthorizationManager<MethodInvocation> authorization = AuthorizationManagers.anyOf( |
|
// ... your list of authorization managers |
|
) |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val authorization = AuthorizationManagers.anyOf( |
|
// ... your list of authorization managers |
|
) |
|
---- |
|
====== |
|
|
|
Once you have implemented `AuthorizationManager`, please follow the details in the reference manual for xref:servlet/authorization/method-security.adoc#jc-method-security-custom-authorization-manager[adding a custom `AuthorizationManager`]. |
|
|
|
==== I use `ConsensusBased` |
|
|
|
There is no framework-provided equivalent for {security-api-url}org/springframework/security/access/vote/ConsensusBased.html[`ConsensusBased`]. |
|
In that case, please implement a composite {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`] that takes the set of delegate ``AuthorizationManager``s into account. |
|
|
|
Once you have implemented `AuthorizationManager`, please follow the details in the reference manual for xref:servlet/authorization/method-security.adoc#jc-method-security-custom-authorization-manager[adding a custom `AuthorizationManager`]. |
|
|
|
==== I use a custom `AccessDecisionVoter` |
|
|
|
You should either change the class to implement {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`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 {security-api-url}org/springframework/security/access/SecurityMetadataSource.html[`SecurityMetadataSource`] and {security-api-url}org/springframework/security/access/AccessDecisionVoter.html[`AccessDecisionVoter`] for `@PreAuthorize` would look like: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
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 xref:servlet/authorization/method-security.adoc#jc-method-security-custom-authorization-manager[adding a custom `AuthorizationManager`]. |
|
|
|
==== I use `AfterInvocationManager` or `AfterInvocationProvider` |
|
|
|
{security-api-url}org/springframework/security/access/intercept/AfterInvocationManager.html;[`AfterInvocationManager`] and {security-api-url}org/springframework/security/access/intercept/AfterInvocationProvider.html[`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 xref:servlet/authorization/method-security.adoc[`@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, <<_i_use_a_custom_accessdecisionvoter,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 https://github.com/spring-projects/spring-security/issues/11331[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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
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 xref:servlet/authorization/method-security.adoc#jc-method-security-custom-authorization-manager[adding a custom `AuthorizationManager`]. |
|
|
|
[[servlet-check-for-annotationconfigurationexceptions]] |
|
=== Check for ``AnnotationConfigurationException``s |
|
|
|
`@EnableMethodSecurity` and `<method-security>` activate stricter enforcement of Spring Security's non-repeatable or otherwise incompatible annotations. |
|
If after moving to either you see ``AnnotationConfigurationException``s 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 |
|
|
|
xref:servlet/integrations/websocket.adoc[Message Security] has been xref:servlet/integrations/websocket.adoc#websocket-configuration[improved] through {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[the `AuthorizationManager` API] and direct use of Spring AOP. |
|
|
|
Should you run into trouble with making these changes, you can follow the <<servlet-authorizationmanager-messages-opt-out,opt out steps>> at the end of this section. |
|
|
|
=== Ensure all messages have defined authorization rules |
|
|
|
The now-deprecated {security-api-url}org/springframework/security/config/annotation/web/socket/AbstractSecurityWebSocketMessageBrokerConfigurer.html[message security support] permits all messages by default. |
|
xref:servlet/integrations/websocket.adoc[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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Override |
|
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { |
|
messages |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN"); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) { |
|
messages |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN") |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<websocket-message-broker> |
|
<intercept-message pattern="/user/queue/errors" access="permitAll"/> |
|
<intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/> |
|
</websocket-message-broker> |
|
---- |
|
====== |
|
|
|
should change to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Override |
|
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { |
|
messages |
|
.simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll() |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN") |
|
.anyMessage().denyAll(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) { |
|
messages |
|
.simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll() |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN") |
|
.anyMessage().denyAll() |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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` |
|
|
|
[NOTE] |
|
==== |
|
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 xref:servlet/integrations/websocket.adoc#websocket-sameorigin-disable[the reference manual] for details about this step. |
|
==== |
|
|
|
If you are using Java Configuration, add {security-api-url}org/springframework/security/config/annotation/web/socket/EnableWebSocketSecurity.html[`@EnableWebSocketSecurity`] to your application. |
|
|
|
For example, you can add it to your websocket security configuration class, like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { |
|
// ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Override |
|
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) { |
|
messages |
|
.simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll() |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN") |
|
.anyMessage().denyAll(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) { |
|
messages |
|
.simpTypeMatchers(CONNECT, DISCONNECT, UNSUBSCRIBE).permitAll() |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN") |
|
.anyMessage().denyAll() |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@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() |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { |
|
// ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
class WebSocketSecurityConfig: AbstractSecurityWebSocketMessageBrokerConfigurer() { |
|
// ... |
|
} |
|
---- |
|
====== |
|
|
|
changes to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer { |
|
// ... |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
class WebSocketSecurityConfig: WebSocketMessageBrokerConfigurer { |
|
// ... |
|
} |
|
---- |
|
====== |
|
|
|
This also implies that you will need to xref:servlet/integrations/websocket.adoc#websocket-authorization[use a `MessageMatcherDelegationAuthorizationManager`] to specify your authorization rules. |
|
|
|
[[servlet-authorizationmanager-messages-opt-out]] |
|
=== 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 {security-api-url}org/springframework/security/messaging/access/intercept/MessageMatcherDelegatingAuthorizationManager.Builder.Constraint.html#permitAll()[`permitAll`] instead, like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
AuthorizationManager<Message<?>> messageSecurity(MessageMatcherDelegatingAuthorizationManager.Builder messages) { |
|
messages |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN") |
|
// ... |
|
.anyMessage().permitAll(); |
|
return messages.build(); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun messageSecurity(val messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> { |
|
messages |
|
.simpDestMatchers("/user/queue/errors").permitAll() |
|
.simpDestMatchers("/admin/**").hasRole("ADMIN") |
|
// ... |
|
.anyMessage().permitAll(); |
|
return messages.build() |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 |
|
[source,xml,role="secondary"] |
|
---- |
|
<websocket-message-broker> |
|
<intercept-message pattern="/user/queue/errors" access="permitAll"/> |
|
<intercept-message pattern="/admin/**" access="hasRole('ADMIN')"/> |
|
</websocket-message-broker> |
|
---- |
|
|
|
to: |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 |
|
|
|
xref:servlet/authorization/authorize-requests.adoc[HTTP Request Security] has been xref:servlet/authorization/authorize-http-requests.adoc[simplified] through {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[the `AuthorizationManager` API]. |
|
|
|
Should you run into trouble with making these changes, you can follow the <<servlet-authorizationmanager-requests-opt-out,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 {security-api-url}org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.html#anyRequest()[`anyRequest`] rule as the last authorization rule. |
|
The recommendation is {security-api-url}org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.AuthorizedUrl.html#denyAll()[`denyAll`] since that is the implied 6.0 default. |
|
|
|
[NOTE] |
|
==== |
|
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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeRequests((authorize) -> authorize |
|
.filterSecurityInterceptorOncePerRequest(true) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeRequests { |
|
filterSecurityInterceptorOncePerRequest = true |
|
authorize("/app/**", hasRole("APP")) |
|
// ... |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<http once-per-request="true"> |
|
<intercept-url pattern="/app/*" access="hasRole('APP')"/> |
|
<!-- ... --> |
|
</http> |
|
---- |
|
====== |
|
|
|
to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeRequests((authorize) -> authorize |
|
.filterSecurityInterceptorOncePerRequest(true) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeRequests { |
|
filterSecurityInterceptorOncePerRequest = true |
|
authorize("/app/**", hasRole("APP")) |
|
// ... |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 xref:servlet/appendix/namespace/http.adoc#nsa-http-use-authorization-manager[`use-authorization-manager`] for Java or XML, respectively. |
|
|
|
Change: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeRequests((authorize) -> authorize |
|
.filterSecurityInterceptorOncePerRequest(true) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeRequests { |
|
filterSecurityInterceptorOncePerRequest = true |
|
authorize("/app/**", hasRole("APP")) |
|
// ... |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<http once-per-request="true"> |
|
<intercept-url pattern="/app/*" access="hasRole('APP')"/> |
|
<!-- ... --> |
|
<intercept-url pattern="/**" access="denyAll"/> |
|
</http> |
|
---- |
|
====== |
|
|
|
to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.shouldFilterAllDispatcherTypes(false) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
shouldFilterAllDispatcherTypes = false |
|
authorize("/app/**", hasRole("APP")) |
|
// ... |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 |
|
[source,java,role="primary"] |
|
---- |
|
IpAddressMatcher hasIpAddress = new IpAddressMatcher("127.0.0.1"); |
|
---- |
|
==== |
|
|
|
And then change from this: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeRequests((authorize) -> authorize |
|
.mvcMatchers("/app/**").hasIpAddress("127.0.0.1") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
==== |
|
|
|
to this: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.requestMatchers("/app/**").access((authentication, context) -> |
|
new AuthorizationDecision(hasIpAddress.matches(context.getRequest())) |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
==== |
|
|
|
[NOTE] |
|
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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeRequests((authorize) -> authorize |
|
.filterSecurityInterceptorOncePerRequest(true) |
|
.mvcMatchers("/complicated/**").access("hasRole('ADMIN') || hasAuthority('SCOPE_read')") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.shouldFilterAllDispatcherTypes(false) |
|
.mvcMatchers("/complicated/**").access(anyOf(hasRole("ADMIN"), hasAuthority("SCOPE_read")) |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeRequests((authorize) -> authorize |
|
.filterSecurityInterceptorOncePerRequest(true) |
|
.mvcMatchers("/complicated/**").access( |
|
new WebExpressionAuthorizationManager("hasRole('ADMIN') || hasAuthority('SCOPE_read')") |
|
) |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeRequests { |
|
filterSecurityInterceptorOncePerRequest = true |
|
authorize("/complicated/**", access( |
|
WebExpressionAuthorizationManager("hasRole('ADMIN') || hasAuthority('SCOPE_read')")) |
|
) |
|
// ... |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
====== |
|
|
|
[[switch-filter-all-dispatcher-types]] |
|
=== Switch to filter all dispatcher types |
|
|
|
Spring Security 5.8 and earlier only xref:servlet/authorization/architecture.adoc[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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.shouldFilterAllDispatcherTypes(false) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
shouldFilterAllDispatcherTypes = false |
|
authorize("/app/**", hasRole("APP")) |
|
// ... |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<http filter-all-dispatcher-types="false" use-authorization-manager="true"> |
|
<intercept-url pattern="/app/*" access="hasRole('APP')"/> |
|
<!-- ... --> |
|
<intercept-url pattern="/**" access="denyAll"/> |
|
</http> |
|
---- |
|
====== |
|
|
|
to: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.shouldFilterAllDispatcherTypes(true) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
shouldFilterAllDispatcherTypes = true |
|
authorize("/app/**", hasRole("APP")) |
|
// ... |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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, https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties.security.spring.security.filter.dispatcher-types[you have to change the `spring.security.filter.dispatcher-types` property] to include all dispatcher types: |
|
|
|
[tabs] |
|
====== |
|
application.properties:: |
|
+ |
|
[source,properties,role="primary"] |
|
---- |
|
spring.security.filter.dispatcher-types=request,async,error,forward,include |
|
---- |
|
====== |
|
|
|
If you are xref:servlet/configuration/java.adoc#_abstractsecuritywebapplicationinitializer[using the `AbstractSecurityWebApplicationInitializer`] you should override the `getSecurityDispatcherTypes` method and return all dispatcher types: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
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-framework-reference-url}/web.html#mvc-viewresolver[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 <<switch-filter-all-dispatcher-types,previous section>>, Spring Security 6.0 will apply authorization to `FORWARD` requests by default. |
|
|
|
Consider the following common configuration: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Controller |
|
public class MyController { |
|
|
|
@GetMapping("/login") |
|
public String login() { |
|
return "login"; |
|
} |
|
|
|
} |
|
---- |
|
====== |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.shouldFilterAllDispatcherTypes(true) |
|
.dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
shouldFilterAllDispatcherTypes = true |
|
authorize(DispatcherTypeRequestMatcher(DispatcherType.FORWARD), permitAll) |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 ``AccessDecisionManager``s |
|
|
|
Your application may have a custom {security-api-url}org/springframework/security/access/AccessDecisionManager.html[`AccessDecisionManager`] or {security-api-url}org/springframework/security/access/AccessDecisionVoter.html[`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 {security-api-url}org/springframework/security/access/vote/UnanimousBased.html[`UnanimousBased`], you should first adapt or replace any ``AccessDecisionVoter``s and then you can construct an `AuthorizationManager` like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
AuthorizationManager<RequestAuthorizationContext> requestAuthorization() { |
|
PolicyAuthorizationManager policy = ...; |
|
LocalAuthorizationManager local = ...; |
|
return AuthorizationManagers.allOf(policy, local); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun requestAuthorization(): AuthorizationManager<RequestAuthorizationContext> { |
|
val policy: PolicyAuthorizationManager = ... |
|
val local: LocalAuthorizationManager = ... |
|
return AuthorizationManagers.allOf(policy, local) |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(requestAuthorization)) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
authorize(anyRequest, requestAuthorization) |
|
} |
|
// ... |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<http authorization-manager-ref="requestAuthorization"/> |
|
---- |
|
====== |
|
|
|
[NOTE] |
|
==== |
|
`authorizeHttpRequests` is designed so that you can apply a custom `AuthorizationManager` to any url pattern. |
|
See xref:servlet/authorization/authorize-http-requests.adoc#custom-authorization-manager[the reference] for more details. |
|
==== |
|
|
|
==== I use `AffirmativeBased` |
|
|
|
If your application uses {security-api-url}org/springframework/security/access/vote/AffirmativeBased.html[`AffirmativeBased`], then you can construct an equivalent {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`], like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
AuthorizationManager<RequestAuthorizationContext> requestAuthorization() { |
|
PolicyAuthorizationManager policy = ...; |
|
LocalAuthorizationManager local = ...; |
|
return AuthorizationManagers.anyOf(policy, local); |
|
} |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun requestAuthorization(): AuthorizationManager<RequestAuthorizationContext> { |
|
val policy: PolicyAuthorizationManager = ... |
|
val local: LocalAuthorizationManager = ... |
|
return AuthorizationManagers.anyOf(policy, local) |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize.anyRequest().access(requestAuthorization)) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
authorize(anyRequest, requestAuthorization) |
|
} |
|
// ... |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<http authorization-manager-ref="requestAuthorization"/> |
|
---- |
|
====== |
|
|
|
[NOTE] |
|
==== |
|
`authorizeHttpRequests` is designed so that you can apply a custom `AuthorizationManager` to any url pattern. |
|
See xref:servlet/authorization/authorize-http-requests.adoc#custom-authorization-manager[the reference] for more details. |
|
==== |
|
|
|
==== I use `ConsensusBased` |
|
|
|
There is no framework-provided equivalent for {security-api-url}org/springframework/security/access/vote/ConsensusBased.html[`ConsensusBased`]. |
|
In that case, please implement a composite {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`] that takes the set of delegate ``AuthorizationManager``s into account. |
|
|
|
Once you have implemented `AuthorizationManager`, please follow the details in the reference manual for xref:servlet/authorization/authorize-http-requests.adoc#custom-authorization-manager[adding a custom `AuthorizationManager`]. |
|
|
|
==== I use a custom `AccessDecisionVoter` |
|
|
|
You should either change the class to implement {security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`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 {security-api-url}org/springframework/security/access/SecurityMetadataSource.html[`SecurityMetadataSource`] and {security-api-url}org/springframework/security/access/AccessDecisionVoter.html[`AccessDecisionVoter`] for `anyRequest().authenticated()` would look like: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
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 xref:servlet/authorization/authorize-http-requests.adoc#custom-authorization-manager[adding a custom `AuthorizationManager`]. |
|
|
|
[[replace-hasrole-hasauthority]] |
|
=== 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 |
|
[source,java] |
|
---- |
|
@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 |
|
[source,java] |
|
---- |
|
@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 https://github.com/spring-projects/spring-security/issues/13215[gh-13227] for more details. |
|
|
|
[[servlet-authorizationmanager-requests-opt-out]] |
|
=== 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: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.shouldFilterAllDispatcherTypes(true) |
|
.dispatcherTypeMatchers(FORWARD, INCLUDE).permitAll() |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
.anyRequest().denyAll() |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
shouldFilterAllDispatcherTypes = true |
|
authorize(DispatcherTypeRequestMatcher(FORWARD, INCLUDE), permitAll) |
|
authorize("/app/**", hasRole("APP")) |
|
// ... |
|
authorize(anyRequest, denyAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpRequests((authorize) -> authorize |
|
.filterAllDispatcherTypes(false) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
filterAllDispatcherTypes = false |
|
authorize("/messages/**", hasRole("APP")) |
|
// ... |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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`: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeRequests((authorize) -> authorize |
|
.filterSecurityInterceptorOncePerRequest(true) |
|
.mvcMatchers("/app/**").hasRole("APP") |
|
// ... |
|
) |
|
// ... |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeRequests { |
|
filterSecurityInterceptorOncePerRequest = true |
|
authorize("/messages/**", hasRole("APP")) |
|
// ... |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 {security-api-url}org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.AuthorizedUrl.html#permitAll()[`permitAll`] instead, like so: |
|
|
|
[tabs] |
|
====== |
|
Java:: |
|
+ |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.authorizeHttpReqeusts((authorize) -> authorize |
|
.mvcMatchers("/app/*").hasRole("APP") |
|
// ... |
|
.anyRequest().permitAll() |
|
) |
|
---- |
|
|
|
Kotlin:: |
|
+ |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
authorizeHttpRequests { |
|
authorize("/app*", hasRole("APP")) |
|
// ... |
|
authorize(anyRequest, permitAll) |
|
} |
|
} |
|
---- |
|
|
|
Xml:: |
|
+ |
|
[source,xml,role="secondary"] |
|
---- |
|
<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 |
|
[source,xml,role="secondary"] |
|
---- |
|
<http use-authorization-manager="false"> |
|
<intercept-url pattern="/app/*" access="hasRole('APP')"/> |
|
<!-- ... --> |
|
</http> |
|
----
|
|
|