mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-02-25 00:46:42 +00:00
1282 lines
36 KiB
Plaintext
1282 lines
36 KiB
Plaintext
[[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"/>
|
|
----
|
|
====
|
|
|
|
=== 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
|
|
}
|
|
}
|
|
----
|
|
====
|
|
|
|
'''
|
|
|
|
[[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>
|
|
----
|
|
====
|
|
|
|
[[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>
|
|
----
|
|
====
|
|
|
|
== 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)
|
|
----
|
|
====
|