|
@@ -0,0 +1,796 @@
|
|
|
+/*
|
|
|
+ * 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.Collectors;
|
|
|
+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.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.configuration.EnableWebSecurity;
|
|
|
+import org.springframework.security.config.annotation.web.messaging.AuthorizationManagerMessageMatcherRegistry;
|
|
|
+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.ChannelSecurityInterceptor;
|
|
|
+import org.springframework.security.messaging.access.intercept.MessageAuthorizationContext;
|
|
|
+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
|
|
|
+ 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);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ public void usingLegacyThenNewFiltersNotUsed() {
|
|
|
+ loadConfig(UsingLegacyConfigurerConfig.class);
|
|
|
+ AbstractMessageChannel messageChannel = clientInboundChannel();
|
|
|
+ List<Class<? extends ChannelInterceptor>> interceptors = messageChannel.getInterceptors().stream()
|
|
|
+ .map(ChannelInterceptor::getClass).collect(Collectors.toList());
|
|
|
+ assertThat(interceptors).contains(ChannelSecurityInterceptor.class);
|
|
|
+ assertThat(interceptors).doesNotContain(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
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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(AuthorizationManagerMessageMatcherRegistry messages) {
|
|
|
+ messages
|
|
|
+ .simpDestMatchers("/app/a.*").permitAll()
|
|
|
+ .anyMessage().denyAll();
|
|
|
+
|
|
|
+ return messages.build();
|
|
|
+ }
|
|
|
+ // @formatter:on
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ TestHandshakeHandler testHandshakeHandler() {
|
|
|
+ return new TestHandshakeHandler();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration
|
|
|
+ @EnableWebSocketMessageBroker
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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(AuthorizationManagerMessageMatcherRegistry messages) {
|
|
|
+ messages
|
|
|
+ .simpDestPathMatcher(new AntPathMatcher())
|
|
|
+ .simpDestMatchers("/app/a/*").permitAll()
|
|
|
+ .anyMessage().denyAll();
|
|
|
+ return messages.build();
|
|
|
+ }
|
|
|
+ // @formatter:on
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ TestHandshakeHandler testHandshakeHandler() {
|
|
|
+ return new TestHandshakeHandler();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration
|
|
|
+ @EnableWebSocketMessageBroker
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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(AuthorizationManagerMessageMatcherRegistry messages) {
|
|
|
+ messages
|
|
|
+ .simpDestMatchers("/app/a/*").permitAll()
|
|
|
+ .anyMessage().denyAll();
|
|
|
+
|
|
|
+ return messages.build();
|
|
|
+ }
|
|
|
+ // @formatter:on
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ TestHandshakeHandler testHandshakeHandler() {
|
|
|
+ return new TestHandshakeHandler();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration
|
|
|
+ @EnableWebSocketMessageBroker
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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(AuthorizationManagerMessageMatcherRegistry 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
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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(AuthorizationManagerMessageMatcherRegistry messages) {
|
|
|
+ // @formatter:off
|
|
|
+ messages
|
|
|
+ .simpDestMatchers("/permitAll/**").permitAll()
|
|
|
+ .anyMessage().denyAll();
|
|
|
+ // @formatter:on
|
|
|
+ return messages.build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Bean
|
|
|
+ TestHandshakeHandler testHandshakeHandler() {
|
|
|
+ return new TestHandshakeHandler();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ @Configuration
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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)
|
|
|
+ @EnableWebSecurity
|
|
|
+ @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(AuthorizationManagerMessageMatcherRegistry 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();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+}
|