1
0
mirror of synced 2026-08-04 17:27:13 +00:00

Add AuthorizationManager to Messaging

Closes gh-11076
This commit is contained in:
Josh Cummings
2022-04-07 17:39:10 -06:00
parent bbff945b95
commit f4c0fcb5ef
33 changed files with 2801 additions and 82 deletions
@@ -38,6 +38,12 @@ If not specified, Spring Security will automatically integrate with the messagin
* **same-origin-disabled** Disables the requirement for CSRF token to be present in the Stomp headers (default false).
Changing the default is useful if it is necessary to allow other origins to make SockJS connections.
[[nsa-websocket-message-broker-authorization-manager-ref]]
* **authorization-manager-ref** Use this `AuthorizationManager` instance; when set, `use-authorization-manager` is ignored and assumed to be `true`
[[nsa-websocket-message-broker-use-authorization-manager]]
* **use-authorization-manager** Uses legacy `SecurityMetadataSource` API instead of `AuthorizationManager` API (default false).
[[nsa-websocket-message-broker-children]]
=== Child Elements of <websocket-message-broker>
@@ -11,23 +11,39 @@ This is because the format is unknown, and there is https://docs.spring.io/sprin
Additionally, JSR-356 does not provide a way to intercept messages, so security would be invasive.
****
[[websocket-authentication]]
== WebSocket Authentication
WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.
This means that the `Principal` on the `HttpServletRequest` will be handed off to WebSockets.
If you are using Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically.
More concretely, to ensure a user has authenticated to your WebSocket application, all that is necessary is to ensure that you setup Spring Security to authenticate your HTTP based web application.
[[websocket-configuration]]
== WebSocket Configuration
== WebSocket Authorization
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
To configure authorization by using Java Configuration, extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`:
In Spring Security 5.8, this support has been refreshed to use the `AuthorizationManager` API.
To configure authorization using Java Configuration, simply include the `@EnableWebSocketSecurity` annotation and publish an `AuthorizationManager<Message<?>>` bean or in XML use the `use-authorization-manager` attribute.
One way to do this is by using the `AuthorizationManagerMessageMatcherRegistry` to specify endpoint patterns like so:
====
.Java
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2>
@EnableWebSocketSecurity // <1> <2>
public class WebSocketSecurityConfig {
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/user/**").authenticated() // <3>
return messages.build();
}
}
----
@@ -36,9 +52,12 @@ public class WebSocketSecurityConfig
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2>
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
@EnableWebSocketSecurity // <1> <2>
open class WebSocketSecurityConfig { // <1> <2>
@Bean
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
messages.simpDestMatchers("/user/**").authenticated() // <3>
return messages.build()
}
}
----
@@ -53,49 +72,36 @@ A comparable XML based configuration looks like the following:
====
[source,xml]
----
<websocket-message-broker> <!--1--> <!--2-->
<!--3-->
<intercept-message pattern="/user/**" access="hasRole('USER')" />
<websocket-message-broker use-authorization-manager="true">
<intercept-message pattern="/user/**" access="authenticated"/>
</websocket-message-broker>
----
====
This will ensure that:
<1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
====
[[websocket-authentication]]
== WebSocket Authentication
=== Custom Authorization
WebSockets reuse the same authentication information that is found in the HTTP request when the WebSocket connection was made.
This means that the `Principal` on the `HttpServletRequest` is handed off to WebSockets.
If you use Spring Security, the `Principal` on the `HttpServletRequest` is overridden automatically.
More concretely, to ensure a user has authenticated to your WebSocket application, all you need to do is ensure that you set up Spring Security to authenticate your HTTP based web application.
[[websocket-authorization]]
== WebSocket Authorization
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
To configure authorization by using Java configuration, extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`:
When using `AuthorizationManager`, customization is quite simple.
For example, you can publish an `AuthorizationManager` that requires that all messages have a role of "USER" using `AuthorityAuthorizationManager`, as seen below:
====
.Java
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.nullDestMatcher().authenticated() // <1>
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
.simpDestMatchers("/app/**").hasRole("USER") // <3>
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
.anyMessage().denyAll(); // <6>
@EnableWebSocketSecurity // <1> <2>
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
return AuthorityAuthorizationManager.hasRole("USER");
}
}
----
@@ -104,8 +110,54 @@ public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBro
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
@EnableWebSocketSecurity // <1> <2>
open class WebSocketSecurityConfig {
@Bean
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
return AuthorityAuthorizationManager.hasRole("USER") // <3>
}
}
----
.Xml
[source,xml,role="secondary"]
----
<bean id="authorizationManager" class="org.example.MyAuthorizationManager"/>
<websocket-message-broker authorization-manager-ref="myAuthorizationManager"/>
----
====
There are several ways to further match messages, as can be seen in a more advanced example below:
====
.Java
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig {
@Bean
public AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.nullDestMatcher().authenticated() // <1>
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
.simpDestMatchers("/app/**").hasRole("USER") // <3>
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
.anyMessage().denyAll(); // <6>
return messages.build();
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig {
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?> {
messages
.nullDestMatcher().authenticated() // <1>
.simpSubscribeDestMatchers("/user/queue/errors").permitAll() // <2>
@@ -113,24 +165,16 @@ open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfi
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
.anyMessage().denyAll() // <6>
return messages.build();
}
}
----
<1> Any message without a destination (i.e. anything other than Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated
<2> Anyone can subscribe to /user/queue/errors
<3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER
<4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
====
Spring Security also provides xref:servlet/appendix/namespace/websocket.adoc#nsa-websocket-security[XML Namespace] support for securing WebSockets.
A comparable XML based configuration looks like the following:
====
[source,xml]
.Xml
[source,kotlin,role="secondary"]
----
<websocket-message-broker>
<websocket-message-broker use-authorization-manager="true">
<!--1-->
<intercept-message type="CONNECT" access="permitAll" />
<intercept-message type="UNSUBSCRIBE" access="permitAll" />
@@ -140,8 +184,8 @@ A comparable XML based configuration looks like the following:
<intercept-message pattern="/app/**" access="hasRole('USER')" /> <!--3-->
<!--4-->
<intercept-message pattern="/user/**" access="hasRole('USER')" />
<intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />
<intercept-message pattern="/user/**" type="SUBSCRIBE" access="hasRole('USER')" />
<intercept-message pattern="/topic/friends/*" type="SUBSCRIBE" access="hasRole('USER')" />
<!--5-->
<intercept-message type="MESSAGE" access="denyAll" />
@@ -150,13 +194,16 @@ A comparable XML based configuration looks like the following:
<intercept-message pattern="/**" access="denyAll" /> <!--6-->
</websocket-message-broker>
----
<1> Any message of type CONNECT, UNSUBSCRIBE, or DISCONNECT will require the user to be authenticated
====
This will ensure that:
<1> Any message without a destination (i.e. anything other than Message type of MESSAGE or SUBSCRIBE) will require the user to be authenticated
<2> Anyone can subscribe to /user/queue/errors
<3> Any message that has a destination starting with "/app/" will be require the user to have the role ROLE_USER
<4> Any message that starts with "/user/" or "/topic/friends/" that is of type SUBSCRIBE will require ROLE_USER
<5> Any other message of type MESSAGE or SUBSCRIBE is rejected. Due to 6 we do not need this step, but it illustrates how one can match on specific message types.
<6> Any other message with a destination is rejected. This is a good idea to ensure that you do not miss any messages.
====
<6> Any other Message is rejected. This is a good idea to ensure that you do not miss any messages.
[[websocket-authorization-notes]]
=== WebSocket Authorization Notes
@@ -306,9 +353,65 @@ stompClient.connect(headers, function(frame) {
[[websocket-sameorigin-disable]]
=== Disable CSRF within WebSockets
NOTE: At this point, CSRF is not configurable when using `@EnableWebSocketSecurity`, though this will likely be added in a future release.
If you want to let other domains access your site, you can disable Spring Security's protection.
For example, in Java configuration you can use the following:
To disable CSRF, instead of using `@EnableWebSocketSecurity`, you can use XML support or add the Spring Security components yourself, like so:
====
.Java
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
AuthorizationManager<Message<?>> myAuthorizationRules = AuthenticatedAuthorizationManager.authenticated();
AuthorizationChannelInterceptor authz = new AuthorizationChannelInterceptor(myAuthorizationRules);
AuthorizationEventPublisher publisher = new SpringAuthorizationEventPublisher(this.context);
authz.setAuthorizationEventPublisher(publisher);
registration.interceptors(new SecurityContextChannelInterceptor(), authz);
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig : WebSocketMessageBrokerConfigurer {
@Override
override fun addArgumentResolvers(argumentResolvers: List<HandlerMethodArgumentResolver>) {
argumentResolvers.add(AuthenticationPrincipalArgumentResolver())
}
@Override
override fun configureClientInboundChannel(registration: ChannelRegistration) {
var myAuthorizationRules: AuthorizationManager<Message<?>> = AuthenticatedAuthorizationManager.authenticated()
var authz: AuthorizationChannelInterceptor = AuthorizationChannelInterceptor(myAuthorizationRules)
var publisher: AuthorizationEventPublisher = SpringAuthorizationEventPublisher(this.context)
authz.setAuthorizationEventPublisher(publisher)
registration.interceptors(SecurityContextChannelInterceptor(), authz)
}
}
----
.Xml
[source,xml,role="secondary"]
----
<websocket-message-broker use-authorization-manager="true" same-origin-disabled="true">
<intercept-message pattern="/**" access="authenticated"/>
</websocket-message-broker>
----
====
On the other hand, if you are using the <<legacy `AbstractSecurityWebSocketMessageBrokerConfigurer`,legacy-websocket-configuration>> and you want to allow other domains to access your site, you can disable Spring Security's protection.
For example, in Java Configuration you can use the following:
====
.Java
@@ -341,6 +444,39 @@ open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfi
----
====
[[websocket-expression-handler]]
=== Custom Expression Handler
At times, there may be value in customizing how the `access` expressions are handled defined in your `intercept-message` XML elements.
To do this, you can create a class of type `SecurityExpressionHandler<MessageAuthorizationContext<?>>` and refer to it in your XML definition like so:
[source,xml]
----
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler"/>
----
If you are migrating from a legacy usage of `websocket-message-broker` that implements a `SecurityExpressionHandler<Message<?>>`, you can:
1. Additionally implement the `createEvaluationContext(Supplier, Message)` method and then
2. Wrap that value in a `MessageAuthorizationContextSecurityExpressionHandler` like so:
[source,xml]
----
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="myRef"/>
...
</websocket-message-broker>
<b:bean ref="myRef" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler">
<b:constructor-arg>
<b:bean class="org.example.MyLegacyExpressionHandler"/>
</b:constructor-arg>
</b:bean>
----
[[websocket-sockjs]]
== Working with SockJS
@@ -516,4 +652,47 @@ If we use XML-based configuration, we can use thexref:servlet/appendix/namespace
</b:constructor-arg>
</b:bean>
----
[[legacy-websocket-configuration]]
== Legacy WebSocket Configuration
Before Spring Security 5.8, the way to configure messaging authorization using Java Configuration, was to extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
For example:
====
.Java
[source,java,role="primary"]
----
@Configuration
public class WebSocketSecurityConfig
extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2>
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
messages
.simpDestMatchers("/user/**").authenticated() // <3>
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Configuration
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2>
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
messages.simpDestMatchers("/user/**").authenticated() // <3>
}
}
----
====
This will ensure that:
<1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
Using the legacy configuration is helpful in the event that you have a custom `SecurityExpressionHandler` that extends `AbstractSecurityExpressionHandler` and overrides `createEvaluationContextInternal` or `createSecurityExpressionRoot`.
In order to defer `Authorization` lookup, the new `AuthorizationManager` API does not invoke these when evaluating expressions.
If you are using XML, you can use the legacy APIs simply by not using the `use-authorization-manager` element or setting it to `false`.