Add AuthorizationManager to Messaging

Closes gh-11076
This commit is contained in:
Josh Cummings 2022-04-07 17:39:10 -06:00
parent 495028eb85
commit 8a03d1fcec
31 changed files with 2843 additions and 162 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,6 +28,7 @@ import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer;
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
@ -43,7 +44,9 @@ import org.springframework.util.StringUtils;
*
* @author Rob Winch
* @since 4.0
* @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead
*/
@Deprecated
public class MessageSecurityMetadataSourceRegistry {
private static final String permitAll = "permitAll";

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -81,9 +81,12 @@ import org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsSe
*
* @author Rob Winch
* @since 4.0
* @see WebSocketMessageBrokerSecurityConfiguration
* @deprecated Use {@link EnableWebSocketSecurity} instead
*/
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
@Import(ObjectPostProcessorConfiguration.class)
@Deprecated
public abstract class AbstractSecurityWebSocketMessageBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer
implements SmartInitializingSingleton {

View File

@ -0,0 +1,58 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.socket;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;
/**
* Allows configuring WebSocket Authorization.
*
* <p>
* For example:
* </p>
*
* <pre>
* &#064;Configuration
* &#064;EnableWebSocketSecurity
* public class WebSocketSecurityConfig {
*
* &#064;Bean
* AuthorizationManager&lt;Message&lt;?&gt;&gt; (MessageMatcherDelegatingAuthorizationManager.Builder messages) {
* messages.simpDestMatchers(&quot;/user/queue/errors&quot;).permitAll()
* .simpDestMatchers(&quot;/admin/**&quot;).hasRole(&quot;ADMIN&quot;)
* .anyMessage().authenticated();
* return messages.build();
* }
* }
* </pre>
*
* @author Josh Cummings
* @since 5.8
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(WebSocketMessageBrokerSecurityConfiguration.class)
public @interface EnableWebSocketSecurity {
}

View File

@ -0,0 +1,38 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.socket;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Scope;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.util.AntPathMatcher;
final class MessageMatcherAuthorizationManagerConfiguration {
@Bean
@Scope("prototype")
MessageMatcherDelegatingAuthorizationManager.Builder messageAuthorizationManagerBuilder(
ApplicationContext context) {
return MessageMatcherDelegatingAuthorizationManager.builder().simpDestPathMatcher(
() -> (context.getBeanNamesForType(SimpAnnotationMethodMessageHandler.class).length > 0)
? context.getBean(SimpAnnotationMethodMessageHandler.class).getPathMatcher()
: new AntPathMatcher());
}
}

View File

@ -0,0 +1,145 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.SpringAuthorizationEventPublisher;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.util.Assert;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.HandshakeInterceptor;
import org.springframework.web.socket.server.support.WebSocketHttpRequestHandler;
import org.springframework.web.socket.sockjs.SockJsService;
import org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler;
import org.springframework.web.socket.sockjs.transport.TransportHandlingSockJsService;
@Order(Ordered.HIGHEST_PRECEDENCE + 100)
@Import(MessageMatcherAuthorizationManagerConfiguration.class)
final class WebSocketMessageBrokerSecurityConfiguration
implements WebSocketMessageBrokerConfigurer, SmartInitializingSingleton {
private static final String SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME = "stompWebSocketHandlerMapping";
private MessageMatcherDelegatingAuthorizationManager b;
private static final AuthorizationManager<Message<?>> ANY_MESSAGE_AUTHENTICATED = MessageMatcherDelegatingAuthorizationManager
.builder().anyMessage().authenticated().build();
private final ChannelInterceptor securityContextChannelInterceptor = new SecurityContextChannelInterceptor();
private final ChannelInterceptor csrfChannelInterceptor = new CsrfChannelInterceptor();
private AuthorizationChannelInterceptor authorizationChannelInterceptor = new AuthorizationChannelInterceptor(
ANY_MESSAGE_AUTHENTICATED);
private ApplicationContext context;
WebSocketMessageBrokerSecurityConfiguration(ApplicationContext context) {
this.context = context;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
this.authorizationChannelInterceptor
.setAuthorizationEventPublisher(new SpringAuthorizationEventPublisher(this.context));
registration.interceptors(this.securityContextChannelInterceptor, this.csrfChannelInterceptor,
this.authorizationChannelInterceptor);
}
@Autowired(required = false)
void setAuthorizationManager(AuthorizationManager<Message<?>> authorizationManager) {
this.authorizationChannelInterceptor = new AuthorizationChannelInterceptor(authorizationManager);
}
@Override
public void afterSingletonsInstantiated() {
SimpleUrlHandlerMapping mapping = getBeanOrNull(SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME,
SimpleUrlHandlerMapping.class);
if (mapping == null) {
return;
}
configureCsrf(mapping);
}
private <T> T getBeanOrNull(String name, Class<T> type) {
Map<String, T> beans = this.context.getBeansOfType(type);
return beans.get(name);
}
private void configureCsrf(SimpleUrlHandlerMapping mapping) {
Map<String, Object> mappings = mapping.getHandlerMap();
for (Object object : mappings.values()) {
if (object instanceof SockJsHttpRequestHandler) {
setHandshakeInterceptors((SockJsHttpRequestHandler) object);
}
else if (object instanceof WebSocketHttpRequestHandler) {
setHandshakeInterceptors((WebSocketHttpRequestHandler) object);
}
else {
throw new IllegalStateException(
"Bean " + SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME + " is expected to contain mappings to either a "
+ "SockJsHttpRequestHandler or a WebSocketHttpRequestHandler but got " + object);
}
}
}
private void setHandshakeInterceptors(SockJsHttpRequestHandler handler) {
SockJsService sockJsService = handler.getSockJsService();
Assert.state(sockJsService instanceof TransportHandlingSockJsService,
() -> "sockJsService must be instance of TransportHandlingSockJsService got " + sockJsService);
TransportHandlingSockJsService transportHandlingSockJsService = (TransportHandlingSockJsService) sockJsService;
List<HandshakeInterceptor> handshakeInterceptors = transportHandlingSockJsService.getHandshakeInterceptors();
List<HandshakeInterceptor> interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1);
interceptorsToSet.add(new CsrfTokenHandshakeInterceptor());
interceptorsToSet.addAll(handshakeInterceptors);
transportHandlingSockJsService.setHandshakeInterceptors(interceptorsToSet);
}
private void setHandshakeInterceptors(WebSocketHttpRequestHandler handler) {
List<HandshakeInterceptor> handshakeInterceptors = handler.getHandshakeInterceptors();
List<HandshakeInterceptor> interceptorsToSet = new ArrayList<>(handshakeInterceptors.size() + 1);
interceptorsToSet.add(new CsrfTokenHandshakeInterceptor());
interceptorsToSet.addAll(handshakeInterceptors);
handler.setHandshakeInterceptors(interceptorsToSet);
}
}

View File

@ -19,6 +19,7 @@ package org.springframework.security.config.websocket;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.w3c.dom.Element;
@ -37,20 +38,34 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.beans.factory.xml.XmlReaderContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler;
import org.springframework.security.access.expression.ExpressionUtils;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.vote.ConsensusBased;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.Elements;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.expression.ExpressionBasedMessageSecurityMetadataSourceFactory;
import org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler;
import org.springframework.security.messaging.access.expression.MessageExpressionVoter;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.context.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.messaging.web.socket.server.CsrfTokenHandshakeInterceptor;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
@ -99,6 +114,10 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
private static final String DISABLED_ATTR = "same-origin-disabled";
private static final String USE_AUTHORIZATION_MANAGER_ATTR = "use-authorization-manager";
private static final String AUTHORIZATION_MANAGER_REF_ATTR = "authorization-manager-ref";
private static final String PATTERN_ATTR = "pattern";
private static final String ACCESS_ATTR = "access";
@ -114,14 +133,83 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
*/
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
String id = element.getAttribute(ID_ATTR);
String inSecurityInterceptorName = parseAuthorization(element, parserContext);
BeanDefinitionRegistry registry = parserContext.getRegistry();
if (StringUtils.hasText(id)) {
registry.registerAlias(inSecurityInterceptorName, id);
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
}
}
else {
boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR));
XmlReaderContext context = parserContext.getReaderContext();
BeanDefinitionBuilder mspp = BeanDefinitionBuilder.rootBeanDefinition(MessageSecurityPostProcessor.class);
mspp.addConstructorArgValue(inSecurityInterceptorName);
mspp.addConstructorArgValue(sameOriginDisabled);
context.registerWithGeneratedName(mspp.getBeanDefinition());
}
return null;
}
private String parseAuthorization(Element element, ParserContext parserContext) {
boolean useAuthorizationManager = Boolean.parseBoolean(element.getAttribute(USE_AUTHORIZATION_MANAGER_ATTR));
if (useAuthorizationManager) {
return parseAuthorizationManager(element, parserContext);
}
if (StringUtils.hasText(element.getAttribute(AUTHORIZATION_MANAGER_REF_ATTR))) {
return parseAuthorizationManager(element, parserContext);
}
return parseSecurityMetadataSource(element, parserContext);
}
private String parseAuthorizationManager(Element element, ParserContext parserContext) {
XmlReaderContext context = parserContext.getReaderContext();
String mdsId = createAuthorizationManager(element, parserContext);
BeanDefinitionBuilder inboundChannelSecurityInterceptor = BeanDefinitionBuilder
.rootBeanDefinition(AuthorizationChannelInterceptor.class);
inboundChannelSecurityInterceptor.addConstructorArgReference(mdsId);
return context.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition());
}
private String createAuthorizationManager(Element element, ParserContext parserContext) {
XmlReaderContext context = parserContext.getReaderContext();
String authorizationManagerRef = element.getAttribute(AUTHORIZATION_MANAGER_REF_ATTR);
if (StringUtils.hasText(authorizationManagerRef)) {
return authorizationManagerRef;
}
Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null;
ManagedMap<BeanDefinition, BeanDefinition> matcherToExpression = new ManagedMap<>();
List<Element> interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_MESSAGE);
for (Element interceptMessage : interceptMessages) {
String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR);
String accessExpression = interceptMessage.getAttribute(ACCESS_ATTR);
String messageType = interceptMessage.getAttribute(TYPE_ATTR);
BeanDefinition matcher = createMatcher(matcherPattern, messageType, parserContext, interceptMessage);
BeanDefinitionBuilder authorizationManager = BeanDefinitionBuilder
.rootBeanDefinition(ExpressionBasedAuthorizationManager.class);
if (StringUtils.hasText(expressionHandlerRef)) {
authorizationManager.addConstructorArgReference(expressionHandlerRef);
}
authorizationManager.addConstructorArgValue(accessExpression);
matcherToExpression.put(matcher, authorizationManager.getBeanDefinition());
}
BeanDefinitionBuilder mds = BeanDefinitionBuilder
.rootBeanDefinition(MessageMatcherDelegatingAuthorizationManagerFactory.class);
mds.setFactoryMethod("createMessageMatcherDelegatingAuthorizationManager");
mds.addConstructorArgValue(matcherToExpression);
return context.registerWithGeneratedName(mds.getBeanDefinition());
}
private String parseSecurityMetadataSource(Element element, ParserContext parserContext) {
BeanDefinitionRegistry registry = parserContext.getRegistry();
XmlReaderContext context = parserContext.getReaderContext();
ManagedMap<BeanDefinition, String> matcherToExpression = new ManagedMap<>();
String id = element.getAttribute(ID_ATTR);
Element expressionHandlerElt = DomUtils.getChildElementByTagName(element, Elements.EXPRESSION_HANDLER);
String expressionHandlerRef = (expressionHandlerElt != null) ? expressionHandlerElt.getAttribute("ref") : null;
boolean expressionHandlerDefined = StringUtils.hasText(expressionHandlerRef);
boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR));
List<Element> interceptMessages = DomUtils.getChildElementsByTagName(element, Elements.INTERCEPT_MESSAGE);
for (Element interceptMessage : interceptMessages) {
String matcherPattern = interceptMessage.getAttribute(PATTERN_ATTR);
@ -151,21 +239,7 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
.rootBeanDefinition(ChannelSecurityInterceptor.class);
inboundChannelSecurityInterceptor.addConstructorArgValue(registry.getBeanDefinition(mdsId));
inboundChannelSecurityInterceptor.addPropertyValue("accessDecisionManager", adm.getBeanDefinition());
String inSecurityInterceptorName = context
.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition());
if (StringUtils.hasText(id)) {
registry.registerAlias(inSecurityInterceptorName, id);
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
}
}
else {
BeanDefinitionBuilder mspp = BeanDefinitionBuilder.rootBeanDefinition(MessageSecurityPostProcessor.class);
mspp.addConstructorArgValue(inSecurityInterceptorName);
mspp.addConstructorArgValue(sameOriginDisabled);
context.registerWithGeneratedName(mspp.getBeanDefinition());
}
return null;
return context.registerWithGeneratedName(inboundChannelSecurityInterceptor.getBeanDefinition());
}
private BeanDefinition createMatcher(String matcherPattern, String messageType, ParserContext parserContext,
@ -341,4 +415,48 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
}
private static final class ExpressionBasedAuthorizationManager
implements AuthorizationManager<MessageAuthorizationContext<?>> {
private final SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler;
private final Expression expression;
private ExpressionBasedAuthorizationManager(String expression) {
this(new MessageAuthorizationContextSecurityExpressionHandler(), expression);
}
private ExpressionBasedAuthorizationManager(
SecurityExpressionHandler<MessageAuthorizationContext<?>> expressionHandler, String expression) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
Assert.notNull(expression, "expression cannot be null");
this.expressionHandler = expressionHandler;
this.expression = this.expressionHandler.getExpressionParser().parseExpression(expression);
}
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication,
MessageAuthorizationContext<?> object) {
EvaluationContext context = this.expressionHandler.createEvaluationContext(authentication, object);
boolean granted = ExpressionUtils.evaluateAsBoolean(this.expression, context);
return new AuthorizationDecision(granted);
}
}
private static class MessageMatcherDelegatingAuthorizationManagerFactory {
private static AuthorizationManager<Message<?>> createMessageMatcherDelegatingAuthorizationManager(
Map<MessageMatcher<?>, AuthorizationManager<MessageAuthorizationContext<?>>> beans) {
MessageMatcherDelegatingAuthorizationManager.Builder builder = MessageMatcherDelegatingAuthorizationManager
.builder();
for (Map.Entry<MessageMatcher<?>, AuthorizationManager<MessageAuthorizationContext<?>>> entry : beans
.entrySet()) {
builder.matchers(entry.getKey()).access(entry.getValue());
}
return builder.anyMessage().permitAll().build();
}
}
}

