Use PathPatternMessageMatcher By Default

Issue gh-17501
This commit is contained in:
Josh Cummings 2025-07-10 13:54:15 -06:00
parent ff7359b54a
commit 684775b46a
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
9 changed files with 96 additions and 163 deletions

View File

@ -16,29 +16,24 @@
package org.springframework.security.config.annotation.web.socket; package org.springframework.security.config.annotation.web.socket;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Fallback;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.messaging.simp.annotation.support.SimpAnnotationMethodMessageHandler; import org.springframework.security.config.web.messaging.PathPatternMessageMatcherBuilderFactoryBean;
import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager; import org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager;
import org.springframework.security.messaging.util.matcher.MessageMatcherFactory;
import org.springframework.util.AntPathMatcher;
final class MessageMatcherAuthorizationManagerConfiguration { final class MessageMatcherAuthorizationManagerConfiguration {
@Bean
@Fallback
PathPatternMessageMatcherBuilderFactoryBean messageMatcherBuilderFactoryBean() {
return new PathPatternMessageMatcherBuilderFactoryBean();
}
@Bean @Bean
@Scope("prototype") @Scope("prototype")
MessageMatcherDelegatingAuthorizationManager.Builder messageAuthorizationManagerBuilder( MessageMatcherDelegatingAuthorizationManager.Builder messageAuthorizationManagerBuilder() {
ApplicationContext context) { return MessageMatcherDelegatingAuthorizationManager.builder();
MessageMatcherFactory.setApplicationContext(context);
if (MessageMatcherFactory.usesPathPatterns()) {
return MessageMatcherDelegatingAuthorizationManager.builder();
}
return MessageMatcherDelegatingAuthorizationManager.builder()
.simpDestPathMatcher(
() -> (context.getBeanNamesForType(SimpAnnotationMethodMessageHandler.class).length > 0)
? context.getBean(SimpAnnotationMethodMessageHandler.class).getPathMatcher()
: new AntPathMatcher());
} }
} }

View File

@ -23,9 +23,6 @@ import org.springframework.context.ApplicationContextAware;
import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.SimpMessageType;
import org.springframework.security.messaging.util.matcher.MessageMatcher; import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher; import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;
@Deprecated @Deprecated
public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatcher<?>>, ApplicationContextAware { public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatcher<?>>, ApplicationContextAware {
@ -36,8 +33,6 @@ public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatch
private final String path; private final String path;
private PathMatcher pathMatcher = new AntPathMatcher();
public MessageMatcherFactoryBean(String path) { public MessageMatcherFactoryBean(String path) {
this(path, null); this(path, null);
} }
@ -49,16 +44,7 @@ public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatch
@Override @Override
public MessageMatcher<?> getObject() throws Exception { public MessageMatcher<?> getObject() throws Exception {
if (this.builder != null) { return this.builder.matcher(this.method, this.path);
return this.builder.matcher(this.method, this.path);
}
if (this.method == SimpMessageType.SUBSCRIBE) {
return SimpDestinationMessageMatcher.createSubscribeMatcher(this.path, this.pathMatcher);
}
if (this.method == SimpMessageType.MESSAGE) {
return SimpDestinationMessageMatcher.createMessageMatcher(this.path, this.pathMatcher);
}
return new SimpDestinationMessageMatcher(this.path, this.pathMatcher);
} }
@Override @Override
@ -66,13 +52,9 @@ public final class MessageMatcherFactoryBean implements FactoryBean<MessageMatch
return null; return null;
} }
public void setPathMatcher(PathMatcher pathMatcher) {
this.pathMatcher = pathMatcher;
}
@Override @Override
public void setApplicationContext(ApplicationContext context) throws BeansException { public void setApplicationContext(ApplicationContext context) throws BeansException {
this.builder = context.getBeanProvider(PathPatternMessageMatcher.Builder.class).getIfUnique(); this.builder = context.getBean(PathPatternMessageMatcher.Builder.class);
} }
} }

