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.
2777 lines
91 KiB
2777 lines
91 KiB
[[migration]] |
|
= Migrating to 6.0 |
|
|
|
The Spring Security team has prepared the 5.8 release to simplify upgrading to Spring Security 6.0. |
|
Use 5.8 and the steps below to minimize changes when |
|
ifdef::spring-security-version[] |
|
xref:6.0.0@migration.adoc[updating to 6.0] |
|
endif::[] |
|
ifndef::spring-security-version[] |
|
updating to 6.0 |
|
endif::[] |
|
. |
|
|
|
== Servlet |
|
|
|
=== Defer Loading CsrfToken |
|
|
|
In Spring Security 5, the default behavior is that the `CsrfToken` will be loaded on every request. |
|
This means that in a typical setup, the `HttpSession` must be read for every request even if it is unnecessary. |
|
|
|
In Spring Security 6, the default is that the lookup of the `CsrfToken` will be deferred until it is needed. |
|
|
|
To opt into the new Spring Security 6 default, the following configuration can be used. |
|
|
|
.Defer Loading `CsrfToken` |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { |
|
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); |
|
// set the name of the attribute the CsrfToken will be populated on |
|
requestHandler.setCsrfRequestAttributeName("_csrf"); |
|
http |
|
// ... |
|
.csrf((csrf) -> csrf |
|
.csrfTokenRequestHandler(requestHandler) |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: HttpSecurity): SecurityFilterChain { |
|
val requestHandler = CsrfTokenRequestAttributeHandler() |
|
// set the name of the attribute the CsrfToken will be populated on |
|
requestHandler.setCsrfRequestAttributeName("_csrf") |
|
http { |
|
csrf { |
|
csrfTokenRequestHandler = requestHandler |
|
} |
|
} |
|
return http.build() |
|
} |
|
---- |
|
|
|
.XML |
|
[source,xml,role="secondary"] |
|
---- |
|
<http> |
|
<!-- ... --> |
|
<csrf request-handler-ref="requestHandler"/> |
|
</http> |
|
<b:bean id="requestHandler" |
|
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler" |
|
p:csrfRequestAttributeName="_csrf"/> |
|
---- |
|
==== |
|
|
|
If this breaks your application, then you can explicitly opt into the 5.8 defaults using the following configuration: |
|
|
|
.Explicit Configure `CsrfToken` with 5.8 Defaults |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { |
|
CsrfTokenRequestAttributeHandler requestHandler = new CsrfTokenRequestAttributeHandler(); |
|
// set the name of the attribute the CsrfToken will be populated on |
|
requestHandler.setCsrfRequestAttributeName(null); |
|
http |
|
// ... |
|
.csrf((csrf) -> csrf |
|
.csrfTokenRequestHandler(requestHandler) |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: HttpSecurity): SecurityFilterChain { |
|
val requestHandler = CsrfTokenRequestAttributeHandler() |
|
// set the name of the attribute the CsrfToken will be populated on |
|
requestHandler.setCsrfRequestAttributeName(null) |
|
http { |
|
csrf { |
|
csrfTokenRequestHandler = requestHandler |
|
} |
|
} |
|
return http.build() |
|
} |
|
---- |
|
|
|
.XML |
|
[source,xml,role="secondary"] |
|
---- |
|
<http> |
|
<!-- ... --> |
|
<csrf request-handler-ref="requestHandler"/> |
|
</http> |
|
<b:bean id="requestHandler" |
|
class="org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler"> |
|
<b:property name="csrfRequestAttributeName"> |
|
<b:null/> |
|
</b:property> |
|
</b:bean> |
|
---- |
|
==== |
|
|
|
=== CSRF BREACH Protection |
|
|
|
If the steps for <<Defer Loading CsrfToken>> work for you, then you can also opt into Spring Security 6's default support for BREACH protection of the `CsrfToken` using the following configuration: |
|
|
|
.`CsrfToken` BREACH Protection |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
DefaultSecurityFilterChain springSecurity(HttpSecurity http) throws Exception { |
|
XorCsrfTokenRequestAttributeHandler requestHandler = new XorCsrfTokenRequestAttributeHandler(); |
|
// set the name of the attribute the CsrfToken will be populated on |
|
requestHandler.setCsrfRequestAttributeName("_csrf"); |
|
http |
|
// ... |
|
.csrf((csrf) -> csrf |
|
.csrfTokenRequestHandler(requestHandler) |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
open fun springSecurity(http: HttpSecurity): SecurityFilterChain { |
|
val requestHandler = XorCsrfTokenRequestAttributeHandler() |
|
// set the name of the attribute the CsrfToken will be populated on |
|
requestHandler.setCsrfRequestAttributeName("_csrf") |
|
http { |
|
csrf { |
|
csrfTokenRequestHandler = requestHandler |
|
} |
|
} |
|
return http.build() |
|
} |
|
---- |
|
|
|
.XML |
|
[source,xml,role="secondary"] |
|
---- |
|
<http> |
|
<!-- ... --> |
|
<csrf request-handler-ref="requestHandler"/> |
|
</http> |
|
<b:bean id="requestHandler" |
|
class="org.springframework.security.web.csrf.XorCsrfTokenRequestAttributeHandler" |
|
p:csrfRequestAttributeName="_csrf"/> |
|
---- |
|
==== |
|
|
|
=== Explicit Save SecurityContextRepository |
|
|
|
In Spring Security 5, the default behavior is for the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontext[`SecurityContext`] to automatically be saved to the xref:servlet/authentication/persistence.adoc#securitycontextrepository[`SecurityContextRepository`] using the xref:servlet/authentication/persistence.adoc#securitycontextpersistencefilter[`SecurityContextPersistenceFilter`]. |
|
Saving must be done just prior to the `HttpServletResponse` being committed and just before `SecurityContextPersistenceFilter`. |
|
Unfortunately, automatic persistence of the `SecurityContext` can surprise users when it is done prior to the request completing (i.e. just prior to committing the `HttpServletResponse`). |
|
It also is complex to keep track of the state to determine if a save is necessary causing unnecessary writes to the `SecurityContextRepository` (i.e. `HttpSession`) at times. |
|
|
|
In Spring Security 6, the default behavior is that the xref:servlet/authentication/persistence.adoc#securitycontextholderfilter[`SecurityContextHolderFilter`] will only read the `SecurityContext` from `SecurityContextRepository` and populate it in the `SecurityContextHolder`. |
|
Users now must explicitly save the `SecurityContext` with the `SecurityContextRepository` if they want the `SecurityContext` to persist between requests. |
|
This removes ambiguity and improves performance by only requiring writing to the `SecurityContextRepository` (i.e. `HttpSession`) when it is necessary. |
|
|
|
To opt into the new Spring Security 6 default, the following configuration can be used. |
|
|
|
include::partial$servlet/architecture/security-context-explicit.adoc[] |
|
|
|
[[requestcache-query-optimization]] |
|
=== Optimize Querying of `RequestCache` |
|
|
|
In Spring Security 5, the default behavior is to query the xref:servlet/architecture.adoc#savedrequests[saved request] on every request. |
|
This means that in a typical setup, that in order to use the xref:servlet/architecture.adoc#requestcache[`RequestCache`] the `HttpSession` is queried on every request. |
|
|
|
In Spring Security 6, the default is that `RequestCache` will only be queried for a cached request if the HTTP parameter `continue` is defined. |
|
This allows Spring Security to avoid unnecessarily reading the `HttpSession` with the `RequestCache`. |
|
|
|
In Spring Security 5 the default is to use `HttpSessionRequestCache` which will be queried for a cached request on every request. |
|
If you are not overriding the defaults (i.e. using `NullRequestCache`), then the following configuration can be used to explicitly opt into the Spring Security 6 behavior in Spring Security 5.8: |
|
|
|
include::partial$servlet/architecture/request-cache-continue.adoc[] |
|
|
|
=== 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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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"/> |
|
---- |
|
==== |
|
|
|
[[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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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. |
|
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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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 a custom `AfterInvocationManager` |
|
|
|
{security-api-url}org/springframework/security/authorization/AuthorizationManager.html[`AuthorizationManager`] replaces both {security-api-url}org/springframework/security/access/AccessDecisionManager.html[`AccessDecisionManager`] and {security-api-url}org/springframework/security/access/intercept/AfterInvocationManager.html[`AfterInvocationManager`]. |
|
The difference is that `AuthorizationManager<MethodInvocation>` replaces `AccessDecisionManager` and `AuthorizationManager<MethodInvocationResult>` replaces `AfterInvocationManager`. |
|
|
|
Given that, <<_i_use_a_custom_accessdecisionvoter,the same rules apply for adaptation>>, where the goal this time is to implement `AuthorizationManager<MethodInvocationResult>` instead of `AuthorizationManager<MethodInvocation>` and use `AuthorizationManagerAfterMethodInterceptor` instead of `AuthorizationManagerBeforeMethodInterceptor`. |
|
|
|
===== 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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { |
|
// ... |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
class WebSocketSecurityConfig: AbstractSecurityWebSocketMessageBrokerConfigurer() { |
|
// ... |
|
} |
|
---- |
|
==== |
|
|
|
changes to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer { |
|
// ... |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableWebSocketSecurity |
|
@Configuration |
|
class WebSocketSecurityConfig: WebSocketMessageBrokerConfigurer { |
|
// ... |
|
} |
|
---- |
|
==== |
|
|
|
[[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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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 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 |
|
[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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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 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, change: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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> |
|
---- |
|
==== |
|
|
|
==== 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: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
AuthorizationManager<RequestAuthorizationContext> requestAuthorization() { |
|
PolicyAuthorizationManager policy = ...; |
|
LocalAuthorizationManager local = ...; |
|
return AuthorizationMangers.allOf(policy, local); |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun requestAuthorization(): AuthorizationManager<RequestAuthorizationContext> { |
|
val policy: PolicyAuthorizationManager = ... |
|
val local: LocalAuthorizationManager = ... |
|
return AuthorizationMangers.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
AuthorizationManager<RequestAuthorizationContext> requestAuthorization() { |
|
PolicyAuthorizationManager policy = ...; |
|
LocalAuthorizationManager local = ...; |
|
return AuthorizationMangers.anyOf(policy, local); |
|
} |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@Bean |
|
fun requestAuthorization(): AuthorizationManager<RequestAuthorizationContext> { |
|
val policy: PolicyAuthorizationManager = ... |
|
val local: LocalAuthorizationManager = ... |
|
return AuthorizationMangers.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: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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`]. |
|
|
|
[[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: |
|
|
|
==== |
|
.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`: |
|
|
|
==== |
|
.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`: |
|
|
|
==== |
|
.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: |
|
|
|
==== |
|
.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> |
|
---- |
|
==== |
|
|
|
=== Propagate ``AuthenticationServiceException``s |
|
|
|
{security-api-url}org/springframework/security/web/authentication/AuthenticationFilter.html[`AuthenticationFilter`] propagates {security-api-url}org/springframework/security/authentication/AuthenticationServiceException.html[``AuthenticationServiceException``]s to the {security-api-url}org/springframework/security/authentication/AuthenticationEntryPoint.html[`AuthenticationEntryPoint`]. |
|
Because ``AuthenticationServiceException``s represent a server-side error instead of a client-side error, in 6.0, this changes to propagate them to the container. |
|
|
|
==== Configure `AuthenticationFailureHandler` to rethrow ``AuthenticationServiceException``s |
|
|
|
To prepare for the 6.0 default, wire `AuthenticationFilter` instances with a `AuthenticationFailureHandler` that rethrows ``AuthenticationServiceException``s, like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
AuthenticationFilter authenticationFilter = new AuthenticationFilter(...); |
|
AuthenticationEntryPointFailureHandler handler = new AuthenticationEntryPointFailureHandler(...); |
|
handler.setRethrowAuthenticationServiceException(true); |
|
authenticationFilter.setAuthenticationFailureHandler(handler); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val authenticationFilter: AuthenticationFilter = new AuthenticationFilter(...) |
|
val handler: AuthenticationEntryPointFailureHandler = new AuthenticationEntryPointFailureHandler(...) |
|
handler.setRethrowAuthenticationServiceException(true) |
|
authenticationFilter.setAuthenticationFailureHandler(handler) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<bean id="authenticationFilter" class="org.springframework.security.web.authentication.AuthenticationFilter"> |
|
<!-- ... --> |
|
<property ref="authenticationFailureHandler"/> |
|
</bean> |
|
|
|
<bean id="authenticationFailureHandler" class="org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler"> |
|
<property name="rethrowAuthenticationServiceException" value="true"/> |
|
</bean> |
|
---- |
|
==== |
|
|
|
[[servlet-authenticationfailurehandler-opt-out]] |
|
==== Opt-out Steps |
|
|
|
If rethrowing ``AuthenticationServiceException``s gives you trouble, you can set the value to false instead of taking the 6.0 default, like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
AuthenticationFilter authenticationFilter = new AuthenticationFilter(...); |
|
AuthenticationEntryPointFailureHandler handler = new AuthenticationEntryPointFailureHandler(...); |
|
handler.setRethrowAuthenticationServiceException(false); |
|
authenticationFilter.setAuthenticationFailureHandler(handler); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val authenticationFilter: AuthenticationFilter = new AuthenticationFilter(...) |
|
val handler: AuthenticationEntryPointFailureHandler = new AuthenticationEntryPointFailureHandler(...) |
|
handler.setRethrowAuthenticationServiceException(false) |
|
authenticationFilter.setAuthenticationFailureHandler(handler) |
|
---- |
|
|
|
.Xml |
|
[source,xml,role="secondary"] |
|
---- |
|
<bean id="authenticationFilter" class="org.springframework.security.web.authentication.AuthenticationFilter"> |
|
<!-- ... --> |
|
<property ref="authenticationFailureHandler"/> |
|
</bean> |
|
|
|
<bean id="authenticationFailureHandler" class="org.springframework.security.web.authentication.AuthenticationEntryPointFailureHandler"> |
|
<property name="rethrowAuthenticationServiceException" value="false"/> |
|
</bean> |
|
---- |
|
==== |
|
|
|
[[servlet-opt-in-sha256-rememberme]] |
|
=== Use SHA-256 in Remember Me |
|
|
|
The `TokenBasedRememberMeServices` implementation now supports SHA-256 for the Remember Me token and this is the default in Spring Security 6. |
|
This change makes the implementation more secure by default since MD5 is already proven to be a weak hashing algorithm and vulnerable against collision attacks and modular differential attacks. |
|
|
|
The new generated tokens now have the information of which algorithm was used to generate the token and that information is used in order to match it. |
|
If the algorithm name is not present, then the `matchingAlgorithm` property is used to check the token. |
|
This allows for a smooth transition from MD5 to SHA-256. |
|
|
|
To opt into the new Spring Security 6 default to encode the tokens while still being able to decode tokens encoded with MD5, you can set the `encodingAlgorithm` property to SHA-256 and the `matchingAlgorithm` property to MD5. |
|
See the xref:servlet/authentication/rememberme.adoc#_tokenbasedremembermeservices[reference documentation] and the {security-api-url}org/springframework/security/web/authentication/rememberme/TokenBasedRememberMeServices.html[API docs] for more information. |
|
|
|
[[servlet-opt-in-sha256-sha256-encoding]] |
|
.Use Spring Security 6 defaults for encoding, SHA-256 for encoding and MD5 for matching |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception { |
|
http |
|
// ... |
|
.rememberMe((remember) -> remember |
|
.rememberMeServices(rememberMeServices) |
|
); |
|
return http.build(); |
|
} |
|
|
|
@Bean |
|
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { |
|
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256; |
|
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm); |
|
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5); |
|
return rememberMe; |
|
} |
|
|
|
} |
|
---- |
|
|
|
.XML |
|
[source,xml,role="secondary"] |
|
---- |
|
<http> |
|
<remember-me services-ref="rememberMeServices"/> |
|
</http> |
|
|
|
<bean id="rememberMeServices" class= |
|
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices"> |
|
<property name="userDetailsService" ref="myUserDetailsService"/> |
|
<property name="key" value="springRocks"/> |
|
<property name="matchingAlgorithm" value="MD5"/> |
|
<property name="encodingAlgorithm" value="SHA256"/> |
|
</bean> |
|
---- |
|
==== |
|
|
|
At some point, you will want to fully migrate to Spring Security 6 defaults. But how do you know when it is safe to do so? |
|
Let's suppose that you deployed your application using SHA-256 as the encoding algorithm (as you have done <<servlet-opt-in-sha256-sha256-encoding,here>>) on November 1st, if you have the value for the `tokenValiditySeconds` property set to N days (14 is the default), you can migrate to SHA-256 N days after November 1st (which is November 15th in this example). |
|
By that time, all the tokens generated with MD5 will have expired. |
|
|
|
.Use Spring Security 6 defaults, SHA-256 for both encoding and matching |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception { |
|
http |
|
// ... |
|
.rememberMe((remember) -> remember |
|
.rememberMeServices(rememberMeServices) |
|
); |
|
return http.build(); |
|
} |
|
|
|
@Bean |
|
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { |
|
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.SHA256; |
|
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm); |
|
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.SHA256); |
|
return rememberMe; |
|
} |
|
|
|
} |
|
---- |
|
|
|
.XML |
|
[source,xml,role="secondary"] |
|
---- |
|
<http> |
|
<remember-me services-ref="rememberMeServices"/> |
|
</http> |
|
|
|
<bean id="rememberMeServices" class= |
|
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices"> |
|
<property name="userDetailsService" ref="myUserDetailsService"/> |
|
<property name="key" value="springRocks"/> |
|
<property name="matchingAlgorithm" value="SHA256"/> |
|
<property name="encodingAlgorithm" value="SHA256"/> |
|
</bean> |
|
---- |
|
==== |
|
|
|
If you are having problems with the Spring Security 6 defaults, you can explicitly opt into 5.8 defaults using the following configuration: |
|
|
|
.Use MD5 for both encoding and matching algorithms |
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http, RememberMeServices rememberMeServices) throws Exception { |
|
http |
|
// ... |
|
.rememberMe((remember) -> remember |
|
.rememberMeServices(rememberMeServices) |
|
); |
|
return http.build(); |
|
} |
|
|
|
@Bean |
|
RememberMeServices rememberMeServices(UserDetailsService userDetailsService) { |
|
RememberMeTokenAlgorithm encodingAlgorithm = RememberMeTokenAlgorithm.MD5; |
|
TokenBasedRememberMeServices rememberMe = new TokenBasedRememberMeServices(myKey, userDetailsService, encodingAlgorithm); |
|
rememberMe.setMatchingAlgorithm(RememberMeTokenAlgorithm.MD5); |
|
return rememberMe; |
|
} |
|
|
|
} |
|
---- |
|
|
|
.XML |
|
[source,xml,role="secondary"] |
|
---- |
|
<http> |
|
<remember-me services-ref="rememberMeServices"/> |
|
</http> |
|
|
|
<bean id="rememberMeServices" class= |
|
"org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices"> |
|
<property name="userDetailsService" ref="myUserDetailsService"/> |
|
<property name="key" value="springRocks"/> |
|
<property name="matchingAlgorithm" value="MD5"/> |
|
<property name="encodingAlgorithm" value="MD5"/> |
|
</bean> |
|
---- |
|
==== |
|
|
|
=== Stop Using SAML 2.0 `Converter` constructors |
|
|
|
In an early release of Spring Security's SAML 2.0 support, `Saml2MetadataFilter` and `Saml2AuthenticationTokenConverter` shipped with constructors of type `Converter`. |
|
This level of abstraction made it tricky to evolve the class and so a dedicated interface `RelyingPartyRegistrationResolver` was introduced in a later release. |
|
|
|
In 6.0, the `Converter` constructors are removed. |
|
To prepare for this in 5.8, change classes that implement `Converter<HttpServletRequest, RelyingPartyRegistration>` to instead implement `RelyingPartyRegistrationResolver`. |
|
|
|
=== Change to Using `Saml2AuthenticationRequestResolver` |
|
|
|
`Saml2AuthenticationContextResolver` and `Saml2AuthenticationRequestFactory` are removed in 6.0 as is the `Saml2WebSsoAuthenticationRequestFilter` that requires them. |
|
They are replaced by `Saml2AuthenticationRequestResolver` and a new constructor in `Saml2WebSsoAuthenticationRequestFilter`. |
|
The new interface removes an unnecessary transport object between the two classes. |
|
|
|
Most applications need do nothing; however, if you use or configure `Saml2AuthenticationRequestContextResolver` or `Saml2AuthenticationRequestFactory`, try the following steps to convert instead use `Saml2AuthenticationRequestResolver`. |
|
|
|
==== Use `setAuthnRequestCustomizer` instead of `setAuthenticationRequestContextConverter` |
|
|
|
If you are calling `OpenSaml4AuthenticationReqeustFactory#setAuthenticationRequestContextConverter`, for example, like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
Saml2AuthenticationRequestFactory authenticationRequestFactory() { |
|
OpenSaml4AuthenticationRequestFactory factory = new OpenSaml4AuthenticationRequestFactory(); |
|
factory.setAuthenticationRequestContextConverter((context) -> { |
|
AuthnRequestBuilder authnRequestBuilder = ConfigurationService.get(XMLObjectProviderRegistry.class) |
|
.getBuilderFactory().getBuilder(AuthnRequest.DEFAULT_ELEMENT_NAME); |
|
IssuerBuilder issuerBuilder = ConfigurationService.get(XMLObjectProviderRegistry.class) |
|
.getBuilderFactory().getBuilder(Issuer.DEFAULT_ELEMENT_NAME); |
|
tring issuer = context.getIssuer(); |
|
String destination = context.getDestination(); |
|
String assertionConsumerServiceUrl = context.getAssertionConsumerServiceUrl(); |
|
String protocolBinding = context.getRelyingPartyRegistration().getAssertionConsumerServiceBinding().getUrn(); |
|
AuthnRequest auth = authnRequestBuilder.buildObject(); |
|
auth.setID("ARQ" + UUID.randomUUID().toString().substring(1)); |
|
auth.setIssueInstant(Instant.now()); |
|
auth.setForceAuthn(Boolean.TRUE); |
|
auth.setIsPassive(Boolean.FALSE); |
|
auth.setProtocolBinding(SAMLConstants.SAML2_POST_BINDING_URI); |
|
Issuer iss = issuerBuilder.buildObject(); |
|
iss.setValue(issuer); |
|
auth.setIssuer(iss); |
|
auth.setDestination(destination); |
|
auth.setAssertionConsumerServiceURL(assertionConsumerServiceUrl); |
|
}); |
|
return factory; |
|
} |
|
---- |
|
==== |
|
|
|
to ensure that ForceAuthn is set to `true`, you can instead do: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistrationResolver registrations) { |
|
OpenSaml4AuthenticationRequestResolver reaolver = new OpenSaml4AuthenticationRequestResolver(registrations); |
|
resolver.setAuthnRequestCustomizer((context) -> context.getAuthnRequest().setForceAuthn(Boolean.TRUE)); |
|
return resolver; |
|
} |
|
---- |
|
==== |
|
|
|
Also, since `setAuthnRequestCustomizer` has direct access to the `HttpServletRequest`, there is no need for a `Saml2AuthenticationRequestContextResolver`. |
|
Simply use `setAuthnRequestCustomizer` to read directly from `HttpServletRequest` this information you need. |
|
|
|
==== Use `setAuthnRequestCustomizer` instead of `setProtocolBinding` |
|
|
|
Instead of doing: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
Saml2AuthenticationRequestFactory authenticationRequestFactory() { |
|
OpenSaml4AuthenticationRequestFactory factory = new OpenSaml4AuthenticationRequestFactory(); |
|
factory.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST") |
|
return factory; |
|
} |
|
---- |
|
==== |
|
|
|
you can do: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
Saml2AuthenticationRequestResolver authenticationRequestResolver() { |
|
OpenSaml4AuthenticationRequestResolver reaolver = new OpenSaml4AuthenticationRequestResolver(registrations); |
|
resolver.setAuthnRequestCustomizer((context) -> context.getAuthnRequest() |
|
.setProtocolBinding("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")); |
|
return resolver; |
|
} |
|
---- |
|
==== |
|
|
|
[NOTE] |
|
==== |
|
Since Spring Security only supports the `POST` binding for authentication, there is not very much value in overriding the protocol binding at this point in time. |
|
==== |
|
|
|
=== Use the latest `Saml2AuthenticationToken` constructor |
|
|
|
In an early release, `Saml2AuthenticationToken` took several individual settings as constructor parameters. |
|
This created a challenge each time a new parameter needed to be added. |
|
Since most of these settings were part of `RelyingPartyRegistration`, a new constructor was added where a `RelyingPartyRegistration` could be provided, making the constructor more stable. |
|
It also is valuable in that it more closely aligns with the design of `OAuth2LoginAuthenticationToken`. |
|
|
|
Most applications do not construct this class directly since `Saml2WebSsoAuthenticationFilter` does. |
|
However, in the event that your application constructs one, please change from: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
new Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(), |
|
registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials()) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
Saml2AuthenticationToken(saml2Response, registration.getSingleSignOnServiceLocation(), |
|
registration.getAssertingParty().getEntityId(), registration.getEntityId(), registration.getCredentials()) |
|
---- |
|
==== |
|
|
|
to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
new Saml2AuthenticationToken(saml2Response, registration) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
Saml2AuthenticationToken(saml2Response, registration) |
|
---- |
|
==== |
|
|
|
=== Use `RelyingPartyRegistration` updated methods |
|
|
|
In an early release of Spring Security's SAML support, there was some ambiguity on the meaning of certain `RelyingPartyRegistration` methods and their function. |
|
As more capabilities were added to `RelyingPartyRegistration`, it became necessary to clarify this ambiguity by changing method names to ones that aligned with spec language. |
|
|
|
The deprecated methods in `RelyingPartyRegstration` are removed. |
|
To prepare for that, consider the following representative usage of `RelyingPartyRegistration`: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
String idpEntityId = registration.getRemoteIdpEntityId(); |
|
String assertionConsumerServiceUrl = registration.getAssertionConsumerServiceUrlTemplate(); |
|
String idpWebSsoUrl = registration.getIdpWebSsoUrl(); |
|
String localEntityId = registration.getLocalEntityIdTemplate(); |
|
List<Saml2X509Credential> verifying = registration.getCredentials().stream() |
|
.filter(Saml2X509Credential::isSignatureVerficationCredential) |
|
.collect(Collectors.toList()); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val idpEntityId: String = registration.getRemoteIdpEntityId() |
|
val assertionConsumerServiceUrl: String = registration.getAssertionConsumerServiceUrlTemplate() |
|
val idpWebSsoUrl: String = registration.getIdpWebSsoUrl() |
|
val localEntityId: String = registration.getLocalEntityIdTemplate() |
|
val verifying: List<Saml2X509Credential> = registration.getCredentials() |
|
.filter(Saml2X509Credential::isSignatureVerficationCredential) |
|
---- |
|
==== |
|
|
|
This should change to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
String assertingPartyEntityId = registration.getAssertingPartyDetails().getEntityId(); |
|
String assertionConsumerServiceLocation = registration.getAssertionConsumerServiceLocation(); |
|
String singleSignOnServiceLocation = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation(); |
|
String entityId = registration.getEntityId(); |
|
List<Saml2X509Credential> verifying = registration.getAssertingPartyDetails().getVerificationX509Credentials(); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val assertingPartyEntityId: String = registration.getAssertingPartyDetails().getEntityId() |
|
val assertionConsumerServiceLocation: String = registration.getAssertionConsumerServiceLocation() |
|
val singleSignOnServiceLocation: String = registration.getAssertingPartyDetails().getSingleSignOnServiceLocation() |
|
val entityId: String = registration.getEntityId() |
|
val verifying: List<Saml2X509Credential> = registration.getAssertingPartyDetails().getVerificationX509Credentials() |
|
---- |
|
==== |
|
|
|
For a complete listing of all changed methods, please see {security-api-url}org/springframework/security/saml2/provider/service/registration/RelyingPartyRegistration.html[``RelyingPartyRegistration``'s JavaDoc]. |
|
|
|
=== Use OpenSAML 4 |
|
|
|
OpenSAML 3 has reached its end-of-life. |
|
As such, Spring Security 6 drops support for it, bumping up its OpenSAML baseline to 4. |
|
|
|
To prepare for the upgrade, update your pom to depend on OpenSAML 4 instead of 3: |
|
|
|
==== |
|
.Maven |
|
[source,maven,role="primary"] |
|
---- |
|
<dependencyManagement> |
|
<dependency> |
|
<groupId>org.opensaml</groupId> |
|
<artifactId>opensaml-core</artifactId> |
|
<version>4.2.1</version> |
|
</dependency> |
|
<dependency> |
|
<groupId>org.opensaml</groupId> |
|
<artifactId>opensaml-saml-api</artifactId> |
|
<version>4.2.1</version> |
|
</dependency> |
|
<dependency> |
|
<groupId>org.opensaml</groupId> |
|
<artifactId>opensaml-saml-impl</artifactId> |
|
<version>4.2.1</version> |
|
</dependency> |
|
</dependencyManagement> |
|
---- |
|
|
|
.Gradle |
|
[source,gradle,role="secondary"] |
|
---- |
|
dependencies { |
|
constraints { |
|
api "org.opensaml:opensaml-core:4.2.1" |
|
api "org.opensaml:opensaml-saml-api:4.2.1" |
|
api "org.opensaml:opensaml-saml-impl:4.2.1" |
|
} |
|
} |
|
---- |
|
==== |
|
|
|
You must use at least OpenSAML 4.1.1 to update to Spring Security 6's SAML support. |
|
|
|
=== Use `OpenSaml4AuthenticationProvider` |
|
|
|
In order to support both OpenSAML 3 and 4 at the same time, Spring Security released `OpenSamlAuthenticationProvider` and `OpenSaml4AuthenticationProvider`. |
|
In 6.0, because OpenSAML3 support is removed, `OpenSamlAuthenticationProvider` is removed as well. |
|
|
|
Not all methods in `OpenSamlAuthenticationProvider` were ported 1-to-1 to `OpenSaml4AuthenticationProvider`. |
|
As such, some adjustment will be required to make the challenge. |
|
|
|
Consider the following representative usage of `OpenSamlAuthenticationProvider`: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
OpenSamlAuthenticationProvider versionThree = new OpenSamlAuthenticationProvider(); |
|
versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor); |
|
versionThree.setResponseTimeValidationSkew(myDuration); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val versionThree: OpenSamlAuthenticationProvider = OpenSamlAuthenticationProvider() |
|
versionThree.setAuthoritiesExtractor(myAuthoritiesExtractor) |
|
versionThree.setResponseTimeValidationSkew(myDuration) |
|
---- |
|
==== |
|
|
|
This should change to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
Converter<ResponseToken, Saml2Authentication> delegate = OpenSaml4AuthenticationProvider |
|
.createDefaultResponseAuthenticationConverter(); |
|
OpenSaml4AuthenticationProvider versionFour = new OpenSaml4AuthenticationProvider(); |
|
versionFour.setResponseAuthenticationConverter((responseToken) -> { |
|
Saml2Authentication authentication = delegate.convert(responseToken); |
|
Assertion assertion = responseToken.getResponse().getAssertions().get(0); |
|
AuthenticatedPrincipal principal = (AuthenticatedPrincipal) authentication.getPrincipal(); |
|
Collection<GrantedAuthority> authorities = myAuthoritiesExtractor.convert(assertion); |
|
return new Saml2Authentication(principal, authentication.getSaml2Response(), authorities); |
|
}); |
|
Converter<AssertionToken, Saml2ResponseValidationResult> validator = OpenSaml4AuthenticationProvider |
|
.createDefaultAssertionValidatorWithParameters((p) -> p.put(CLOCK_SKEW, myDuration)); |
|
versionFour.setAssertionValidator(validator); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val delegate = OpenSaml4AuthenticationProvider.createDefaultResponseAuthenticationConverter() |
|
val versionFour = OpenSaml4AuthenticationProvider() |
|
versionFour.setResponseAuthenticationConverter({ |
|
responseToken -> { |
|
val authentication = delegate.convert(responseToken) |
|
val assertion = responseToken.getResponse().getAssertions().get(0) |
|
val principal = (AuthenticatedPrincipal) authentication.getPrincipal() |
|
val authorities = myAuthoritiesExtractor.convert(assertion) |
|
return Saml2Authentication(principal, authentication.getSaml2Response(), authorities) |
|
} |
|
}) |
|
val validator = OpenSaml4AuthenticationProvider |
|
.createDefaultAssertionValidatorWithParameters({ p -> p.put(CLOCK_SKEW, myDuration) }) |
|
versionFour.setAssertionValidator(validator) |
|
---- |
|
==== |
|
|
|
[[use-new-requestmatchers]] |
|
=== Use the new `requestMatchers` methods |
|
|
|
In Spring Security 5.8, the {security-api-url}org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.html#antMatchers(java.lang.String...)[`antMatchers`], {security-api-url}org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.html#mvcMatchers(java.lang.String...)`mvcMatchers`, and {security-api-url}org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.html#regexMatchers(java.lang.String...)[`regexMatchers`] methods were deprecated in favor of new xref::servlet/authorization/authorize-http-requests.adoc#_request_matchers[`requestMatchers` methods]. |
|
|
|
The new `requestMatchers` methods were added xref::servlet/authorization/authorize-http-requests.adoc[to `authorizeHttpRequests`], `authorizeRequests`, CSRF configuration, `WebSecurityCustomizer` and any other places that had the specialized `RequestMatcher` methods. |
|
The deprecated methods are removed in Spring Security 6. |
|
|
|
These new methods have more secure defaults since they choose the most appropriate `RequestMatcher` implementation for your application. |
|
In summary, the new methods choose the `MvcRequestMatcher` implementation if your application has Spring MVC in the classpath, falling back to the `AntPathRequestMatcher` implementation if Spring MVC is not present (aligning the behavior with the Kotlin equivalent methods). |
|
|
|
To start using the new methods, you can replace the deprecated methods with the new ones. For example, the following application configuration: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
|
http |
|
.authorizeHttpRequests((authz) -> authz |
|
.antMatchers("/api/admin/**").hasRole("ADMIN") |
|
.antMatchers("/api/user/**").hasRole("USER") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
} |
|
---- |
|
==== |
|
|
|
can be changed to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
|
http |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/api/admin/**").hasRole("ADMIN") |
|
.requestMatchers("/api/user/**").hasRole("USER") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
} |
|
---- |
|
==== |
|
|
|
If you have Spring MVC in the classpath and are using the `mvcMatchers` methods, you can replace it with the new methods and Spring Security will choose the `MvcRequestMatcher` implementation for you. |
|
The following configuration: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
@EnableWebMvc |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
|
http |
|
.authorizeHttpRequests((authz) -> authz |
|
.mvcMatchers("/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
} |
|
---- |
|
==== |
|
|
|
is equivalent to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
@EnableWebMvc |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
|
http |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
} |
|
---- |
|
==== |
|
|
|
If you are customizing the `servletPath` property of the `MvcRequestMatcher`, you can now use the `MvcRequestMatcher.Builder` to create `MvcRequestMatcher` instances that share the same servlet path: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
@EnableWebMvc |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
|
http |
|
.authorizeHttpRequests((authz) -> authz |
|
.mvcMatchers("/admin").servletPath("/path").hasRole("ADMIN") |
|
.mvcMatchers("/user").servletPath("/path").hasRole("USER") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
} |
|
---- |
|
==== |
|
|
|
The code above can be rewritten using the `MvcRequestMatcher.Builder` and the `requestMatchers` method: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Configuration |
|
@EnableWebSecurity |
|
@EnableWebMvc |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { |
|
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/path"); |
|
http |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN") |
|
.requestMatchers(mvcMatcherBuilder.pattern("/user")).hasRole("USER") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
} |
|
---- |
|
==== |
|
|
|
If you are having problem with the new `requestMatchers` methods, you can always switch back to the `RequestMatcher` implementation that you were using. |
|
For example, if you still want to use `AntPathRequestMatcher` and `RegexRequestMatcher` implementations, you can use the `requestMatchers` method that accepts a `RequestMatcher` instance: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; |
|
import static org.springframework.security.web.util.matcher.RegexRequestMatcher.regexMatcher; |
|
|
|
@Configuration |
|
@EnableWebSecurity |
|
public class SecurityConfig { |
|
|
|
@Bean |
|
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { |
|
http |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers(antMatcher("/user/**")).hasRole("USER") |
|
.requestMatchers(antMatcher(HttpMethod.POST, "/user/**")).hasRole("ADMIN") |
|
.requestMatchers(regexMatcher(".*\\?x=y")).hasRole("SPECIAL") // matches /any/path?x=y |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
} |
|
---- |
|
==== |
|
|
|
Note that the above sample uses static factory methods from {security-api-url}org/springframework/security/web/util/matcher/AntPathRequestMatcher.html[`AntPathRequestMatcher`] and {security-api-url}org/springframework/security/web/util/matcher/RegexRequestMatcher.html[`RegexRequestMatcher`] to improve readability. |
|
|
|
If you are using the `WebSecurityCustomizer` interface, you can replace the deprecated `antMatchers` methods: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public WebSecurityCustomizer webSecurityCustomizer() { |
|
return (web) -> web.ignoring().antMatchers("/ignore1", "/ignore2"); |
|
} |
|
---- |
|
==== |
|
|
|
with their `requestMatchers` counterparts: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public WebSecurityCustomizer webSecurityCustomizer() { |
|
return (web) -> web.ignoring().requestMatchers("/ignore1", "/ignore2"); |
|
} |
|
---- |
|
==== |
|
|
|
The same way, if you are customizing the CSRF configuration to ignore some paths, you can replace the deprecated methods with the `requestMatchers` methods: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.csrf((csrf) -> csrf |
|
.ignoringAntMatchers("/no-csrf") |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
==== |
|
|
|
can be changed to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.csrf((csrf) -> csrf |
|
.ignoringRequestMatchers("/no-csrf") |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
==== |
|
|
|
[[use-new-security-matchers]] |
|
=== Use the new `securityMatchers` methods |
|
|
|
In Spring Security 5.8, the `antMatchers`, `mvcMatchers` and `requestMatchers` methods from `HttpSecurity` were deprecated in favor of new `securityMatchers` methods. |
|
|
|
Note that these methods are not the same from `authorizeHttpRequests` methods <<use-new-requestmatchers,which were deprecated>> in favor of the `requestMatchers` methods. |
|
However, the `securityMatchers` methods are similar to the `requestMatchers` methods in the sense that they will choose the most appropriate `RequestMatcher` implementation for your application. |
|
In summary, the new methods choose the `MvcRequestMatcher` implementation if your application has Spring MVC in the classpath, falling back to the `AntPathRequestMatcher` implementation if Spring MVC is not present (aligning the behavior with the Kotlin equivalent methods). |
|
Another reason for adding the `securityMatchers` methods is to avoid confusion with the `requestMatchers` methods from `authorizeHttpRequests`. |
|
|
|
The following configuration: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.antMatcher("/api/**", "/app/**") |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/api/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
==== |
|
|
|
can be rewritten using the `securityMatchers` methods: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.securityMatcher("/api/**", "/app/**") |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/api/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
==== |
|
|
|
If you are using a custom `RequestMatcher` in your `HttpSecurity` configuration: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.requestMatcher(new MyCustomRequestMatcher()) |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/api/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
public class MyCustomRequestMatcher implements RequestMatcher { |
|
// ... |
|
} |
|
---- |
|
==== |
|
|
|
you can do the same using `securityMatcher`: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.securityMatcher(new MyCustomRequestMatcher()) |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/api/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
|
|
public class MyCustomRequestMatcher implements RequestMatcher { |
|
// ... |
|
} |
|
---- |
|
==== |
|
|
|
If you are combining multiple `RequestMatcher` implementations in your `HttpSecurity` configuration: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.requestMatchers((matchers) -> matchers |
|
.antMatchers("/api/**", "/app/**") |
|
.mvcMatchers("/admin/**") |
|
.requestMatchers(new MyCustomRequestMatcher()) |
|
) |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
==== |
|
|
|
you can change it by using `securityMatchers`: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.securityMatchers((matchers) -> matchers |
|
.requestMatchers("/api/**", "/app/**", "/admin/**") |
|
.requestMatchers(new MyCustomRequestMatcher()) |
|
) |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers("/admin/**").hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
==== |
|
|
|
If you are having problems with the `securityMatchers` methods choosing the `RequestMatcher` implementation for you, you can always choose the `RequestMatcher` implementation yourself: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; |
|
|
|
@Bean |
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
|
http |
|
.securityMatcher(antMatcher("/api/**"), antMatcher("/app/**")) |
|
.authorizeHttpRequests((authz) -> authz |
|
.requestMatchers(antMatcher("/api/admin/**")).hasRole("ADMIN") |
|
.anyRequest().authenticated() |
|
); |
|
return http.build(); |
|
} |
|
---- |
|
==== |
|
|
|
== Reactive |
|
|
|
=== Use `AuthorizationManager` for Method Security |
|
|
|
xref:reactive/authorization/method.adoc[Method Security] has been xref:reactive/authorization/method.adoc#jc-enable-reactive-method-security-authorization-manager[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 |
|
<<reactive-authorizationmanager-methods-opt-out,opt out steps>> at the end of this section. |
|
|
|
In Spring Security 5.8, `useAuthorizationManager` was added to {security-api-url}org/springframework/security/config/annotation/method/configuration/EnableReactiveMethodSecurity.html[`@EnableReactiveMethodSecurity`] to allow applications to opt in to ``AuthorizationManager``'s features. |
|
|
|
[[reactive-change-to-useauthorizationmanager]] |
|
==== Change `useAuthorizationManager` to `true` |
|
|
|
To opt in, change `useAuthorizationManager` to `true` like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableReactiveMethodSecurity |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableReactiveMethodSecurity |
|
---- |
|
==== |
|
|
|
changes to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableReactiveMethodSecurity(useAuthorizationManager = true) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableReactiveMethodSecurity(useAuthorizationManager = true) |
|
---- |
|
==== |
|
|
|
[[reactive-check-for-annotationconfigurationexceptions]] |
|
==== Check for ``AnnotationConfigurationException``s |
|
|
|
`useAuthorizationManager` activates stricter enforcement of Spring Security's non-repeatable or otherwise incompatible annotations. |
|
If after turning on `useAuthorizationManager` you see ``AnnotationConfigurationException``s in your logs, follow the instructions in the exception message to clean up your application's method security annotation usage. |
|
|
|
[[reactive-authorizationmanager-methods-opt-out]] |
|
==== Opt-out Steps |
|
|
|
If you ran into trouble with `AuthorizationManager` for reactive method security, you can opt out by changing: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableReactiveMethodSecurity |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableReactiveMethodSecurity |
|
---- |
|
==== |
|
|
|
to: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
@EnableReactiveMethodSecurity(useAuthorizationManager = false) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
@EnableReactiveMethodSecurity(useAuthorizationManager = false) |
|
---- |
|
==== |
|
|
|
=== Propagate ``AuthenticationServiceException``s |
|
|
|
{security-api-url}org/springframework/security/web/server/Webauthentication/AuthenticationWebFilter.html[`AuthenticationFilter`] propagates {security-api-url}org/springframework/security/authentication/AuthenticationServiceException.html[``AuthenticationServiceException``]s to the {security-api-url}org/springframework/security/web/server/ServerAuthenticationEntryPoint.html[`ServerAuthenticationEntryPoint`]. |
|
Because ``AuthenticationServiceException``s represent a server-side error instead of a client-side error, in 6.0, this changes to propagate them to the container. |
|
|
|
==== Configure `ServerAuthenticationFailureHandler` to rethrow ``AuthenticationServiceException``s |
|
|
|
To prepare for the 6.0 default, `httpBasic` and `oauth2ResourceServer` should be configured to rethrow ``AuthenticationServiceException``s. |
|
|
|
For each, construct the appropriate authentication entry point for `httpBasic` and for `oauth2ResourceServer`: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
ServerAuthenticationEntryPoint bearerEntryPoint = new BearerTokenServerAuthenticationEntryPoint(); |
|
ServerAuthenticationEntryPoint basicEntryPoint = new HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED); |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val bearerEntryPoint: ServerAuthenticationEntryPoint = BearerTokenServerAuthenticationEntryPoint() |
|
val basicEntryPoint: ServerAuthenticationEntryPoint = HttpStatusServerEntryPoint(HttpStatus.UNAUTHORIZED) |
|
---- |
|
==== |
|
|
|
[NOTE] |
|
==== |
|
If you use a custom `AuthenticationEntryPoint` for either or both mechanisms, use that one instead for the remaining steps. |
|
==== |
|
|
|
Then, construct and configure a `ServerAuthenticationEntryPointFailureHandler` for each one: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
AuthenticationFailureHandler bearerFailureHandler = new ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint); |
|
bearerFailureHandler.setRethrowAuthenticationServiceException(true); |
|
AuthenticationFailureHandler basicFailureHandler = new ServerAuthenticationEntryPointFailureHandler(basicEntryPoint); |
|
basicFailureHandler.setRethrowAuthenticationServiceException(true) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
val bearerFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(bearerEntryPoint) |
|
bearerFailureHandler.setRethrowAuthenticationServiceException(true) |
|
val basicFailureHandler: AuthenticationFailureHandler = ServerAuthenticationEntryPointFailureHandler(basicEntryPoint) |
|
basicFailureHandler.setRethrowAuthenticationServiceException(true) |
|
---- |
|
==== |
|
|
|
Finally, wire each authentication failure handler into the DSL, like so: |
|
|
|
==== |
|
.Java |
|
[source,java,role="primary"] |
|
---- |
|
http |
|
.httpBasic((basic) -> basic.authenticationFailureHandler(basicFailureHandler)) |
|
.oauth2ResourceServer((oauth2) -> oauth2.authenticationFailureHandler(bearerFailureHandler)) |
|
---- |
|
|
|
.Kotlin |
|
[source,kotlin,role="secondary"] |
|
---- |
|
http { |
|
httpBasic { |
|
authenticationFailureHandler = basicFailureHandler |
|
} |
|
oauth2ResourceServer { |
|
authenticationFailureHandler = bearerFailureHandler |
|
} |
|
} |
|
---- |
|
==== |
|
|
|
[[reactive-authenticationfailurehandler-opt-out]] |
|
==== Opt-out Steps |
|
|
|
To opt-out of the 6.0 defaults and instead continue to pass `AuthenticationServiceException` on to ``ServerAuthenticationEntryPoint``s, you can follow the same steps as above, except set `rethrowAuthenticationServiceException` to false.
|
|
|