View File

@ -291,6 +291,12 @@ websocket-message-broker.attrlist &=
websocket-message-broker.attrlist &=
## 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.
attribute same-origin-disabled {xsd:boolean}?
websocket-message-broker.attlist &=
## Use this AuthorizationManager instead of deriving one from <intercept-message> elements
attribute authorization-manager-ref {xsd:string}?
websocket-message-broker.attrlist &=
## Use AuthorizationManager API instead of SecurityMetadatasource
attribute use-authorization-manager {xsd:boolean}?
intercept-message =
## Creates an authorization rule for a websocket message.

View File

@ -124,7 +124,7 @@
</xs:annotation>
<xs:complexType/>
</xs:element>
<xs:attributeGroup name="password-encoder.attlist">
<xs:attribute name="ref" type="xs:token">
<xs:annotation>
@ -408,7 +408,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="ldap-ap.attlist">
<xs:attribute name="server-ref" type="xs:token">
<xs:annotation>
@ -488,7 +488,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="password-compare.attlist">
<xs:attribute name="password-attribute" type="xs:token">
<xs:annotation>
@ -541,7 +541,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="protect.attlist">
<xs:attribute name="method" use="required" type="xs:token">
<xs:annotation>
@ -842,13 +842,13 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="protect-pointcut.attlist">
<xs:attribute name="expression" use="required" type="xs:string">
<xs:annotation>
@ -915,6 +915,18 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="authorization-manager-ref" type="xs:string">
<xs:annotation>
<xs:documentation>Use this instance of AuthorizationManager instead of the `intercept-message` elements
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="use-authorization-manager" type="xs:boolean">
<xs:annotation>
<xs:documentation>Uses AuthorizationManager API in favor of legacy authorization APIs
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="intercept-message">
<xs:annotation>
@ -1323,7 +1335,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="access-denied-handler.attlist">
<xs:attribute name="ref" type="xs:token">
<xs:annotation>
@ -1348,7 +1360,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="intercept-url.attlist">
<xs:attribute name="pattern" type="xs:token">
<xs:annotation>
@ -1405,7 +1417,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="logout.attlist">
<xs:attribute name="logout-url" type="xs:token">
<xs:annotation>
@ -1452,7 +1464,7 @@
<xs:attributeGroup ref="security:ref"/>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="form-login.attlist">
<xs:attribute name="login-processing-url" type="xs:token">
<xs:annotation>
@ -1967,7 +1979,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="attribute-exchange">
<xs:annotation>
<xs:documentation>Sets up an attribute exchange configuration to request specified attributes from the
@ -2034,7 +2046,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="saml2-login.attlist">
<xs:attribute name="relying-party-registration-repository-ref" type="xs:token">
<xs:annotation>
@ -2091,7 +2103,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="saml2-logout.attlist">
<xs:attribute name="logout-url" type="xs:token">
<xs:annotation>
@ -2544,7 +2556,7 @@
</xs:simpleType>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="http-basic.attlist">
<xs:attribute name="entry-point-ref" type="xs:token">
<xs:annotation>
@ -2577,7 +2589,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="session-management.attlist">
<xs:attribute name="session-fixation-protection">
<xs:annotation>
@ -2633,7 +2645,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="concurrency-control.attlist">
<xs:attribute name="max-sessions" type="xs:token">
<xs:annotation>
@ -2680,7 +2692,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="remember-me.attlist">
<xs:attribute name="key" type="xs:token">
<xs:annotation>
@ -2778,7 +2790,7 @@
<xs:attributeGroup name="remember-me-data-source-ref">
<xs:attributeGroup ref="security:data-source-ref"/>
</xs:attributeGroup>
<xs:attributeGroup name="anonymous.attlist">
<xs:attribute name="key" type="xs:token">
<xs:annotation>
@ -2811,8 +2823,8 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="http-port">
<xs:attribute name="http" use="required" type="xs:token">
<xs:annotation>
@ -2829,7 +2841,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="x509.attlist">
<xs:attribute name="subject-principal-regex" type="xs:token">
<xs:annotation>
@ -2966,7 +2978,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="ap.attlist">
<xs:attribute name="ref" type="xs:token">
<xs:annotation>
@ -3018,7 +3030,7 @@
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="user.attlist">
<xs:attribute name="name" use="required" type="xs:token">
<xs:annotation>
@ -3761,4 +3773,4 @@
<xs:enumeration value="LAST"/>
</xs:restriction>
</xs:simpleType>
</xs:schema>
</xs:schema>