View File

@ -31,9 +31,9 @@ import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValue;
import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
@ -56,6 +56,7 @@ import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.config.Elements; import org.springframework.security.config.Elements;
import org.springframework.security.config.http.MessageMatcherFactoryBean; import org.springframework.security.config.http.MessageMatcherFactoryBean;
import org.springframework.security.config.web.messaging.PathPatternMessageMatcherBuilderFactoryBean;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.core.context.SecurityContextHolderStrategy;
@ -134,7 +135,7 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
private static final String TYPE_ATTR = "type"; private static final String TYPE_ATTR = "type";
private static final String PATH_MATCHER_BEAN_NAME = "springSecurityMessagePathMatcher"; private static final String MESSAGE_MATCHER_BUILDER_BEAN_NAME = "HttpConfigurationBuilder-pathPatternMessageMatcherBuilder";
/** /**
* @param element * @param element
@ -144,13 +145,17 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
@Override @Override
public BeanDefinition parse(Element element, ParserContext parserContext) { public BeanDefinition parse(Element element, ParserContext parserContext) {
String id = element.getAttribute(ID_ATTR); String id = element.getAttribute(ID_ATTR);
if (!parserContext.getRegistry().containsBeanDefinition(MESSAGE_MATCHER_BUILDER_BEAN_NAME)) {
BeanDefinitionBuilder pathPatternMessageMatcherBuilder = BeanDefinitionBuilder
.rootBeanDefinition(PathPatternMessageMatcherBuilderFactoryBean.class);
pathPatternMessageMatcherBuilder.setFallback(true);
BeanDefinition bean = pathPatternMessageMatcherBuilder.getBeanDefinition();
parserContext.registerBeanComponent(new BeanComponentDefinition(bean, MESSAGE_MATCHER_BUILDER_BEAN_NAME));
}
String inSecurityInterceptorName = parseAuthorization(element, parserContext); String inSecurityInterceptorName = parseAuthorization(element, parserContext);
BeanDefinitionRegistry registry = parserContext.getRegistry(); BeanDefinitionRegistry registry = parserContext.getRegistry();
if (StringUtils.hasText(id)) { if (StringUtils.hasText(id)) {
registry.registerAlias(inSecurityInterceptorName, id); registry.registerAlias(inSecurityInterceptorName, id);
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
}
} }
else { else {
boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR)); boolean sameOriginDisabled = Boolean.parseBoolean(element.getAttribute(DISABLED_ATTR));
@ -286,7 +291,6 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
+ " with a pattern because the type does not have a destination.", interceptMessage); + " with a pattern because the type does not have a destination.", interceptMessage);
} }
} }
matcher.addPropertyValue("pathMatcher", new RuntimeBeanReference("springSecurityMessagePathMatcher"));
return matcher.getBeanDefinition(); return matcher.getBeanDefinition();
} }
@ -342,13 +346,6 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
} }
argResolvers.add(beanDefinition); argResolvers.add(beanDefinition);
bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers); bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers);
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
PropertyValue pathMatcherProp = bd.getPropertyValues().getPropertyValue("pathMatcher");
Object pathMatcher = (pathMatcherProp != null) ? pathMatcherProp.getValue() : null;
if (pathMatcher instanceof BeanReference) {
registry.registerAlias(((BeanReference) pathMatcher).getBeanName(), PATH_MATCHER_BEAN_NAME);
}
}
} }
else if (CSRF_HANDSHAKE_HANDLER_CLASSES.contains(beanClassName)) { else if (CSRF_HANDSHAKE_HANDLER_CLASSES.contains(beanClassName)) {
addCsrfTokenHandshakeInterceptor(bd); addCsrfTokenHandshakeInterceptor(bd);
@ -376,9 +373,6 @@ public final class WebSocketMessageBrokerSecurityBeanDefinitionParser implements
interceptors.addAll(currentInterceptors); interceptors.addAll(currentInterceptors);
} }
inboundChannel.getPropertyValues().add(INTERCEPTORS_PROP, interceptors); inboundChannel.getPropertyValues().add(INTERCEPTORS_PROP, interceptors);
if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
registry.registerBeanDefinition(PATH_MATCHER_BEAN_NAME, new RootBeanDefinition(AntPathMatcher.class));
}
} }
private void addCsrfTokenHandshakeInterceptor(BeanDefinition bd) { private void addCsrfTokenHandshakeInterceptor(BeanDefinition bd) {

View File

@ -45,6 +45,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.http.server.PathContainer;
import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
@ -70,6 +71,7 @@ import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.observation.SecurityObservationSettings;
import org.springframework.security.config.web.messaging.PathPatternMessageMatcherBuilderFactoryBean;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults;
import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.annotation.AuthenticationPrincipal;
@ -99,6 +101,7 @@ import org.springframework.web.socket.server.HandshakeHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor; import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor;
import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler; import org.springframework.web.socket.sockjs.transport.handler.SockJsWebSocketHandler;
import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession; import org.springframework.web.socket.sockjs.transport.session.WebSocketServerSockJsSession;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@ -507,6 +510,13 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
@Import(SyncExecutorConfig.class) @Import(SyncExecutorConfig.class)
static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer { static class MsmsRegistryCustomPatternMatcherConfig implements WebSocketMessageBrokerConfigurer {
@Bean
PathPatternMessageMatcherBuilderFactoryBean messageMatcherBuilder() {
PathPatternParser parser = new PathPatternParser();
parser.setPathOptions(PathContainer.Options.MESSAGE_ROUTE);
return new PathPatternMessageMatcherBuilderFactoryBean(parser);
}
// @formatter:off // @formatter:off
@Override @Override
public void registerStompEndpoints(StompEndpointRegistry registry) { public void registerStompEndpoints(StompEndpointRegistry registry) {
@ -518,7 +528,6 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
@Override @Override
public void configureMessageBroker(MessageBrokerRegistry registry) { public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setPathMatcher(new AntPathMatcher("."));
registry.enableSimpleBroker("/queue/", "/topic/"); registry.enableSimpleBroker("/queue/", "/topic/");
registry.setApplicationDestinationPrefixes("/app"); registry.setApplicationDestinationPrefixes("/app");
} }
@ -567,7 +576,6 @@ public class WebSocketMessageBrokerSecurityConfigurationTests {
@Bean @Bean
AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) { AuthorizationManager<Message<?>> authorizationManager(MessageMatcherDelegatingAuthorizationManager.Builder messages) {
messages messages
.simpDestPathMatcher(new AntPathMatcher())
.simpDestMatchers("/app/a/*").permitAll() .simpDestMatchers("/app/a/*").permitAll()
.anyMessage().denyAll(); .anyMessage().denyAll();
return messages.build(); return messages.build();

View File

@ -23,7 +23,6 @@ import org.springframework.expression.Expression;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.messaging.util.matcher.MessageMatcher; import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -42,7 +41,7 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
private final Expression authorizeExpression; private final Expression authorizeExpression;
private final MessageMatcher<?> matcher; private final MessageMatcher<Object> matcher;
/** /**
* Creates a new instance * Creates a new instance
@ -53,7 +52,7 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
Assert.notNull(authorizeExpression, "authorizeExpression cannot be null"); Assert.notNull(authorizeExpression, "authorizeExpression cannot be null");
Assert.notNull(matcher, "matcher cannot be null"); Assert.notNull(matcher, "matcher cannot be null");
this.authorizeExpression = authorizeExpression; this.authorizeExpression = authorizeExpression;
this.matcher = matcher; this.matcher = (MessageMatcher<Object>) matcher;
} }
Expression getAuthorizeExpression() { Expression getAuthorizeExpression() {
@ -72,12 +71,9 @@ class MessageExpressionConfigAttribute implements ConfigAttribute, EvaluationCon
@Override @Override
public EvaluationContext postProcess(EvaluationContext ctx, Message<?> message) { public EvaluationContext postProcess(EvaluationContext ctx, Message<?> message) {
if (this.matcher instanceof SimpDestinationMessageMatcher) { Map<String, String> variables = this.matcher.matcher(message).getVariables();
Map<String, String> variables = ((SimpDestinationMessageMatcher) this.matcher) for (Map.Entry<String, String> entry : variables.entrySet()) {
.extractPathVariables(message); ctx.setVariable(entry.getKey(), entry.getValue());
for (Map.Entry<String, String> entry : variables.entrySet()) {
ctx.setVariable(entry.getKey(), entry.getValue());
}
} }
return ctx; return ctx;
} }

View File

@ -23,6 +23,9 @@ import java.util.function.Supplier;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.messaging.Message; import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageType; import org.springframework.messaging.simp.SimpMessageType;
@ -33,15 +36,10 @@ import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.SingleResultAuthorizationManager; import org.springframework.security.authorization.SingleResultAuthorizationManager;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.util.matcher.MessageMatcher; import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.MessageMatcherFactory;
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher; import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher; import org.springframework.security.messaging.util.matcher.SimpMessageTypeMatcher;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import org.springframework.util.PathMatcher;
import org.springframework.util.function.SingletonSupplier;
public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> { public final class MessageMatcherDelegatingAuthorizationManager implements AuthorizationManager<Message<?>> {
@ -99,12 +97,11 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
/** /**
* A builder for {@link MessageMatcherDelegatingAuthorizationManager}. * A builder for {@link MessageMatcherDelegatingAuthorizationManager}.
*/ */
public static final class Builder { public static final class Builder implements ApplicationContextAware {
private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings = new ArrayList<>(); private final List<Entry<AuthorizationManager<MessageAuthorizationContext<?>>>> mappings = new ArrayList<>();
@Deprecated private PathPatternMessageMatcher.Builder messageMatcherBuilder = PathPatternMessageMatcher.withDefaults();
private Supplier<PathMatcher> pathMatcher = AntPathMatcher::new;
public Builder() { public Builder() {
} }
@ -142,11 +139,9 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
} }
/** /**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or * Maps a {@link List} of {@link PathPatternMessageMatcher}s instances without
* {@link PathPatternMessageMatcher} if the application has configured a * regard to the {@link SimpMessageType}. If no destination is found on the
* {@link PathPatternMessageMatcher.Builder} bean) instances without regard to the * Message, then the Matcher returns false.
* {@link SimpMessageType}. If no destination is found on the Message, then the
* Matcher returns false.
* @param patterns the patterns to create {@code MessageMatcher}s from. * @param patterns the patterns to create {@code MessageMatcher}s from.
*/ */
public Builder.Constraint simpDestMatchers(String... patterns) { public Builder.Constraint simpDestMatchers(String... patterns) {
@ -154,10 +149,8 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
} }
/** /**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or * Maps a {@link List} of {@link PathPatternMessageMatcher}s instances that match
* {@link PathPatternMessageMatcher} if the application has configured a * on {@code SimpMessageType.MESSAGE}. If no destination is found on the Message,
* {@link PathPatternMessageMatcher.Builder} bean) instances that match on
* {@code SimpMessageType.MESSAGE}. If no destination is found on the Message,
* then the Matcher returns false. * then the Matcher returns false.
* @param patterns the patterns to create {@code MessageMatcher}s from. * @param patterns the patterns to create {@code MessageMatcher}s from.
*/ */
@ -166,11 +159,9 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
} }
/** /**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or * Maps a {@link List} of {@link PathPatternMessageMatcher}s instances that match
* {@link PathPatternMessageMatcher} if the application has configured a * on {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the
* {@link PathPatternMessageMatcher.Builder} bean) instances that match on * Message, then the Matcher returns false.
* {@code SimpMessageType.SUBSCRIBE}. If no destination is found on the Message,
* then the Matcher returns false.
* @param patterns the patterns to create {@code MessageMatcher}s from. * @param patterns the patterns to create {@code MessageMatcher}s from.
*/ */
public Builder.Constraint simpSubscribeDestMatchers(String... patterns) { public Builder.Constraint simpSubscribeDestMatchers(String... patterns) {
@ -178,10 +169,8 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
} }
/** /**
* Maps a {@link List} of {@link SimpDestinationMessageMatcher} (or * Maps a {@link List} of {@link PathPatternMessageMatcher} instances. If no
* {@link PathPatternMessageMatcher} if the application has configured a * destination is found on the Message, then the Matcher returns false.
* {@link PathPatternMessageMatcher.Builder} bean) instances. If no destination is
* found on the Message, then the Matcher returns false.
* @param type the {@link SimpMessageType} to match on. If null, the * @param type the {@link SimpMessageType} to match on. If null, the
* {@link SimpMessageType} is not considered for matching. * {@link SimpMessageType} is not considered for matching.
* @param patterns the patterns to create {@code MessageMatcher}s from. * @param patterns the patterns to create {@code MessageMatcher}s from.
@ -191,44 +180,12 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) { private Builder.Constraint simpDestMatchers(SimpMessageType type, String... patterns) {
List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length); List<MessageMatcher<?>> matchers = new ArrayList<>(patterns.length);
for (String pattern : patterns) { for (String pattern : patterns) {
MessageMatcher<Object> matcher = MessageMatcherFactory.usesPathPatterns() MessageMatcher<Object> matcher = this.messageMatcherBuilder.matcher(type, pattern);
? MessageMatcherFactory.matcher(type, pattern)
: new LazySimpDestinationMessageMatcher(pattern, type);
matchers.add(matcher); matchers.add(matcher);
} }
return new Builder.Constraint(matchers); return new Builder.Constraint(matchers);
} }
/**
* The {@link PathMatcher} to be used with the
* {@link Builder#simpDestMatchers(String...)}. The default is to use the default
* constructor of {@link AntPathMatcher}.
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null.
* @return the {@link Builder} for further customization.
* @deprecated
*/
@Deprecated
public Builder simpDestPathMatcher(PathMatcher pathMatcher) {
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
this.pathMatcher = () -> pathMatcher;
return this;
}
/**
* The {@link PathMatcher} to be used with the
* {@link Builder#simpDestMatchers(String...)}. Use this method to delay the
* computation or lookup of the {@link PathMatcher}.
* @param pathMatcher the {@link PathMatcher} to use. Cannot be null.
* @return the {@link Builder} for further customization.
* @deprecated
*/
@Deprecated
public Builder simpDestPathMatcher(Supplier<PathMatcher> pathMatcher) {
Assert.notNull(pathMatcher, "pathMatcher cannot be null");
this.pathMatcher = pathMatcher;
return this;
}
/** /**
* Maps a {@link List} of {@link MessageMatcher} instances to a security * Maps a {@link List} of {@link MessageMatcher} instances to a security
* expression. * expression.
@ -248,6 +205,12 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
return new MessageMatcherDelegatingAuthorizationManager(this.mappings); return new MessageMatcherDelegatingAuthorizationManager(this.mappings);
} }
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.messageMatcherBuilder = context.getBeanProvider(PathPatternMessageMatcher.Builder.class)
.getIfUnique(PathPatternMessageMatcher::withDefaults);
}
/** /**
* Represents the security constraint to be applied to the {@link MessageMatcher} * Represents the security constraint to be applied to the {@link MessageMatcher}
* instances. * instances.
@ -379,39 +342,6 @@ public final class MessageMatcherDelegatingAuthorizationManager implements Autho
} }
@Deprecated
private final class LazySimpDestinationMessageMatcher implements MessageMatcher<Object> {
private final Supplier<SimpDestinationMessageMatcher> delegate;
private LazySimpDestinationMessageMatcher(String pattern, SimpMessageType type) {
this.delegate = SingletonSupplier.of(() -> {
PathMatcher pathMatcher = Builder.this.pathMatcher.get();
if (type == null) {
return new SimpDestinationMessageMatcher(pattern, pathMatcher);
}
if (SimpMessageType.MESSAGE == type) {
return SimpDestinationMessageMatcher.createMessageMatcher(pattern, pathMatcher);
}
if (SimpMessageType.SUBSCRIBE == type) {
return SimpDestinationMessageMatcher.createSubscribeMatcher(pattern, pathMatcher);
}
throw new IllegalStateException(type + " is not supported since it does not have a destination");
});
}
@Override
public boolean matches(Message<?> message) {
return this.delegate.get().matches(message);
}
@Override
public MatchResult matcher(Message<?> message) {
return this.delegate.get().matcher(message);
}
}
} }
private static final class Entry<T> { private static final class Entry<T> {

View File

@ -28,7 +28,7 @@ import org.springframework.messaging.Message;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.support.MessageBuilder; import org.springframework.messaging.support.MessageBuilder;
import org.springframework.security.messaging.util.matcher.MessageMatcher; import org.springframework.security.messaging.util.matcher.MessageMatcher;
import org.springframework.security.messaging.util.matcher.SimpDestinationMessageMatcher; import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -81,7 +81,7 @@ public class MessageExpressionConfigAttributeTests {
@Test @Test
public void postProcessContext() { public void postProcessContext() {
SimpDestinationMessageMatcher matcher = new SimpDestinationMessageMatcher("/topics/{topic}/**"); PathPatternMessageMatcher matcher = PathPatternMessageMatcher.withDefaults().matcher("/topics/{topic}/**");
// @formatter:off // @formatter:off
Message<?> message = MessageBuilder.withPayload("M") Message<?> message = MessageBuilder.withPayload("M")
.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1") .setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1")

View File

@ -78,6 +78,7 @@ public class MessageExpressionVoterTests {
@Test @Test
public void voteGranted() { public void voteGranted() {
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(true); given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(true);
given(this.matcher.matcher(any())).willCallRealMethod();
assertThat(this.voter.vote(this.authentication, this.message, this.attributes)) assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED); .isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
} }
@ -85,6 +86,7 @@ public class MessageExpressionVoterTests {
@Test @Test
public void voteDenied() { public void voteDenied() {
given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(false); given(this.expression.getValue(any(EvaluationContext.class), eq(Boolean.class))).willReturn(false);
given(this.matcher.matcher(any())).willCallRealMethod();
assertThat(this.voter.vote(this.authentication, this.message, this.attributes)) assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
.isEqualTo(AccessDecisionVoter.ACCESS_DENIED); .isEqualTo(AccessDecisionVoter.ACCESS_DENIED);
} }
@ -127,6 +129,7 @@ public class MessageExpressionVoterTests {
given(this.expressionHandler.createEvaluationContext(this.authentication, this.message)) given(this.expressionHandler.createEvaluationContext(this.authentication, this.message))
.willReturn(this.evaluationContext); .willReturn(this.evaluationContext);
given(this.expression.getValue(this.evaluationContext, Boolean.class)).willReturn(true); given(this.expression.getValue(this.evaluationContext, Boolean.class)).willReturn(true);
given(this.matcher.matcher(any())).willCallRealMethod();
assertThat(this.voter.vote(this.authentication, this.message, this.attributes)) assertThat(this.voter.vote(this.authentication, this.message, this.attributes))
.isEqualTo(AccessDecisionVoter.ACCESS_GRANTED); .isEqualTo(AccessDecisionVoter.ACCESS_GRANTED);
verify(this.expressionHandler).createEvaluationContext(this.authentication, this.message); verify(this.expressionHandler).createEvaluationContext(this.authentication, this.message);

View File

@ -37,10 +37,11 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.messaging.util.matcher.MessageMatcherFactory;
import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher; import org.springframework.security.messaging.util.matcher.PathPatternMessageMatcher;
import org.springframework.web.util.pattern.PathPatternParser;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
/** /**
@ -58,7 +59,7 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
Mockito.when(this.context.getBeanProvider(PathPatternMessageMatcher.Builder.class)).thenReturn(this.provider); Mockito.when(this.context.getBeanProvider(PathPatternMessageMatcher.Builder.class)).thenReturn(this.provider);
MessageMatcherFactory.setApplicationContext(this.context); Mockito.when(this.provider.getIfUnique(any())).thenReturn(PathPatternMessageMatcher.withDefaults());
} }
@Test @Test
@ -135,8 +136,7 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
@Test @Test
void checkWhenMessageTypeAndPathPatternMatches() { void checkWhenMessageTypeAndPathPatternMatches() {
Mockito.when(this.provider.getIfUnique()).thenReturn(PathPatternMessageMatcher.withDefaults()); Mockito.when(this.provider.getIfUnique(any())).thenReturn(PathPatternMessageMatcher.withDefaults());
MessageMatcherFactory.setApplicationContext(this.context);
AuthorizationManager<Message<?>> authorizationManager = builder().simpMessageDestMatchers("/destination") AuthorizationManager<Message<?>> authorizationManager = builder().simpMessageDestMatchers("/destination")
.permitAll() .permitAll()
.simpSubscribeDestMatchers("/destination") .simpSubscribeDestMatchers("/destination")
@ -154,10 +154,32 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
assertThat(authorizationManager.authorize(mock(Supplier.class), message2).isGranted()).isFalse(); assertThat(authorizationManager.authorize(mock(Supplier.class), message2).isGranted()).isFalse();
} }
@Test
void checkWhenMessageTypeAndPathPatternMatchesCaseInsensitive() {
PathPatternParser pathPatternParser = new PathPatternParser();
pathPatternParser.setCaseSensitive(false);
PathPatternMessageMatcher.Builder messageMatcherBuilder = PathPatternMessageMatcher
.withPathPatternParser(pathPatternParser);
Mockito.when(this.provider.getIfUnique(any())).thenReturn(messageMatcherBuilder);
AuthorizationManager<Message<?>> authorizationManager = builder().simpMessageDestMatchers("/desTinaTion")
.permitAll()
.simpSubscribeDestMatchers("/desTinaTion")
.denyAll()
.anyMessage()
.denyAll()
.build();
MessageHeaders headers = new MessageHeaders(Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER,
SimpMessageType.MESSAGE, SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination"));
Message<?> message = new GenericMessage<>(new Object(), headers);
assertThat(authorizationManager.authorize(mock(Supplier.class), message).isGranted()).isTrue();
MessageHeaders headers2 = new MessageHeaders(Map.of(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER,
SimpMessageType.SUBSCRIBE, SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination"));
Message<?> message2 = new GenericMessage<>(new Object(), headers2);
assertThat(authorizationManager.authorize(mock(Supplier.class), message2).isGranted()).isFalse();
}
@Test @Test
void checkPatternMismatch() { void checkPatternMismatch() {
Mockito.when(this.provider.getIfUnique()).thenReturn(PathPatternMessageMatcher.withDefaults());
MessageMatcherFactory.setApplicationContext(this.context);
AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("/destination/*") AuthorizationManager<Message<?>> authorizationManager = builder().simpDestMatchers("/destination/*")
.permitAll() .permitAll()
.anyMessage() .anyMessage()
@ -170,7 +192,10 @@ public final class MessageMatcherDelegatingAuthorizationManagerTests {
} }
private MessageMatcherDelegatingAuthorizationManager.Builder builder() { private MessageMatcherDelegatingAuthorizationManager.Builder builder() {
return MessageMatcherDelegatingAuthorizationManager.builder(); MessageMatcherDelegatingAuthorizationManager.Builder builder = MessageMatcherDelegatingAuthorizationManager
.builder();
builder.setApplicationContext(this.context);
return builder;
} }
private Builder variable(String name) { private Builder variable(String name) {