Add AuthorizationManager to Messaging
Closes gh-11076
This commit is contained in:
@@ -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`.
|
||||
|
||||
Reference in New Issue
Block a user