View File

@ -0,0 +1,176 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.socket;
import java.util.HashMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.stereotype.Controller;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
public class WebSocketMessageBrokerSecurityConfigurationDocTests {
AnnotationConfigWebApplicationContext context;
TestingAuthenticationToken messageUser;
CsrfToken token;
String sessionAttr;
@BeforeEach
public void setup() {
this.token = new DefaultCsrfToken("header", "param", "token");
this.sessionAttr = "sessionAttr";
this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
}
@AfterEach
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void securityMappings() {
loadConfig(WebSocketSecurityConfig.class);
clientInboundChannel().send(message("/user/queue/errors", SimpMessageType.SUBSCRIBE));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/denyAll", SimpMessageType.MESSAGE)))
.withCauseInstanceOf(AccessDeniedException.class);
}
private void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.register(WebSocketConfig.class, SyncExecutorConfig.class);
this.context.setServletConfig(new MockServletConfig());
this.context.refresh();
}
private MessageChannel clientInboundChannel() {
return this.context.getBean("clientInboundChannel", MessageChannel.class);
}
private Message<String> message(String destination, SimpMessageType type) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(type);
return message(headers, destination);
}
private Message<String> message(SimpMessageHeaderAccessor headers, String destination) {
headers.setSessionId("123");
headers.setSessionAttributes(new HashMap<>());
if (destination != null) {
headers.setDestination(destination);
}
if (this.messageUser != null) {
headers.setUser(this.messageUser);
}
return new GenericMessage<>("hi", headers.getMessageHeaders());
}
@Controller
static class MyController {
@MessageMapping("/authentication")
void authentication(@AuthenticationPrincipal String un) {
// ... do something ...
}
}
@Configuration
@EnableWebSocketSecurity
static class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> authorizationManager(
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(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE).denyAll() // <5>
.anyMessage().denyAll(); // <6>
return messages.build();
}
}
@Configuration
@EnableWebSocketMessageBroker
static class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/chat").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
}
@Bean
MyController myController() {
return new MyController();
}
}
@Configuration
static class SyncExecutorConfig {
@Bean
static SyncExecutorSubscribableChannelPostProcessor postProcessor() {
return new SyncExecutorSubscribableChannelPostProcessor();
}
}
}

View File

