10 changed files with 597 additions and 52 deletions
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
/* |
||||
* Copyright 2002-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.messaging.util.matcher; |
||||
|
||||
import org.springframework.context.ApplicationContext; |
||||
import org.springframework.messaging.simp.SimpMessageType; |
||||
|
||||
@Deprecated(forRemoval = true) |
||||
public final class MessageMatcherFactory { |
||||
|
||||
private static PathPatternMessageMatcher.Builder builder; |
||||
|
||||
public static void setApplicationContext(ApplicationContext context) { |
||||
builder = context.getBeanProvider(PathPatternMessageMatcher.Builder.class).getIfUnique(); |
||||
} |
||||
|
||||
public static boolean usesPathPatterns() { |
||||
return builder != null; |
||||
} |
||||
|
||||
public static MessageMatcher<?> matcher(String destination) { |
||||
return builder.matcher(destination); |
||||
} |
||||
|
||||
public static MessageMatcher<Object> matcher(String destination, SimpMessageType type) { |
||||
return (type != null) ? builder.matcher(destination, type) : builder.matcher(destination); |
||||
} |
||||
|
||||
private MessageMatcherFactory() { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,151 @@
@@ -0,0 +1,151 @@
|
||||
/* |
||||
* Copyright 2002-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.messaging.util.matcher; |
||||
|
||||
import java.util.Collections; |
||||
|
||||
import org.springframework.http.server.PathContainer; |
||||
import org.springframework.messaging.Message; |
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor; |
||||
import org.springframework.messaging.simp.SimpMessageType; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.util.pattern.PathPattern; |
||||
import org.springframework.web.util.pattern.PathPatternParser; |
||||
|
||||
/** |
||||
* Match {@link Message}s based on the message destination pattern using a |
||||
* {@link PathPattern}. There is also support for optionally matching on a specified |
||||
* {@link SimpMessageType}. |
||||
* |
||||
* @author Pat McCusker |
||||
* @since 6.5 |
||||
*/ |
||||
public final class PathPatternMessageMatcher implements MessageMatcher<Object> { |
||||
|
||||
public static final MessageMatcher<Object> NULL_DESTINATION_MATCHER = (message) -> getDestination(message) == null; |
||||
|
||||
private final PathPattern pattern; |
||||
|
||||
private final PathPatternParser parser; |
||||
|
||||
/** |
||||
* The {@link MessageMatcher} that determines if the type matches. If the type was |
||||
* null, this matcher will match every Message. |
||||
*/ |
||||
private MessageMatcher<Object> messageTypeMatcher = ANY_MESSAGE; |
||||
|
||||
private PathPatternMessageMatcher(PathPattern pattern, PathPatternParser parser) { |
||||
this.parser = parser; |
||||
this.pattern = pattern; |
||||
} |
||||
|
||||
/** |
||||
* Initialize this builder with the {@link PathPatternParser#defaultInstance} that is |
||||
* configured with the |
||||
* {@link org.springframework.http.server.PathContainer.Options#HTTP_PATH} separator |
||||
*/ |
||||
public static Builder withDefaults() { |
||||
return new Builder(PathPatternParser.defaultInstance); |
||||
} |
||||
|
||||
/** |
||||
* Initialize this builder with the provided {@link PathPatternParser} |
||||
*/ |
||||
public static Builder withPathPatternParser(PathPatternParser parser) { |
||||
return new Builder(parser); |
||||
} |
||||
|
||||
void setMessageTypeMatcher(MessageMatcher<Object> messageTypeMatcher) { |
||||
this.messageTypeMatcher = messageTypeMatcher; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public boolean matches(Message<?> message) { |
||||
if (!this.messageTypeMatcher.matches(message)) { |
||||
return false; |
||||
} |
||||
|
||||
String destination = getDestination(message); |
||||
if (destination == null) { |
||||
return false; |
||||
} |
||||
|
||||
PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.parser.getPathOptions()); |
||||
return this.pattern.matches(destinationPathContainer); |
||||
} |
||||
|
||||
/** |
||||
* Extract the path variables from the {@link Message} destination if the path is a |
||||
* match, otherwise the {@link MatchResult#getVariables()} returns a |
||||
* {@link Collections#emptyMap()} |
||||
* @param message the message whose path variables to extract. |
||||
* @return a {@code MatchResult} of the path variables and values. |
||||
*/ |
||||
@Override |
||||
public MatchResult matcher(Message<?> message) { |
||||
if (!this.messageTypeMatcher.matches(message)) { |
||||
return MatchResult.notMatch(); |
||||
} |
||||
|
||||
String destination = getDestination(message); |
||||
if (destination == null) { |
||||
return MatchResult.notMatch(); |
||||
} |
||||
|
||||
PathContainer destinationPathContainer = PathContainer.parsePath(destination, this.parser.getPathOptions()); |
||||
PathPattern.PathMatchInfo pathMatchInfo = this.pattern.matchAndExtract(destinationPathContainer); |
||||
|
||||
return (pathMatchInfo != null) ? MatchResult.match(pathMatchInfo.getUriVariables()) : MatchResult.notMatch(); |
||||
} |
||||
|
||||
private static String getDestination(Message<?> message) { |
||||
return SimpMessageHeaderAccessor.getDestination(message.getHeaders()); |
||||
} |
||||
|
||||
public static class Builder { |
||||
|
||||
private final PathPatternParser parser; |
||||
|
||||
private MessageMatcher<Object> messageTypeMatcher = ANY_MESSAGE; |
||||
|
||||
Builder(PathPatternParser parser) { |
||||
this.parser = parser; |
||||
} |
||||
|
||||
public PathPatternMessageMatcher matcher(String pattern) { |
||||
Assert.notNull(pattern, "Pattern must not be null"); |
||||
PathPattern pathPattern = this.parser.parse(pattern); |
||||
PathPatternMessageMatcher matcher = new PathPatternMessageMatcher(pathPattern, this.parser); |
||||
if (this.messageTypeMatcher != ANY_MESSAGE) { |
||||
matcher.setMessageTypeMatcher(this.messageTypeMatcher); |
||||
} |
||||
return matcher; |
||||
} |
||||
|
||||
public PathPatternMessageMatcher matcher(String pattern, SimpMessageType type) { |
||||
Assert.notNull(type, "Type must not be null"); |
||||
this.messageTypeMatcher = new SimpMessageTypeMatcher(type); |
||||
|
||||
return matcher(pattern); |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,54 @@
@@ -0,0 +1,54 @@
|
||||
/* |
||||
* Copyright 2002-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.messaging.util.matcher; |
||||
|
||||
import org.springframework.beans.factory.FactoryBean; |
||||
import org.springframework.web.util.pattern.PathPatternParser; |
||||
|
||||
/** |
||||
* Use this factory bean to configure the {@link PathPatternMessageMatcher.Builder} bean |
||||
* used to create request matchers in |
||||
* {@link org.springframework.security.messaging.access.intercept.MessageMatcherDelegatingAuthorizationManager} |
||||
* and other parts of the DSL. |
||||
* |
||||
* @author Pat McCusker |
||||
* @since 6.5 |
||||
*/ |
||||
public class PathPatternMessageMatcherBuilderFactoryBean implements FactoryBean<PathPatternMessageMatcher.Builder> { |
||||
|
||||
private final PathPatternParser parser; |
||||
|
||||
public PathPatternMessageMatcherBuilderFactoryBean() { |
||||
this(null); |
||||
} |
||||
|
||||
public PathPatternMessageMatcherBuilderFactoryBean(PathPatternParser parser) { |
||||
this.parser = parser; |
||||
} |
||||
|
||||
@Override |
||||
public PathPatternMessageMatcher.Builder getObject() throws Exception { |
||||
return (this.parser != null) ? PathPatternMessageMatcher.withPathPatternParser(this.parser) |
||||
: PathPatternMessageMatcher.withDefaults(); |
||||
} |
||||
|
||||
@Override |
||||
public Class<?> getObjectType() { |
||||
return PathPatternMessageMatcher.Builder.class; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
/* |
||||
* Copyright 2002-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.messaging.util.matcher; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.context.support.GenericApplicationContext; |
||||
import org.springframework.web.util.pattern.PathPatternParser; |
||||
|
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.verify; |
||||
|
||||
class PathPatternMessageMatcherBuilderFactoryBeanTests { |
||||
|
||||
GenericApplicationContext context; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
this.context = new GenericApplicationContext(); |
||||
} |
||||
|
||||
@Test |
||||
void getObjectWhenDefaultsThenBuilder() throws Exception { |
||||
factoryBean().getObject(); |
||||
} |
||||
|
||||
@Test |
||||
void getObjectWithCustomParserThenUses() throws Exception { |
||||
PathPatternParser parser = mock(PathPatternParser.class); |
||||
PathPatternMessageMatcher.Builder builder = factoryBean(parser).getObject(); |
||||
|
||||
builder.matcher("/path/**"); |
||||
verify(parser).parse("/path/**"); |
||||
} |
||||
|
||||
PathPatternMessageMatcherBuilderFactoryBean factoryBean() { |
||||
PathPatternMessageMatcherBuilderFactoryBean factoryBean = new PathPatternMessageMatcherBuilderFactoryBean(); |
||||
return factoryBean; |
||||
} |
||||
|
||||
PathPatternMessageMatcherBuilderFactoryBean factoryBean(PathPatternParser parser) { |
||||
PathPatternMessageMatcherBuilderFactoryBean factoryBean = new PathPatternMessageMatcherBuilderFactoryBean( |
||||
parser); |
||||
return factoryBean; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,155 @@
@@ -0,0 +1,155 @@
|
||||
/* |
||||
* Copyright 2002-2025 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.security.messaging.util.matcher; |
||||
|
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.http.server.PathContainer; |
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor; |
||||
import org.springframework.messaging.simp.SimpMessageType; |
||||
import org.springframework.messaging.support.MessageBuilder; |
||||
import org.springframework.web.util.pattern.PathPatternParser; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; |
||||
|
||||
public class PathPatternMessageMatcherTests { |
||||
|
||||
MessageBuilder<String> messageBuilder; |
||||
|
||||
PathPatternMessageMatcher matcher; |
||||
|
||||
@BeforeEach |
||||
void setUp() { |
||||
this.messageBuilder = MessageBuilder.withPayload("M"); |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/**"); |
||||
} |
||||
|
||||
@Test |
||||
void constructorPatternNull() { |
||||
assertThatIllegalArgumentException().isThrownBy(() -> PathPatternMessageMatcher.withDefaults().matcher(null)); |
||||
} |
||||
|
||||
@Test |
||||
void matchesDoesNotMatchNullDestination() { |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesTrueWithSpecificDestinationPattern() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/destination/1"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesFalseWithDifferentDestination() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/nomatch"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesTrueWithDotSeparator() { |
||||
this.matcher = PathPatternMessageMatcher.withPathPatternParser(dotSeparatedPathParser()) |
||||
.matcher("destination.1"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination.1"); |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesFalseWithDotSeparatorAndAdditionalWildcardPathSegment() { |
||||
this.matcher = PathPatternMessageMatcher.withPathPatternParser(dotSeparatedPathParser()) |
||||
.matcher("/destination/a.*"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/a.b"); |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/a.b.c"); |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void matchesFalseWithDifferentMessageType() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/match", SimpMessageType.MESSAGE); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.DISCONNECT); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/match"); |
||||
|
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
public void matchesTrueMessageType() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/match", SimpMessageType.MESSAGE); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/match"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE); |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
public void matchesTrueSubscribeType() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/match", SimpMessageType.SUBSCRIBE); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/match"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.SUBSCRIBE); |
||||
assertThat(this.matcher.matches(this.messageBuilder.build())).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void extractPathVariablesFromDestination() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/topics/{topic}/**"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/topics/someTopic/sub1"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE); |
||||
|
||||
MessageMatcher.MatchResult matchResult = this.matcher.matcher(this.messageBuilder.build()); |
||||
assertThat(matchResult.isMatch()).isTrue(); |
||||
assertThat(matchResult.getVariables()).containsEntry("topic", "someTopic"); |
||||
} |
||||
|
||||
@Test |
||||
void extractPathVariablesFromMessageDestinationPath() { |
||||
this.matcher = PathPatternMessageMatcher.withPathPatternParser(dotSeparatedPathParser()) |
||||
.matcher("destination.{destinationNum}"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "destination.1"); |
||||
MessageMatcher.MatchResult matchResult = this.matcher.matcher(this.messageBuilder.build()); |
||||
assertThat(matchResult.getVariables()).containsEntry("destinationNum", "1"); |
||||
} |
||||
|
||||
@Test |
||||
void extractPathVariables_isEmptyWithNullDestination() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/topics/{topic}/**"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.MESSAGE_TYPE_HEADER, SimpMessageType.MESSAGE); |
||||
|
||||
MessageMatcher.MatchResult matchResult = this.matcher.matcher(this.messageBuilder.build()); |
||||
assertThat(matchResult.isMatch()).isFalse(); |
||||
assertThat(matchResult.getVariables()).isEmpty(); |
||||
} |
||||
|
||||
@Test |
||||
void getUriVariablesIsEmpty_onExtractPathVariables_whenNoMatch() { |
||||
this.matcher = PathPatternMessageMatcher.withDefaults().matcher("/nomatch"); |
||||
this.messageBuilder.setHeader(SimpMessageHeaderAccessor.DESTINATION_HEADER, "/destination/1"); |
||||
MessageMatcher.MatchResult matchResult = this.matcher.matcher(this.messageBuilder.build()); |
||||
assertThat(matchResult.isMatch()).isFalse(); |
||||
assertThat(matchResult.getVariables()).isEmpty(); |
||||
} |
||||
|
||||
private static PathPatternParser dotSeparatedPathParser() { |
||||
PathPatternParser parser = new PathPatternParser(); |
||||
parser.setPathOptions(PathContainer.Options.MESSAGE_ROUTE); |
||||
return parser; |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue