mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-06-29 23:32:14 +00:00
Remove AbstractSecurityWebSocketMessageBrokerConfigurer
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
This commit is contained in:
parent
a74ce06dae
commit
e686ac6b11
@ -1,286 +0,0 @@
|
||||
/*
|
||||
* 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.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.SmartInitializingSingleton;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
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.annotation.support.SimpAnnotationMethodMessageHandler;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.security.access.AccessDecisionVoter;
|
||||
import org.springframework.security.access.expression.SecurityExpressionHandler;
|
||||
import org.springframework.security.access.vote.AffirmativeBased;
|
||||
import org.springframework.security.config.ObjectPostProcessor;
|
||||
import org.springframework.security.config.annotation.configuration.ObjectPostProcessorConfiguration;
|
||||
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
|
||||
import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler;
|
||||
import org.springframework.security.messaging.access.expression.MessageExpressionVoter;
|
||||
import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor;
|
||||
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
|
||||
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.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.PathMatcher;
|
||||
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Allows configuring WebSocket Authorization.
|
||||
*
|
||||
* <p>
|
||||
* For example:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* @Configuration
|
||||
* public class WebSocketSecurityConfig extends
|
||||
* AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
*
|
||||
* @Override
|
||||
* protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
* messages.simpDestMatchers("/user/queue/errors").permitAll()
|
||||
* .simpDestMatchers("/admin/**").hasRole("ADMIN").anyMessage()
|
||||
* .authenticated();
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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 {
|
||||
|
||||
private final WebSocketMessageSecurityMetadataSourceRegistry inboundRegistry = new WebSocketMessageSecurityMetadataSourceRegistry();
|
||||
|
||||
private SecurityExpressionHandler<Message<Object>> defaultExpressionHandler = new DefaultMessageSecurityExpressionHandler<>();
|
||||
|
||||
private SecurityExpressionHandler<Message<Object>> expressionHandler;
|
||||
|
||||
private ApplicationContext context;
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
|
||||
argumentResolvers.add(new AuthenticationPrincipalArgumentResolver());
|
||||
}
|
||||
|
||||
@Override
|
||||
public final void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
ChannelSecurityInterceptor inboundChannelSecurity = this.context.getBean(ChannelSecurityInterceptor.class);
|
||||
registration.interceptors(this.context.getBean(SecurityContextChannelInterceptor.class));
|
||||
if (!sameOriginDisabled()) {
|
||||
registration.interceptors(this.context.getBean(CsrfChannelInterceptor.class));
|
||||
}
|
||||
if (this.inboundRegistry.containsMapping()) {
|
||||
registration.interceptors(inboundChannelSecurity);
|
||||
}
|
||||
customizeClientInboundChannel(registration);
|
||||
}
|
||||
|
||||
private PathMatcher getDefaultPathMatcher() {
|
||||
try {
|
||||
return this.context.getBean(SimpAnnotationMethodMessageHandler.class).getPathMatcher();
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
return new AntPathMatcher();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* Determines if a CSRF token is required for connecting. This protects against remote
|
||||
* sites from connecting to the application and being able to read/write data over the
|
||||
* connection. The default is false (the token is required).
|
||||
* </p>
|
||||
* <p>
|
||||
* Subclasses can override this method to disable CSRF protection
|
||||
* </p>
|
||||
* @return false if a CSRF token is required for connecting, else true
|
||||
*/
|
||||
protected boolean sameOriginDisabled() {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows subclasses to customize the configuration of the {@link ChannelRegistration}
|
||||
* .
|
||||
* @param registration the {@link ChannelRegistration} to customize
|
||||
*/
|
||||
protected void customizeClientInboundChannel(ChannelRegistration registration) {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CsrfChannelInterceptor csrfChannelInterceptor() {
|
||||
return new CsrfChannelInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ChannelSecurityInterceptor inboundChannelSecurity(
|
||||
MessageSecurityMetadataSource messageSecurityMetadataSource) {
|
||||
ChannelSecurityInterceptor channelSecurityInterceptor = new ChannelSecurityInterceptor(
|
||||
messageSecurityMetadataSource);
|
||||
MessageExpressionVoter<Object> voter = new MessageExpressionVoter<>();
|
||||
voter.setExpressionHandler(getMessageExpressionHandler());
|
||||
List<AccessDecisionVoter<?>> voters = new ArrayList<>();
|
||||
voters.add(voter);
|
||||
AffirmativeBased manager = new AffirmativeBased(voters);
|
||||
channelSecurityInterceptor.setAccessDecisionManager(manager);
|
||||
return channelSecurityInterceptor;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityContextChannelInterceptor securityContextChannelInterceptor() {
|
||||
return new SecurityContextChannelInterceptor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MessageSecurityMetadataSource inboundMessageSecurityMetadataSource() {
|
||||
this.inboundRegistry.expressionHandler(getMessageExpressionHandler());
|
||||
configureInbound(this.inboundRegistry);
|
||||
return this.inboundRegistry.createMetadataSource();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param messages
|
||||
*/
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
}
|
||||
|
||||
@Autowired
|
||||
public void setApplicationContext(ApplicationContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
public void setMessageExpessionHandler(List<SecurityExpressionHandler<Message<Object>>> expressionHandlers) {
|
||||
setMessageExpressionHandler(expressionHandlers);
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setMessageExpressionHandler(List<SecurityExpressionHandler<Message<Object>>> expressionHandlers) {
|
||||
if (expressionHandlers.size() == 1) {
|
||||
this.expressionHandler = expressionHandlers.get(0);
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired(required = false)
|
||||
public void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
|
||||
this.defaultExpressionHandler = objectPostProcessor.postProcess(this.defaultExpressionHandler);
|
||||
}
|
||||
|
||||
private SecurityExpressionHandler<Message<Object>> getMessageExpressionHandler() {
|
||||
if (this.expressionHandler == null) {
|
||||
return this.defaultExpressionHandler;
|
||||
}
|
||||
return this.expressionHandler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
if (sameOriginDisabled()) {
|
||||
return;
|
||||
}
|
||||
String beanName = "stompWebSocketHandlerMapping";
|
||||
SimpleUrlHandlerMapping mapping = this.context.getBean(beanName, SimpleUrlHandlerMapping.class);
|
||||
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 " + beanName + " is expected to contain mappings to either a "
|
||||
+ "SockJsHttpRequestHandler or a WebSocketHttpRequestHandler but got " + object);
|
||||
}
|
||||
}
|
||||
if (this.inboundRegistry.containsMapping() && !this.inboundRegistry.isSimpDestPathMatcherConfigured()) {
|
||||
PathMatcher pathMatcher = getDefaultPathMatcher();
|
||||
this.inboundRegistry.simpDestPathMatcher(pathMatcher);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static class WebSocketMessageSecurityMetadataSourceRegistry extends MessageSecurityMetadataSourceRegistry {
|
||||
|
||||
@Override
|
||||
public MessageSecurityMetadataSource createMetadataSource() {
|
||||
return super.createMetadataSource();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean containsMapping() {
|
||||
return super.containsMapping();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isSimpDestPathMatcherConfigured() {
|
||||
return super.isSimpDestPathMatcherConfigured();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,178 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
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 AbstractSecurityWebSocketMessageBrokerConfigurerDocTests {
|
||||
|
||||
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
|
||||
static class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages.nullDestMatcher()
|
||||
.authenticated()
|
||||
// <1>
|
||||
.simpSubscribeDestMatchers("/user/queue/errors")
|
||||
.permitAll()
|
||||
// <2>
|
||||
.simpDestMatchers("/app/**")
|
||||
.hasRole("USER")
|
||||
// <3>
|
||||
.simpSubscribeDestMatchers("/user/**", "/topic/friends/*")
|
||||
.hasRole("USER") // <4>
|
||||
.simpTypeMatchers(SimpMessageType.MESSAGE, SimpMessageType.SUBSCRIBE)
|
||||
.denyAll() // <5>
|
||||
.anyMessage()
|
||||
.denyAll(); // <6>
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,733 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.Map;
|
||||
|
||||
import jakarta.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.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.access.expression.SecurityExpressionHandler;
|
||||
import org.springframework.security.access.expression.SecurityExpressionOperations;
|
||||
import org.springframework.security.authentication.TestingAuthenticationToken;
|
||||
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.expression.DefaultMessageSecurityExpressionHandler;
|
||||
import org.springframework.security.messaging.access.expression.MessageSecurityExpressionRoot;
|
||||
import org.springframework.security.messaging.access.intercept.ChannelSecurityInterceptor;
|
||||
import org.springframework.security.messaging.access.intercept.MessageSecurityMetadataSource;
|
||||
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.DeferredCsrfToken;
|
||||
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.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.springframework.security.web.csrf.CsrfTokenAssert.assertThatCsrfToken;
|
||||
|
||||
public class AbstractSecurityWebSocketMessageBrokerConfigurerTests {
|
||||
|
||||
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();
|
||||
CsrfChannelInterceptor csrfChannelInterceptor = this.context.getBean(CsrfChannelInterceptor.class);
|
||||
assertThat(((AbstractMessageChannel) messageChannel).getInterceptors()).contains(csrfChannelInterceptor);
|
||||
}
|
||||
|
||||
@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);
|
||||
ChannelSecurityInterceptor channelSecurityInterceptor = this.context.getBean(ChannelSecurityInterceptor.class);
|
||||
MessageSecurityMetadataSource messageSecurityMetadataSource = this.context
|
||||
.getBean(MessageSecurityMetadataSource.class);
|
||||
assertThat(channelSecurityInterceptor.obtainSecurityMetadataSource()).isSameAs(messageSecurityMetadataSource);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void securityContextChannelInterceptorDefinedByBean() {
|
||||
loadConfig(SockJsProxylessSecurityConfig.class);
|
||||
MessageChannel messageChannel = clientInboundChannel();
|
||||
SecurityContextChannelInterceptor securityContextChannelInterceptor = this.context
|
||||
.getBean(SecurityContextChannelInterceptor.class);
|
||||
assertThat(((AbstractMessageChannel) messageChannel).getInterceptors())
|
||||
.contains(securityContextChannelInterceptor);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void inboundChannelSecurityDefinedByBean() {
|
||||
loadConfig(SockJsProxylessSecurityConfig.class);
|
||||
MessageChannel messageChannel = clientInboundChannel();
|
||||
ChannelSecurityInterceptor inboundChannelSecurity = this.context.getBean(ChannelSecurityInterceptor.class);
|
||||
assertThat(((AbstractMessageChannel) messageChannel).getInterceptors()).contains(inboundChannelSecurity);
|
||||
}
|
||||
|
||||
private void assertHandshake(HttpServletRequest request) {
|
||||
TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class);
|
||||
assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token);
|
||||
assertThat(handshakeHandler.attributes).containsEntry(this.sessionAttr,
|
||||
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(DeferredCsrfToken.class.getName(), new TestDeferredCsrfToken(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 MessageChannel clientInboundChannel() {
|
||||
return 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
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class MsmsRegistryCustomPatternMatcherConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/other")
|
||||
.setHandshakeHandler(testHandshakeHandler());
|
||||
}
|
||||
// @formatter:on
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.simpDestMatchers("/app/a.*").permitAll()
|
||||
.anyMessage().denyAll();
|
||||
}
|
||||
// @formatter:on
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.setPathMatcher(new AntPathMatcher("."));
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestHandshakeHandler testHandshakeHandler() {
|
||||
return new TestHandshakeHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class OverrideMsmsRegistryCustomPatternMatcherConfig
|
||||
extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/other")
|
||||
.setHandshakeHandler(testHandshakeHandler());
|
||||
}
|
||||
// @formatter:on
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.simpDestPathMatcher(new AntPathMatcher())
|
||||
.simpDestMatchers("/app/a/*").permitAll()
|
||||
.anyMessage().denyAll();
|
||||
}
|
||||
// @formatter:on
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.setPathMatcher(new AntPathMatcher("."));
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestHandshakeHandler testHandshakeHandler() {
|
||||
return new TestHandshakeHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class DefaultPatternMatcherConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/other")
|
||||
.setHandshakeHandler(testHandshakeHandler());
|
||||
}
|
||||
// @formatter:on
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.simpDestMatchers("/app/a/*").permitAll()
|
||||
.anyMessage().denyAll();
|
||||
}
|
||||
// @formatter:on
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestHandshakeHandler testHandshakeHandler() {
|
||||
return new TestHandshakeHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class CustomExpressionConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/other")
|
||||
.setHandshakeHandler(testHandshakeHandler());
|
||||
}
|
||||
// @formatter:on
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.anyMessage().access("denyRob()");
|
||||
}
|
||||
// @formatter:on
|
||||
@Bean
|
||||
static SecurityExpressionHandler<Message<Object>> messageSecurityExpressionHandler() {
|
||||
return new DefaultMessageSecurityExpressionHandler<Object>() {
|
||||
@Override
|
||||
protected SecurityExpressionOperations createSecurityExpressionRoot(Authentication authentication,
|
||||
Message<Object> invocation) {
|
||||
return new MessageSecurityExpressionRoot(authentication, invocation) {
|
||||
public boolean denyRob() {
|
||||
Authentication auth = getAuthentication();
|
||||
return auth != null && !"rob".equals(auth.getName());
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestHandshakeHandler testHandshakeHandler() {
|
||||
return new TestHandshakeHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class MyController {
|
||||
|
||||
String authenticationPrincipal;
|
||||
|
||||
MyCustomArgument myCustomArgument;
|
||||
|
||||
@MessageMapping("/authentication")
|
||||
public void authentication(@AuthenticationPrincipal String un) {
|
||||
this.authenticationPrincipal = un;
|
||||
}
|
||||
|
||||
@MessageMapping("/myCustom")
|
||||
public 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
|
||||
@EnableWebSocketMessageBroker
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class SockJsSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@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
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.simpDestMatchers("/permitAll/**").permitAll()
|
||||
.simpDestMatchers("/beanResolver/**").access("@security.check()")
|
||||
.anyMessage().denyAll();
|
||||
}
|
||||
// @formatter:on
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MyController myController() {
|
||||
return new MyController();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestHandshakeHandler testHandshakeHandler() {
|
||||
return new TestHandshakeHandler();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityCheck security() {
|
||||
return new SecurityCheck();
|
||||
}
|
||||
|
||||
static class SecurityCheck {
|
||||
|
||||
private boolean check;
|
||||
|
||||
public boolean check() {
|
||||
this.check = !this.check;
|
||||
return this.check;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class NoInboundSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@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
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.enableSimpleBroker("/queue/", "/topic/");
|
||||
registry.setApplicationDestinationPrefixes("/permitAll", "/denyAll");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public MyController myController() {
|
||||
return new MyController();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class CsrfDisabledSockJsSecurityConfig extends SockJsSecurityConfig {
|
||||
|
||||
@Override
|
||||
protected boolean sameOriginDisabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
// @formatter:off
|
||||
registry.addEndpoint("/websocket")
|
||||
.setHandshakeHandler(testHandshakeHandler())
|
||||
.addInterceptors(new HttpSessionHandshakeInterceptor());
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
// @formatter:off
|
||||
messages
|
||||
.simpDestMatchers("/permitAll/**").permitAll()
|
||||
.simpDestMatchers("/customExpression/**").access("denyRob")
|
||||
.anyMessage().denyAll();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
@Bean
|
||||
public TestHandshakeHandler testHandshakeHandler() {
|
||||
return new TestHandshakeHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableWebSocketMessageBroker
|
||||
@Import(SyncExecutorConfig.class)
|
||||
static class SockJsProxylessSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
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
|
||||
public void setContext(ApplicationContext context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
// @formatter:off
|
||||
@Override
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.anyMessage().denyAll();
|
||||
}
|
||||
// @formatter:on
|
||||
@Bean
|
||||
public TestHandshakeHandler testHandshakeHandler() {
|
||||
return new TestHandshakeHandler();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class SyncExecutorConfig {
|
||||
|
||||
@Bean
|
||||
public static SyncExecutorSubscribableChannelPostProcessor postProcessor() {
|
||||
return new SyncExecutorSubscribableChannelPostProcessor();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -69,7 +69,6 @@ import org.springframework.security.authorization.AuthorizationDecision;
|
||||
import org.springframework.security.authorization.AuthorizationManager;
|
||||
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
|
||||
import org.springframework.security.config.observation.SecurityObservationSettings;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
|
||||
@ -878,37 +877,6 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@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
|
||||
|
@ -492,43 +492,6 @@ Xml::
|
||||
----
|
||||
======
|
||||
|
||||
On the other hand, if you are using the <<legacy-websocket-configuration,legacy `AbstractSecurityWebSocketMessageBrokerConfigurer`>> and you want to allow other domains to access your site, you can disable Spring Security's protection.
|
||||
For example, in Java Configuration you can use the following:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
|
||||
|
||||
...
|
||||
|
||||
@Override
|
||||
protected boolean sameOriginDisabled() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() {
|
||||
|
||||
// ...
|
||||
|
||||
override fun sameOriginDisabled(): Boolean {
|
||||
return true
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[websocket-expression-handler]]
|
||||
=== Custom Expression Handler
|
||||
|
||||
@ -742,50 +705,3 @@ If we use XML-based configuration, we can use thexref:servlet/appendix/namespace
|
||||
</b:constructor-arg>
|
||||
</b:bean>
|
||||
----
|
||||
|
||||
[[legacy-websocket-configuration]]
|
||||
== Legacy WebSocket Configuration
|
||||
|
||||
Before Spring Security 5.8, the way to configure messaging authorization using Java Configuration, was to extend the `AbstractSecurityWebSocketMessageBrokerConfigurer` and configure the `MessageSecurityMetadataSourceRegistry`.
|
||||
For example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,role="primary"]
|
||||
----
|
||||
@Configuration
|
||||
public class WebSocketSecurityConfig
|
||||
extends AbstractSecurityWebSocketMessageBrokerConfigurer { // <1> <2>
|
||||
|
||||
protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
|
||||
messages
|
||||
.simpDestMatchers("/user/**").authenticated() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,role="secondary"]
|
||||
----
|
||||
@Configuration
|
||||
open class WebSocketSecurityConfig : AbstractSecurityWebSocketMessageBrokerConfigurer() { // <1> <2>
|
||||
override fun configureInbound(messages: MessageSecurityMetadataSourceRegistry) {
|
||||
messages.simpDestMatchers("/user/**").authenticated() // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
This will ensure that:
|
||||
|
||||
<1> Any inbound CONNECT message requires a valid CSRF token to enforce <<websocket-sameorigin,Same Origin Policy>>
|
||||
<2> The SecurityContextHolder is populated with the user within the simpUser header attribute for any inbound request.
|
||||
<3> Our messages require the proper authorization. Specifically, any inbound message that starts with "/user/" will require ROLE_USER. Additional details on authorization can be found in <<websocket-authorization>>
|
||||
|
||||
Using the legacy configuration is helpful in the event that you have a custom `SecurityExpressionHandler` that extends `AbstractSecurityExpressionHandler` and overrides `createEvaluationContextInternal` or `createSecurityExpressionRoot`.
|
||||
In order to defer `Authorization` lookup, the new `AuthorizationManager` API does not invoke these when evaluating expressions.
|
||||
|
||||
If you are using XML, you can use the legacy APIs simply by not using the `use-authorization-manager` element or setting it to `false`.
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
<!-- Method Visibility that we can't reduce -->
|
||||
<suppress files="AbstractAclVoterTests\.java" checks="SpringMethodVisibility"/>
|
||||
<suppress files="AbstractSecurityWebSocketMessageBrokerConfigurerTests\.java" checks="SpringMethodVisibility"/>
|
||||
<suppress files="AnnotationParameterNameDiscovererTests\.java" checks="SpringMethodVisibility"/>
|
||||
<suppress files="AnnotationSecurityAspectTests\.java" checks="SpringMethodVisibility"/>
|
||||
<suppress files="AuthenticationPrincipalArgumentResolverTests\.java" checks="SpringMethodVisibility"/>
|
||||
|
Loading…
x
Reference in New Issue
Block a user