Add AuthorizationManager to Messaging
Closes gh-11076
This commit is contained in:
+4
-1
@@ -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";
|
||||
|
||||
+4
-1
@@ -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
|
||||
implements WebSocketMessageBrokerConfigurer, SmartInitializingSingleton {
|
||||
|
||||
|
||||
+58
@@ -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>
|
||||
* @Configuration
|
||||
* @EnableWebSocketSecurity
|
||||
* public class WebSocketSecurityConfig {
|
||||
*
|
||||
* @Bean
|
||||
* AuthorizationManager<Message<?>> (MessageMatcherDelegatingAuthorizationManager.Builder messages) {
|
||||
* messages.simpDestMatchers("/user/queue/errors").permitAll()
|
||||
* .simpDestMatchers("/admin/**").hasRole("ADMIN")
|
||||
* .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 {
|
||||
|
||||
}
|
||||
+38
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
+145
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
+135
-17
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -915,6 +915,20 @@
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="use-authorization-manager" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Use AuthorizationManager API instead of SecurityMetadatasource
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:attributeGroup name="websocket-message-broker.attlist">
|
||||
<xs:attribute name="authorization-manager-ref" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Use this AuthorizationManager instead of deriving one from <intercept-message> elements
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="intercept-message">
|
||||
<xs:annotation>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -915,6 +915,20 @@
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
<xs:attribute name="use-authorization-manager" type="xs:boolean">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Use AuthorizationManager API instead of SecurityMetadatasource
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:attributeGroup name="websocket-message-broker.attlist">
|
||||
<xs:attribute name="authorization-manager-ref" type="xs:string">
|
||||
<xs:annotation>
|
||||
<xs:documentation>Use this AuthorizationManager instead of deriving one from <intercept-message> elements
|
||||
</xs:documentation>
|
||||
</xs:annotation>
|
||||
</xs:attribute>
|
||||
</xs:attributeGroup>
|
||||
<xs:element name="intercept-message">
|
||||
<xs:annotation>
|
||||
|
||||
+176
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+785
@@ -0,0 +1,785 @@
|
||||
/*
|
||||
* 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 jakarta.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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+148
@@ -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.ChannelInterceptor;
|
||||
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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+31
@@ -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>
|
||||
+37
@@ -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>
|
||||
+46
@@ -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>
|
||||
+31
@@ -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>
|
||||
+31
@@ -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>
|
||||
+31
@@ -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>
|
||||
Reference in New Issue
Block a user