@ -0,0 +1,786 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.config.annotation.web.socket;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.MethodParameter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.messaging.support.AbstractMessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.mock.web.MockServletConfig;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.context.SecurityContextChannelInterceptor;
import org.springframework.security.messaging.web.csrf.CsrfChannelInterceptor;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.DefaultCsrfToken;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.stereotype.Controller;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.HttpRequestHandler;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.server.HandshakeFailureException;
import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler;
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.fail;
public class WebSocketMessageBrokerSecurityConfigurationTests {
AnnotationConfigWebApplicationContext context;
TestingAuthenticationToken messageUser;
CsrfToken token;
String sessionAttr;
@BeforeEach
public void setup() {
this.token = new DefaultCsrfToken("header", "param", "token");
this.sessionAttr = "sessionAttr";
this.messageUser = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
}
@AfterEach
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void simpleRegistryMappings() {
loadConfig(SockJsSecurityConfig.class);
clientInboundChannel().send(message("/permitAll"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/denyAll")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void annonymousSupported() {
loadConfig(SockJsSecurityConfig.class);
this.messageUser = null;
clientInboundChannel().send(message("/permitAll"));
}
// gh-3797
@Test
public void beanResolver() {
loadConfig(SockJsSecurityConfig.class);
this.messageUser = null;
clientInboundChannel().send(message("/beanResolver"));
}
@Test
public void addsAuthenticationPrincipalResolver() {
loadConfig(SockJsSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Message<String> message = message("/permitAll/authentication");
messageChannel.send(message);
assertThat(this.context.getBean(MyController.class).authenticationPrincipal)
.isEqualTo((String) this.messageUser.getPrincipal());
}
@Test
public void addsAuthenticationPrincipalResolverWhenNoAuthorization() {
loadConfig(NoInboundSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Message<String> message = message("/permitAll/authentication");
messageChannel.send(message);
assertThat(this.context.getBean(MyController.class).authenticationPrincipal)
.isEqualTo((String) this.messageUser.getPrincipal());
}
@Test
public void addsCsrfProtectionWhenNoAuthorization() {
loadConfig(NoInboundSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MessageChannel messageChannel = clientInboundChannel();
assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message))
.withCauseInstanceOf(MissingCsrfTokenException.class);
}
@Test
public void csrfProtectionForConnect() {
loadConfig(SockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MessageChannel messageChannel = clientInboundChannel();
assertThatExceptionOfType(MessageDeliveryException.class).isThrownBy(() -> messageChannel.send(message))
.withCauseInstanceOf(MissingCsrfTokenException.class);
}
@Test
@Disabled // to be added back in with the introduction of DSL support
public void csrfProtectionDisabledForConnect() {
loadConfig(CsrfDisabledSockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/permitAll/connect");
MessageChannel messageChannel = clientInboundChannel();
messageChannel.send(message);
}
@Test
public void csrfProtectionDefinedByBean() {
loadConfig(SockJsProxylessSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel)
.getInterceptors().stream().map(ChannelInterceptor::getClass);
assertThat(interceptors).contains(CsrfChannelInterceptor.class);
}
@Test
public void messagesConnectUseCsrfTokenHandshakeInterceptor() throws Exception {
loadConfig(SockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MockHttpServletRequest request = sockjsHttpRequest("/chat");
HttpRequestHandler handler = handler(request);
handler.handleRequest(request, new MockHttpServletResponse());
assertHandshake(request);
}
@Test
public void messagesConnectUseCsrfTokenHandshakeInterceptorMultipleMappings() throws Exception {
loadConfig(SockJsSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MockHttpServletRequest request = sockjsHttpRequest("/other");
HttpRequestHandler handler = handler(request);
handler.handleRequest(request, new MockHttpServletResponse());
assertHandshake(request);
}
@Test
public void messagesConnectWebSocketUseCsrfTokenHandshakeInterceptor() throws Exception {
loadConfig(WebSocketSecurityConfig.class);
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
Message<?> message = message(headers, "/authentication");
MockHttpServletRequest request = websocketHttpRequest("/websocket");
HttpRequestHandler handler = handler(request);
handler.handleRequest(request, new MockHttpServletResponse());
assertHandshake(request);
}
@Test
public void msmsRegistryCustomPatternMatcher() {
loadConfig(MsmsRegistryCustomPatternMatcherConfig.class);
clientInboundChannel().send(message("/app/a.b"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/app/a.b.c")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void overrideMsmsRegistryCustomPatternMatcher() {
loadConfig(OverrideMsmsRegistryCustomPatternMatcherConfig.class);
clientInboundChannel().send(message("/app/a/b"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void defaultPatternMatcher() {
loadConfig(DefaultPatternMatcherConfig.class);
clientInboundChannel().send(message("/app/a/b"));
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/app/a/b/c")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void customExpression() {
loadConfig(CustomExpressionConfig.class);
clientInboundChannel().send(message("/denyRob"));
this.messageUser = new TestingAuthenticationToken("rob", "password", "ROLE_USER");
assertThatExceptionOfType(MessageDeliveryException.class)
.isThrownBy(() -> clientInboundChannel().send(message("/denyRob")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void channelSecurityInterceptorUsesMetadataSourceBeanWhenProxyingDisabled() {
loadConfig(SockJsProxylessSecurityConfig.class);
AbstractMessageChannel messageChannel = clientInboundChannel();
AuthorizationManager<Message<?>> authorizationManager = this.context.getBean(AuthorizationManager.class);
for (ChannelInterceptor interceptor : messageChannel.getInterceptors()) {
if (interceptor instanceof AuthorizationChannelInterceptor) {
assertThat(ReflectionTestUtils.getField(interceptor, "preSendAuthorizationManager"))
.isSameAs(authorizationManager);
return;
}
}
fail("did not find AuthorizationChannelInterceptor");
}
@Test
public void securityContextChannelInterceptorDefinedByBean() {
loadConfig(SockJsProxylessSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel)
.getInterceptors().stream().map(ChannelInterceptor::getClass);
assertThat(interceptors).contains(SecurityContextChannelInterceptor.class);
}
@Test
public void inboundChannelSecurityDefinedByBean() {
loadConfig(SockJsProxylessSecurityConfig.class);
MessageChannel messageChannel = clientInboundChannel();
Stream<Class<? extends ChannelInterceptor>> interceptors = ((AbstractMessageChannel) messageChannel)
.getInterceptors().stream().map(ChannelInterceptor::getClass);
assertThat(interceptors).contains(AuthorizationChannelInterceptor.class);
}
private void assertHandshake(HttpServletRequest request) {
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
assertThat(handshakeHandler.attributes.get(CsrfToken.class.getName())).isSameAs(this.token);
assertThat(handshakeHandler.attributes.get(this.sessionAttr))
.isEqualTo(request.getSession().getAttribute(this.sessionAttr));
}
private HttpRequestHandler handler(HttpServletRequest request) throws Exception {
HandlerMapping handlerMapping = this.context.getBean(HandlerMapping.class);
return (HttpRequestHandler) handlerMapping.getHandler(request).getHandler();
}
private MockHttpServletRequest websocketHttpRequest(String mapping) {
MockHttpServletRequest request = sockjsHttpRequest(mapping);
request.setRequestURI(mapping);
return request;
}
private MockHttpServletRequest sockjsHttpRequest(String mapping) {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "");
request.setMethod("GET");
request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, "/289/tpyx6mde/websocket");
request.setRequestURI(mapping + "/289/tpyx6mde/websocket");
request.getSession().setAttribute(this.sessionAttr, "sessionValue");
request.setAttribute(CsrfToken.class.getName(), this.token);
return request;
}
private Message<String> message(String destination) {
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create();
return message(headers, destination);
}
private Message<String> message(SimpMessageHeaderAccessor headers, String destination) {
headers.setSessionId("123");
headers.setSessionAttributes(new HashMap<>());
if (destination != null) {
headers.setDestination(destination);
}
if (this.messageUser != null) {
headers.setUser(this.messageUser);
}
return new GenericMessage<>("hi", headers.getMessageHeaders());
}
private <T extends MessageChannel> T clientInboundChannel() {
return (T) this.context.getBean("clientInboundChannel", MessageChannel.class);
}
private void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.setServletConfig(new MockServletConfig());
this.context.refresh();
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/app/a.*").permitAll()
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class OverrideMsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestPathMatcher(new AntPathMatcher())
.simpDestMatchers("/app/a/*").permitAll()
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class DefaultPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.simpDestMatchers("/app/a/*").permitAll()
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketMessageBroker
@EnableWebSocketSecurity
@Import(SyncExecutorConfig.class)
static class CustomExpressionConfig implements WebSocketMessageBrokerConfigurer {
// @formatter:off
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/other")
.setHandshakeHandler(testHandshakeHandler());
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app");
}
@Bean
AuthorizationManager<Message<Object>> authorizationManager() {
return (authentication, message) -> {
Authentication auth = authentication.get();
return new AuthorizationDecision(auth != null && !"rob".equals(auth.getName()));
};
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Controller
static class MyController {
String authenticationPrincipal;
MyCustomArgument myCustomArgument;
@MessageMapping("/authentication")
void authentication(@AuthenticationPrincipal String un) {
this.authenticationPrincipal = un;
}
@MessageMapping("/myCustom")
void myCustom(MyCustomArgument myCustomArgument) {
this.myCustomArgument = myCustomArgument;
}
}
static class MyCustomArgument {
MyCustomArgument(String notDefaultConstr) {
}
}
static class MyCustomArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().isAssignableFrom(MyCustomArgument.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, Message<?> message) {
return new MyCustomArgument("");
}
}
static class TestHandshakeHandler implements HandshakeHandler {
Map<String, Object> attributes;
@Override
public boolean doHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler,
Map<String, Object> attributes) throws HandshakeFailureException {
this.attributes = attributes;
if (wsHandler instanceof SockJsWebSocketHandler) {
// work around SPR-12716
SockJsWebSocketHandler sockJs = (SockJsWebSocketHandler) wsHandler;
WebSocketServerSockJsSession session = (WebSocketServerSockJsSession) ReflectionTestUtils
.getField(sockJs, "sockJsSession");
this.attributes = session.getAttributes();
}
return true;
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class SockJsSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/other").setHandshakeHandler(testHandshakeHandler())
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
registry.addEndpoint("/chat").setHandshakeHandler(testHandshakeHandler())
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages,
SecurityCheck security) {
AuthorizationManager<MessageAuthorizationContext<?>> beanResolver =
(authentication, context) -> new AuthorizationDecision(security.check());
messages
.simpDestMatchers("/permitAll/**").permitAll()
.simpDestMatchers("/beanResolver/**").access(beanResolver)
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
}
@Bean
MyController myController() {
return new MyController();
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
@Bean
SecurityCheck security() {
return new SecurityCheck();
}
static class SecurityCheck {
private boolean check;
boolean check() {
this.check = !this.check;
return this.check;
}
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class NoInboundSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/other")
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
registry.addEndpoint("/chat")
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
}
@Bean
MyController myController() {
return new MyController();
}
}
@Configuration
@Import(SockJsSecurityConfig.class)
static class CsrfDisabledSockJsSecurityConfig {
@Bean
Consumer<List<ChannelInterceptor>> channelInterceptorCustomizer() {
return (interceptors) -> interceptors.remove(1);
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class WebSocketSecurityConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/websocket")
.setHandshakeHandler(testHandshakeHandler())
.addInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Bean
AuthorizationManager<Message<?>> authorizationManager(
MessageMatcherDelegatingAuthorizationManager.Builder messages) {
// @formatter:off
messages
.simpDestMatchers("/permitAll/**").permitAll()
.anyMessage().denyAll();
// @formatter:on
return messages.build();
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class UsingLegacyConfigurerConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/websocket")
.setHandshakeHandler(testHandshakeHandler())
.addInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Override
public void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
// @formatter:off
messages
.simpDestMatchers("/permitAll/**").permitAll()
.anyMessage().denyAll();
// @formatter:on
}
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration(proxyBeanMethods = false)
@EnableWebSocketSecurity
@EnableWebSocketMessageBroker
@Import(SyncExecutorConfig.class)
static class SockJsProxylessSecurityConfig implements WebSocketMessageBrokerConfigurer {
private ApplicationContext context;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// @formatter:off
registry.addEndpoint("/chat")
.setHandshakeHandler(this.context.getBean(TestHandshakeHandler.class))
.withSockJS().setInterceptors(new HttpSessionHandshakeInterceptor());
// @formatter:on
}
@Autowired
void setContext(ApplicationContext context) {
this.context = context;
}
// @formatter:off
@Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages
.anyMessage().denyAll();
return messages.build();
}
// @formatter:on
@Bean
TestHandshakeHandler testHandshakeHandler() {
return new TestHandshakeHandler();
}
}
@Configuration
static class SyncExecutorConfig {
@Bean
static SyncExecutorSubscribableChannelPostProcessor postProcessor() {
return new SyncExecutorSubscribableChannelPostProcessor();
}
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.security.config.websocket;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import org.assertj.core.api.ThrowableAssert;
import org.junit.jupiter.api.Test;
@ -33,6 +34,8 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProce
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.MethodParameter;
import org.springframework.core.task.SyncTaskExecutor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
@ -44,6 +47,8 @@ import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.expression.SecurityExpressionOperations;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
@ -68,6 +73,9 @@ import org.springframework.web.socket.server.HandshakeHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
/**
@ -168,6 +176,78 @@ public class WebSocketMessageBrokerConfigTests {
send(message);
}
@Test
public void sendWhenNoIdSpecifiedThenIntegratesWithAuthorizationManager() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
this.clientInboundChannel.send(message("/permitAll"));
assertThatExceptionOfType(Exception.class).isThrownBy(() -> this.clientInboundChannel.send(message("/denyAll")))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenAnonymousMessageWithConnectMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.CONNECT);
headers.setNativeHeader(this.token.getHeaderName(), this.token.getToken());
this.clientInboundChannel.send(message("/permitAll", headers));
}
@Test
public void sendWhenAnonymousMessageWithConnectAckMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.CONNECT_ACK);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithDisconnectMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.DISCONNECT);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithDisconnectAckMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.DISCONNECT_ACK);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithHeartbeatMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.HEARTBEAT);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithMessageMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.MESSAGE);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithOtherMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.OTHER);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithSubscribeMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.SUBSCRIBE);
send(message);
}
@Test
public void sendWhenAnonymousMessageWithUnsubscribeMessageTypeThenAuthorizationManagerPermits() {
this.spring.configLocations(xml("NoIdAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.UNSUBSCRIBE);
send(message);
}
@Test
public void sendWhenConnectWithoutCsrfTokenThenDenied() {
this.spring.configLocations(xml("SyncConfig")).autowire();
@ -196,6 +276,19 @@ public class WebSocketMessageBrokerConfigTests {
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenInterceptWiredForMessageTypeThenAuthorizationManagerDeniesOnTypeMismatch() {
this.spring.configLocations(xml("MessageInterceptTypeAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.MESSAGE);
send(message);
message = message("/permitAll", SimpMessageType.UNSUBSCRIBE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
message = message("/anyOther", SimpMessageType.MESSAGE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenInterceptWiredForSubscribeTypeThenDeniesOnTypeMismatch() {
this.spring.configLocations(xml("SubscribeInterceptTypeConfig")).autowire();
@ -209,6 +302,19 @@ public class WebSocketMessageBrokerConfigTests {
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenInterceptWiredForSubscribeTypeThenAuthorizationManagerDeniesOnTypeMismatch() {
this.spring.configLocations(xml("SubscribeInterceptTypeAuthorizationManager")).autowire();
Message<?> message = message("/permitAll", SimpMessageType.SUBSCRIBE);
send(message);
message = message("/permitAll", SimpMessageType.UNSUBSCRIBE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
message = message("/anyOther", SimpMessageType.SUBSCRIBE);
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void configureWhenUsingConnectMessageTypeThenAutowireFails() {
assertThatExceptionOfType(BeanDefinitionParsingException.class)
@ -309,6 +415,16 @@ public class WebSocketMessageBrokerConfigTests {
send(message);
}
@Test
public void sendWhenUsingCustomPathMatcherThenAuthorizationManagerAppliesIt() {
this.spring.configLocations(xml("CustomPathMatcherAuthorizationManager")).autowire();
Message<?> message = message("/denyAll.a");
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
message = message("/denyAll.a.b");
send(message);
}
@Test
public void sendWhenIdSpecifiedThenSecurityDoesNotIntegrateWithClientInboundChannel() {
this.spring.configLocations(xml("IdConfig")).autowire();
@ -342,6 +458,27 @@ public class WebSocketMessageBrokerConfigTests {
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
@WithMockUser(username = "nile")
public void sendWhenCustomExpressionHandlerThenAuthorizationManagerAuthorizesAccordingly() {
this.spring.configLocations(xml("CustomExpressionHandlerAuthorizationManager")).autowire();
Message<?> message = message("/denyNile");
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
}
@Test
public void sendWhenCustomAuthorizationManagerThenAuthorizesAccordingly() {
this.spring.configLocations(xml("CustomAuthorizationManagerConfig")).autowire();
AuthorizationManager<Message<?>> authorizationManager = this.spring.getContext()
.getBean(AuthorizationManager.class);
given(authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false));
Message<?> message = message("/any");
assertThatExceptionOfType(Exception.class).isThrownBy(send(message))
.withCauseInstanceOf(AccessDeniedException.class);
verify(authorizationManager).check(any(), any());
}
private String xml(String configName) {
return CONFIG_LOCATION_PREFIX + "-" + configName + ".xml";
}
@ -466,6 +603,17 @@ public class WebSocketMessageBrokerConfigTests {
};
}
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication,
Message<Object> message) {
return new StandardEvaluationContext(new MessageSecurityExpressionRoot(authentication, message) {
public boolean denyNile() {
Authentication auth = getAuthentication();
return auth != null && !"nile".equals(auth.getName());
}
});
}
}
}

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:import resource="classpath:org/springframework/security/config/websocket/websocket.xml"/>
<b:bean name="authorizationManager" class="org.mockito.Mockito" factory-method="mock">
<b:constructor-arg value="org.springframework.security.authorization.AuthorizationManager"/>
</b:bean>
<websocket-message-broker authorization-manager-ref="authorizationManager"/>
</b:beans>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:import resource="classpath:org/springframework/security/config/websocket/controllers.xml"/>
<b:import resource="classpath:org/springframework/security/config/websocket/websocket.xml"/>
<b:bean name="expressionHandler" class="org.springframework.security.messaging.access.expression.MessageAuthorizationContextSecurityExpressionHandler">
<b:constructor-arg>
<b:bean class="org.springframework.security.config.websocket.WebSocketMessageBrokerConfigTests.DenyNileMessageSecurityExpressionHandler"/>
</b:constructor-arg>
</b:bean>
<websocket-message-broker use-authorization-manager="true">
<expression-handler ref="expressionHandler"/>
<intercept-message pattern="/**" access="denyNile()"/>
</websocket-message-broker>
</b:beans>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:websocket="http://www.springframework.org/schema/websocket"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/websocket https://www.springframework.org/schema/websocket/spring-websocket.xsd">
<b:import resource="classpath:org/springframework/security/config/websocket/controllers.xml"/>
<websocket:message-broker path-matcher="pathMatcher">
<websocket:transport/>
<websocket:stomp-endpoint path="/app">
<websocket:handshake-handler ref="testHandler"/>
</websocket:stomp-endpoint>
<websocket:simple-broker prefix="/queue, /topic"/>
</websocket:message-broker>
<b:bean name="pathMatcher" class="org.springframework.util.AntPathMatcher">
<b:constructor-arg value="."/>
</b:bean>
<b:bean name="testHandler" class="org.springframework.security.config.websocket.WebSocketMessageBrokerConfigTests.TestHandshakeHandler"/>
<websocket-message-broker use-authorization-manager="true">
<intercept-message pattern="/denyAll.*" access="denyAll"/>
</websocket-message-broker>
</b:beans>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:import resource="classpath:org/springframework/security/config/websocket/controllers.xml"/>
<b:import resource="classpath:org/springframework/security/config/websocket/websocket.xml"/>
<websocket-message-broker use-authorization-manager="true">
<intercept-message pattern="/permitAll" type="MESSAGE" access="permitAll"/>
<intercept-message pattern="/**" access="denyAll"/>
</websocket-message-broker>
</b:beans>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:import resource="classpath:org/springframework/security/config/websocket/controllers.xml"/>
<b:import resource="classpath:org/springframework/security/config/websocket/websocket.xml"/>
<websocket-message-broker use-authorization-manager="true">
<intercept-message pattern="/permitAll" access="permitAll"/>
<intercept-message pattern="/denyAll" access="denyAll"/>
</websocket-message-broker>
</b:beans>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2002-2018 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ https://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<b:beans xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">
<b:import resource="classpath:org/springframework/security/config/websocket/controllers.xml"/>
<b:import resource="classpath:org/springframework/security/config/websocket/websocket.xml"/>
<websocket-message-broker use-authorization-manager="true">
<intercept-message pattern="/permitAll" type="SUBSCRIBE" access="permitAll"/>
<intercept-message pattern="/**" access="denyAll"/>
</websocket-message-broker>
</b:beans>

View File

@ -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>

View File

@ -11,63 +11,6 @@ This is because the format is unknown, so there is https://docs.spring.io/spring
Additionally, JSR-356 does not provide a way to intercept messages, so security would be rather invasive.
****
[[websocket-configuration]]
== WebSocket Configuration
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
To configure authorization using Java Configuration, simply 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>>
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]
----
<websocket-message-broker> <!--1--> <!--2-->
<!--3-->
<intercept-message pattern="/user/**" access="hasRole('USER')" />
</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
@ -77,30 +20,30 @@ If you are using Spring Security, the `Principal` on the `HttpServletRequest` is
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-authorization]]
[[websocket-configuration]]
== WebSocket Authorization
Spring Security 4.0 has introduced authorization support for WebSockets through the Spring Messaging abstraction.
To configure authorization using Java Configuration, simply extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
For example:
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 {
@EnableWebSocketSecurity // <1> <2>
public class WebSocketSecurityConfig {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
@Bean
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>
.simpDestMatchers("/user/**").authenticated() // <3>
return messages.build();
}
}
----
@ -109,8 +52,103 @@ 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 { // <1> <2>
@Bean
fun messageAuthorizationManager(messages: MessageMatcherDelegatingAuthorizationManager.Builder): AuthorizationManager<Message<?>> {
messages.simpDestMatchers("/user/**").authenticated() // <3>
return messages.build()
}
}
----
.Xml
[source,xml,role="secondary"]
----
<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>>
=== Custom Authorization
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
@EnableWebSocketSecurity // <1> <2>
public class WebSocketSecurityConfig {
@Bean
AuthorizationManager<Message<?>> messageAuthorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
return AuthorityAuthorizationManager.hasRole("USER");
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Configuration
@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>
@ -118,9 +156,35 @@ open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfi
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*").hasRole("USER") // <4>
.simpTypeMatchers(MESSAGE, SUBSCRIBE).denyAll() // <5>
.anyMessage().denyAll() // <6>
return messages.build();
}
}
----
.Xml
[source,kotlin,role="secondary"]
----
<websocket-message-broker use-authorization-manager="true">
<!--1-->
<intercept-message type="CONNECT" access="permitAll" />
<intercept-message type="UNSUBSCRIBE" access="permitAll" />
<intercept-message type="DISCONNECT" access="permitAll" />
<intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> <!--2-->
<intercept-message pattern="/app/**" access="hasRole('USER')" /> <!--3-->
<!--4-->
<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" />
<intercept-message type="SUBSCRIBE" access="denyAll" />
<intercept-message pattern="/**" access="denyAll" /> <!--6-->
</websocket-message-broker>
----
====
This will ensure that:
@ -132,41 +196,6 @@ This will ensure that:
<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]
----
<websocket-message-broker>
<!--1-->
<intercept-message type="CONNECT" access="permitAll" />
<intercept-message type="UNSUBSCRIBE" access="permitAll" />
<intercept-message type="DISCONNECT" access="permitAll" />
<intercept-message pattern="/user/queue/errors" type="SUBSCRIBE" access="permitAll" /> <!--2-->
<intercept-message pattern="/app/**" access="hasRole('USER')" /> <!--3-->
<!--4-->
<intercept-message pattern="/user/**" access="hasRole('USER')" />
<intercept-message pattern="/topic/friends/*" access="hasRole('USER')" />
<!--5-->
<intercept-message type="MESSAGE" access="denyAll" />
<intercept-message type="SUBSCRIBE" access="denyAll" />
<intercept-message pattern="/**" access="denyAll" /> <!--6-->
</websocket-message-broker>
----
This will ensure that:
<1> Any message of type CONNECT, UNSUBSCRIBE, or DISCONNECT 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.
[[websocket-authorization-notes]]
=== WebSocket Authorization Notes
@ -311,8 +340,64 @@ 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 allow other domains to access your site, you can disable Spring Security's protection.
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:
====
@ -346,6 +431,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
@ -515,3 +633,47 @@ For example:
</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`.

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -27,7 +27,10 @@ import org.springframework.expression.EvaluationContext;
*
* @author Daniel Bustamante Ospina
* @since 5.2
* @deprecated Since {@link MessageExpressionVoter} is deprecated, there is no more need
* for this class
*/
@Deprecated
interface EvaluationContextPostProcessor<I> {
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -35,7 +35,11 @@ import org.springframework.security.messaging.util.matcher.MessageMatcher;
*
* @author Rob Winch
* @since 4.0
* @deprecated Use
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager}
* instead
*/
@Deprecated
public final class ExpressionBasedMessageSecurityMetadataSourceFactory {
private ExpressionBasedMessageSecurityMetadataSourceFactory() {

View File

@ -0,0 +1,74 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.messaging.access.expression;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.messaging.Message;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
/**
* An expression handler for {@link MessageAuthorizationContext}.
*
* @author Josh Cummings
* @since 5.8
*/
public final class MessageAuthorizationContextSecurityExpressionHandler
implements SecurityExpressionHandler<MessageAuthorizationContext<?>> {
private final SecurityExpressionHandler<Message<?>> delegate;
@SuppressWarnings("rawtypes")
public MessageAuthorizationContextSecurityExpressionHandler() {
this(new DefaultMessageSecurityExpressionHandler());
}
public MessageAuthorizationContextSecurityExpressionHandler(
SecurityExpressionHandler<Message<?>> expressionHandler) {
this.delegate = expressionHandler;
}
@Override
public ExpressionParser getExpressionParser() {
return this.delegate.getExpressionParser();
}
@Override
public EvaluationContext createEvaluationContext(Authentication authentication,
MessageAuthorizationContext<?> message) {
return createEvaluationContext(() -> authentication, message);
}
@Override
public EvaluationContext createEvaluationContext(Supplier<Authentication> authentication,
MessageAuthorizationContext<?> message) {
EvaluationContext context = this.delegate.createEvaluationContext(authentication, message.getMessage());
Map<String, String> variables = message.getVariables();
if (variables != null) {
for (Map.Entry<String, String> entry : variables.entrySet()) {
context.setVariable(entry.getKey(), entry.getValue());
}
}
return context;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -32,7 +32,11 @@ import org.springframework.util.Assert;
* @author Rob Winch
* @author Daniel Bustamante Ospina
* @since 4.0
* @deprecated Use
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager}
* instead
*/
@Deprecated
@SuppressWarnings("serial")
class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationContextPostProcessor<Message<?>> {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -37,7 +37,11 @@ import org.springframework.util.Assert;
* @author Rob Winch
* @author Daniel Bustamante Ospina
* @since 4.0
* @deprecated Use
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager}
* instead
*/
@Deprecated
public class MessageExpressionVoter<T> implements AccessDecisionVoter<Message<T>> {
private SecurityExpressionHandler<Message<T>> expressionHandler = new DefaultMessageSecurityExpressionHandler<>();

View File

@ -0,0 +1,105 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.messaging.access.intercept;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.Assert;
/**
* Authorizes {@link Message} resources using the provided {@link AuthorizationManager}
*
* @author Josh Cummings
* @since 5.8
*/
public final class AuthorizationChannelInterceptor implements ChannelInterceptor {
static final Supplier<Authentication> AUTHENTICATION_SUPPLIER = () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new AuthenticationCredentialsNotFoundException(
"An Authentication object was not found in the SecurityContext");
}
return authentication;
};
private final Log logger = LogFactory.getLog(this.getClass());
private final AuthorizationManager<Message<?>> preSendAuthorizationManager;
private AuthorizationEventPublisher eventPublisher = new NoopAuthorizationEventPublisher();
/**
* Creates a new instance
* @param preSendAuthorizationManager the {@link AuthorizationManager} to use. Cannot
* be null.
*
*/
public AuthorizationChannelInterceptor(AuthorizationManager<Message<?>> preSendAuthorizationManager) {
Assert.notNull(preSendAuthorizationManager, "preSendAuthorizationManager cannot be null");
this.preSendAuthorizationManager = preSendAuthorizationManager;
}
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
this.logger.debug(LogMessage.of(() -> "Authorizing message send"));
AuthorizationDecision decision = this.preSendAuthorizationManager.check(AUTHENTICATION_SUPPLIER, message);
this.eventPublisher.publishAuthorizationEvent(AUTHENTICATION_SUPPLIER, message, decision);
if (decision == null || !decision.isGranted()) { // default deny
this.logger.debug(LogMessage.of(() -> "Failed to authorize message with authorization manager "
+ this.preSendAuthorizationManager + " and decision " + decision));
throw new AccessDeniedException("Access Denied");
}
this.logger.debug(LogMessage.of(() -> "Authorized message send"));
return message;
}
/**
* Use this {@link AuthorizationEventPublisher} to publish the
* {@link AuthorizationManager} result.
* @param eventPublisher
*/
public void setAuthorizationEventPublisher(AuthorizationEventPublisher eventPublisher) {
Assert.notNull(eventPublisher, "eventPublisher cannot be null");
this.eventPublisher = eventPublisher;
}
private static class NoopAuthorizationEventPublisher implements AuthorizationEventPublisher {
@Override
public <T> void publishAuthorizationEvent(Supplier<Authentication> authentication, T object,
AuthorizationDecision decision) {
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -36,7 +36,9 @@ import org.springframework.util.Assert;
*
* @author Rob Winch
* @since 4.0
* @deprecated Use {@link AuthorizationChannelInterceptor} instead
*/
@Deprecated
public final class ChannelSecurityInterceptor extends AbstractSecurityInterceptor implements ChannelInterceptor {
private static final ThreadLocal<InterceptorStatusToken> tokenHolder = new ThreadLocal<>();

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -40,7 +40,9 @@ import org.springframework.security.messaging.util.matcher.MessageMatcher;
* @since 4.0
* @see ChannelSecurityInterceptor
* @see ExpressionBasedMessageSecurityMetadataSourceFactory
* @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead
*/
@Deprecated
public final class DefaultMessageSecurityMetadataSource implements MessageSecurityMetadataSource {
private final Map<MessageMatcher<?>, Collection<ConfigAttribute>> messageMap;

View File

@ -0,0 +1,75 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.messaging.access.intercept;
import java.util.Collections;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.messaging.Message;
/**
* An {@link Message} authorization context.
*
* @author Josh Cummings
* @since 5.8
*/
public final class MessageAuthorizationContext<T> {
private final Message<T> message;
private final Map<String, String> variables;
/**
* Creates an instance.
* @param message the {@link HttpServletRequest} to use
*/
public MessageAuthorizationContext(Message<T> message) {
this(message, Collections.emptyMap());
}
/**
* Creates an instance.
* @param message the {@link HttpServletRequest} to use
* @param variables a map containing key-value pairs representing extracted variable
* names and variable values
*/
public MessageAuthorizationContext(Message<T> message, Map<String, String> variables) {
this.message = message;
this.variables = variables;
}
/**
* Returns the {@link HttpServletRequest}.
* @return the {@link HttpServletRequest} to use
*/
public Message<T> getMessage() {
return this.message;
}
/**
* Returns the extracted variable values where the key is the variable name and the
* value is the variable value.
* @return a map containing key-value pairs representing extracted variable names and
* variable values
*/
public Map<String, String> getVariables() {
return this.variables;
}
}

View File

@ -0,0 +1,429 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.messaging.access.intercept;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.security.authorization.AuthenticatedAuthorizationManager;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.PathMatcher;
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> {
private final Log logger = LogFactory.getLog(getClass());
private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings;
private MessageMatcherDelegatingAuthorizationManager(
List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings) {
Assert.notEmpty(mappings, "mappings cannot be empty");
this.mappings = mappings;
}
/**
* Delegates to a specific {@link AuthorizationManager} based on a
* {@link MessageMatcher} evaluation.
* @param authentication the {@link Supplier} of the {@link Authentication} to check
* @param message the {@link Message} to check
* @return an {@link AuthorizationDecision}. If there is no {@link MessageMatcher}
* matching the message, or the {@link AuthorizationManager} could not decide, then
* null is returned
*/
@Override
public AuthorizationDecision check(Supplier<Authentication> authentication, Message<?> message) {
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Authorizing message"));
}
for (Entry<AuthorizationManager<MessageAuthorizationContext<?>>> mapping : this.mappings) {
MessageMatcher<?> matcher = mapping.getMessageMatcher();
MessageAuthorizationContext<?> authorizationContext = authorizationContext(matcher, message);
if (authorizationContext != null) {
AuthorizationManager<MessageAuthorizationContext<?>> manager = mapping.getEntry();
if (this.logger.isTraceEnabled()) {
this.logger.trace(LogMessage.format("Checking authorization on message using %s", manager));
}
return manager.check(authentication, authorizationContext);
}
}
this.logger.trace("Abstaining since did not find matching MessageMatcher");
return null;
}
private MessageAuthorizationContext<?> authorizationContext(MessageMatcher<?> matcher, Message<?> message) {
if (!matcher.matches((Message) message)) {
return null;
}
if (matcher instanceof SimpDestinationMessageMatcher) {
SimpDestinationMessageMatcher simp = (SimpDestinationMessageMatcher) matcher;
return new MessageAuthorizationContext<>(message, simp.extractPathVariables(message));
}
return new MessageAuthorizationContext<>(message);
}
/**
* Creates a builder for {@link MessageMatcherDelegatingAuthorizationManager}.
* @return the new {@link Builder} instance
*/
public static Builder builder() {
return new Builder();
}
/**
* A builder for {@link MessageMatcherDelegatingAuthorizationManager}.
*/
public static final class Builder {
private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings = new ArrayList<>();
private Supplier<PathMatcher> pathMatcher = () -> new AntPathMatcher();
public Builder() {
}
/**
* Maps any {@link Message} to a security expression.
* @return the Expression to associate
*/
public Builder.Constraint anyMessage() {
return matchers(MessageMatcher.ANY_MESSAGE);
}
/**
* Maps any {@link Message} that has a null SimpMessageHeaderAccessor destination
* header (i.e. CONNECT, CONNECT_ACK, HEARTBEAT, UNSUBSCRIBE, DISCONNECT,
* DISCONNECT_ACK, OTHER)
* @return the Expression to associate
*/
public Builder.Constraint nullDestMatcher() {
return matchers(SimpDestinationMessageMatcher.NULL_DESTINATION_MATCHER);
}
/**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances.
* @param typesToMatch the {@link SimpMessageType} instance to match on
* @return the {@link Builder.Constraint} associated to the matchers.
*/
public Builder.Constraint simpTypeMatchers(SimpMessageType... typesToMatch) {
MessageMatcher<?>[] typeMatchers = new MessageMatcher<?>[typesToMatch.length];
for (int i = 0; i < typesToMatch.length; i++) {
SimpMessageType typeToMatch = typesToMatch[i];
typeMatchers[i] = new SimpMessageTypeMatcher(typeToMatch);
}
return matchers(typeMatchers);
}
/**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances without
* regard to the {@link SimpMessageType}. If no destination is found on the
* Message, then the Matcher returns false.
* @param patterns the patterns to create
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
* from.
*/
public Builder.Constraint simpDestMatchers(String... patterns) {
return simpDestMatchers(null, patterns);
}
/**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that
* match on {@code SimpMessageType.MESSAGE}. If no destination is found on the
* Message, then the Matcher returns false.
* @param patterns the patterns to create
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
* from.
*/
public Builder.Constraint simpMessageDestMatchers(String... patterns) {
return simpDestMatchers(SimpMessageType.MESSAGE, patterns);
}
/**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances that
* match on {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the
* Message, then the Matcher returns false.
* @param patterns the patterns to create
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
* from.
*/
public Builder.Constraint simpSubscribeDestMatchers(String... patterns) {
return simpDestMatchers(SimpMessageType.SUBSCRIBE, patterns);
}
/**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} instances. If no
* destination is found on the Message, then the Matcher returns false.
* @param type the {@link SimpMessageType} to match on. If null, the
* {@link SimpMessageType} is not considered for matching.
* @param patterns the patterns to create
* {@link org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher}
* from.
* @return the {@link Builder.Constraint} that is associated to the
* {@link MessageMatcher}
*/
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
for (String pattern : patterns) {
Supplier<MessageMatcher<Object>> supplier = new Builder.PathMatcherMessageMatcherBuilder(pattern, type);
MessageMatcher<?> matcher = new Builder.SupplierMessageMatcher(supplier);
matchers.add(matcher);
}
return new Builder.Constraint(matchers);
}
/**
* The {@link PathMatcher} to be used with the
* {@link Builder#simpDestMatchers(String...)}. The default is to use the default
* constructor of {@link AntPathMatcher}.
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null.
* @return the {@link Builder} for further customization.
*/
public Builder simpDestPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
this.pathMatcher = () -> pathMatcher;
return this;
}
/**
* The {@link PathMatcher} to be used with the
* {@link Builder#simpDestMatchers(String...)}. Use this method to delay the
* computation or lookup of the {@link PathMatcher}.
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null.
* @return the {@link Builder} for further customization.
*/
public Builder simpDestPathMatcher(Supplier<PathMatcher> pathMatcher) {
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
this.pathMatcher = pathMatcher;
return this;
}
/**
* Maps a {@link List} of {@link MessageMatcher} instances to a security
* expression.
* @param matchers the {@link MessageMatcher} instances to map.
* @return The {@link Builder.Constraint} that is associated to the
* {@link MessageMatcher} instances
*/
public Builder.Constraint matchers(MessageMatcher<?>... matchers) {
List<MessageMatcher<?>> builders = new ArrayList<>(matchers.length);
for (MessageMatcher<?> matcher : matchers) {
builders.add(matcher);
}
return new Builder.Constraint(builders);
}
public AuthorizationManager<Message<?>> build() {
return new MessageMatcherDelegatingAuthorizationManager(this.mappings);
}
/**
* Represents the security constraint to be applied to the {@link MessageMatcher}
* instances.
*/
public final class Constraint {
private final List<? extends MessageMatcher<?>> messageMatchers;
/**
* Creates a new instance
* @param messageMatchers the {@link MessageMatcher} instances to map to this
* constraint
*/
private Constraint(List<? extends MessageMatcher<?>> messageMatchers) {
Assert.notEmpty(messageMatchers, "messageMatchers cannot be null or empty");
this.messageMatchers = messageMatchers;
}
/**
* Shortcut for specifying {@link Message} instances require a particular
* role. If you do not want to have "ROLE_" automatically inserted see
* {@link #hasAuthority(String)}.
* @param role the role to require (i.e. USER, ADMIN, etc). Note, it should
* not start with "ROLE_" as this is automatically inserted.
* @return the {@link Builder} for further customization
*/
public Builder hasRole(String role) {
return access(AuthorityAuthorizationManager.hasRole(role));
}
/**
* Shortcut for specifying {@link Message} instances require any of a number
* of roles. If you do not want to have "ROLE_" automatically inserted see
* {@link #hasAnyAuthority(String...)}
* @param roles the roles to require (i.e. USER, ADMIN, etc). Note, it should
* not start with "ROLE_" as this is automatically inserted.
* @return the {@link Builder} for further customization
*/
public Builder hasAnyRole(String... roles) {
return access(AuthorityAuthorizationManager.hasAnyRole(roles));
}
/**
* Specify that {@link Message} instances require a particular authority.
* @param authority the authority to require (i.e. ROLE_USER, ROLE_ADMIN,
* etc).
* @return the {@link Builder} for further customization
*/
public Builder hasAuthority(String authority) {
return access(AuthorityAuthorizationManager.hasAuthority(authority));
}
/**
* Specify that {@link Message} instances requires any of a number
* authorities.
* @param authorities the requests require at least one of the authorities
* (i.e. "ROLE_USER","ROLE_ADMIN" would mean either "ROLE_USER" or
* "ROLE_ADMIN" is required).
* @return the {@link Builder} for further customization
*/
public Builder hasAnyAuthority(String... authorities) {
return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities));
}
/**
* Specify that Messages are allowed by anyone.
* @return the {@link Builder} for further customization
*/
public Builder permitAll() {
return access((authentication, context) -> new AuthorizationDecision(true));
}
/**
* Specify that Messages are not allowed by anyone.
* @return the {@link Builder} for further customization
*/
public Builder denyAll() {
return access((authorization, context) -> new AuthorizationDecision(false));
}
/**
* Specify that Messages are allowed by any authenticated user.
* @return the {@link Builder} for further customization
*/
public Builder authenticated() {
return access(AuthenticatedAuthorizationManager.authenticated());
}
/**
* Allows specifying that Messages are secured by an arbitrary expression
* @param authorizationManager the {@link AuthorizationManager} to secure the
* destinations
* @return the {@link Builder} for further customization
*/
public Builder access(AuthorizationManager<MessageAuthorizationContext<?>> authorizationManager) {
for (MessageMatcher<?> messageMatcher : this.messageMatchers) {
Builder.this.mappings.add(new Entry<>(messageMatcher, authorizationManager));
}
return Builder.this;
}
}
private static final class SupplierMessageMatcher implements MessageMatcher<Object> {
private final Supplier<MessageMatcher<Object>> supplier;
private volatile MessageMatcher<Object> delegate;
SupplierMessageMatcher(Supplier<MessageMatcher<Object>> supplier) {
this.supplier = supplier;
}
@Override
public boolean matches(Message<?> message) {
if (this.delegate == null) {
synchronized (this.supplier) {
if (this.delegate == null) {
this.delegate = this.supplier.get();
}
}
}
return this.delegate.matches(message);
}
}
private final class PathMatcherMessageMatcherBuilder implements Supplier<MessageMatcher<Object>> {
private final String pattern;
private final SimpMessageType type;
private PathMatcherMessageMatcherBuilder(String pattern, SimpMessageType type) {
this.pattern = pattern;
this.type = type;
}
private PathMatcher resolvePathMatcher() {
return Builder.this.pathMatcher.get();
}
@Override
public MessageMatcher<Object> get() {
PathMatcher pathMatcher = resolvePathMatcher();
if (this.type == null) {
return new SimpDestinationMessageMatcher(this.pattern, pathMatcher);
}
if (SimpMessageType.MESSAGE == this.type) {
return SimpDestinationMessageMatcher.createMessageMatcher(this.pattern, pathMatcher);
}
if (SimpMessageType.SUBSCRIBE == this.type) {
return SimpDestinationMessageMatcher.createSubscribeMatcher(this.pattern, pathMatcher);
}
throw new IllegalStateException(this.type + " is not supported since it does not have a destination");
}
}
}
private static final class Entry<T> {
private final MessageMatcher<?> messageMatcher;
private final T entry;
Entry(MessageMatcher requestMatcher, T entry) {
this.messageMatcher = requestMatcher;
this.entry = entry;
}
MessageMatcher<?> getMessageMatcher() {
return this.messageMatcher;
}
T getEntry() {
return this.entry;
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -26,7 +26,9 @@ import org.springframework.security.access.SecurityMetadataSource;
* @since 4.0
* @see ChannelSecurityInterceptor
* @see DefaultMessageSecurityMetadataSource
* @deprecated Use {@link MessageMatcherDelegatingAuthorizationManager} instead
*/
@Deprecated
public interface MessageSecurityMetadataSource extends SecurityMetadataSource {
}

View File

@ -0,0 +1,109 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.messaging.access.intercept;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link AuthorizationChannelInterceptor}
*/
@ExtendWith(MockitoExtension.class)
public class AuthorizationChannelInterceptorTests {
@Mock
Message<Object> message;
@Mock
MessageChannel channel;
@Mock
AuthorizationManager<Message<?>> authorizationManager;
@Mock
AuthorizationEventPublisher eventPublisher;
Authentication originalAuth;
AuthorizationChannelInterceptor interceptor;
@BeforeEach
public void setup() {
this.interceptor = new AuthorizationChannelInterceptor(this.authorizationManager);
this.originalAuth = new TestingAuthenticationToken("user", "pass", "ROLE_USER");
SecurityContextHolder.getContext().setAuthentication(this.originalAuth);
}
@AfterEach
public void cleanup() {
SecurityContextHolder.clearContext();
}
@Test
public void constructorWhenAuthorizationManagerNullThenIllegalArgument() {
assertThatIllegalArgumentException().isThrownBy(() -> new AuthorizationChannelInterceptor(null));
}
@Test
public void preSendWhenAllowThenSameMessage() {
given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true));
assertThat(this.interceptor.preSend(this.message, this.channel)).isSameAs(this.message);
}
@Test
public void preSendWhenDenyThenException() {
given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(false));
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> this.interceptor.preSend(this.message, this.channel));
}
@Test
public void setEventPublisherWhenNullThenException() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(() -> this.interceptor.setAuthorizationEventPublisher(null));
}
@Test
public void preSendWhenAuthorizationEventPublisherThenPublishes() {
this.interceptor.setAuthorizationEventPublisher(this.eventPublisher);
given(this.authorizationManager.check(any(), any())).willReturn(new AuthorizationDecision(true));
this.interceptor.preSend(this.message, this.channel);
verify(this.eventPublisher).publishAuthorizationEvent(any(), any(), any());
}
}