diff --git a/config/src/main/java/org/springframework/security/config/web/server/AbstractServerWebExchangeMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/web/server/AbstractServerWebExchangeMatcherRegistry.java index aa3382f1f0c..5b33466d1a2 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/AbstractServerWebExchangeMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/web/server/AbstractServerWebExchangeMatcherRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -16,6 +16,7 @@ package org.springframework.security.config.web.server; +import java.util.ArrayList; import java.util.List; import org.springframework.http.HttpMethod; @@ -23,6 +24,8 @@ import org.springframework.security.web.server.util.matcher.PathPatternParserServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatcher; import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers; +import org.springframework.web.util.pattern.PathPattern; +import org.springframework.web.util.pattern.PathPatternParser; /** * @author Rob Winch @@ -62,7 +65,8 @@ public T pathMatchers(HttpMethod method) { * {@link ServerWebExchangeMatcher} */ public T pathMatchers(HttpMethod method, String... antPatterns) { - return matcher(ServerWebExchangeMatchers.pathMatchers(method, antPatterns)); + List pathPatterns = parsePatterns(antPatterns); + return matcher(ServerWebExchangeMatchers.pathMatchers(method, pathPatterns.toArray(new PathPattern[0]))); } /** @@ -74,7 +78,19 @@ public T pathMatchers(HttpMethod method, String... antPatterns) { * {@link ServerWebExchangeMatcher} */ public T pathMatchers(String... antPatterns) { - return matcher(ServerWebExchangeMatchers.pathMatchers(antPatterns)); + List pathPatterns = parsePatterns(antPatterns); + return matcher(ServerWebExchangeMatchers.pathMatchers(pathPatterns.toArray(new PathPattern[0]))); + } + + private List parsePatterns(String[] antPatterns) { + PathPatternParser parser = getPathPatternParser(); + List pathPatterns = new ArrayList<>(antPatterns.length); + for (String pattern : antPatterns) { + pattern = parser.initFullPathPattern(pattern); + PathPattern pathPattern = parser.parse(pattern); + pathPatterns.add(pathPattern); + } + return pathPatterns; } /** @@ -96,6 +112,10 @@ public T matchers(ServerWebExchangeMatcher... matchers) { */ protected abstract T registerMatcher(ServerWebExchangeMatcher matcher); + protected PathPatternParser getPathPatternParser() { + return PathPatternParser.defaultInstance; + } + /** * Associates a {@link ServerWebExchangeMatcher} instances * @param matcher the {@link ServerWebExchangeMatcher} instance diff --git a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java index 5c0094f079a..544826dea57 100644 --- a/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/web/server/ServerHttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * 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. @@ -185,9 +185,11 @@ import org.springframework.web.cors.reactive.CorsProcessor; import org.springframework.web.cors.reactive.CorsWebFilter; import org.springframework.web.cors.reactive.DefaultCorsProcessor; +import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; +import org.springframework.web.util.pattern.PathPatternParser; /** * A {@link ServerHttpSecurity} is similar to Spring Security's {@code HttpSecurity} but @@ -1557,6 +1559,18 @@ private T getBeanOrNull(ResolvableType type) { return null; } + private T getBeanOrNull(String beanName, Class requiredClass) { + if (this.context == null) { + return null; + } + try { + return this.context.getBean(beanName, requiredClass); + } + catch (Exception ex) { + return null; + } + } + private String[] getBeanNamesForTypeOrEmpty(Class beanClass) { if (this.context == null) { return new String[0]; @@ -1577,6 +1591,8 @@ protected void setApplicationContext(ApplicationContext applicationContext) thro */ public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegistry { + private static final String REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME = "requestMappingHandlerMapping"; + private DelegatingReactiveAuthorizationManager.Builder managerBldr = DelegatingReactiveAuthorizationManager .builder(); @@ -1584,6 +1600,8 @@ public class AuthorizeExchangeSpec extends AbstractServerWebExchangeMatcherRegis private boolean anyExchangeRegistered; + private PathPatternParser pathPatternParser; + /** * Allows method chaining to continue configuring the {@link ServerHttpSecurity} * @return the {@link ServerHttpSecurity} to continue configuring @@ -1603,6 +1621,22 @@ public Access anyExchange() { return result; } + @Override + protected PathPatternParser getPathPatternParser() { + if (this.pathPatternParser != null) { + return this.pathPatternParser; + } + RequestMappingHandlerMapping requestMappingHandlerMapping = getBeanOrNull( + REQUEST_MAPPING_HANDLER_MAPPING_BEAN_NAME, RequestMappingHandlerMapping.class); + if (requestMappingHandlerMapping != null) { + this.pathPatternParser = requestMappingHandlerMapping.getPathPatternParser(); + } + if (this.pathPatternParser == null) { + this.pathPatternParser = PathPatternParser.defaultInstance; + } + return this.pathPatternParser; + } + @Override protected Access registerMatcher(ServerWebExchangeMatcher matcher) { Assert.state(!this.anyExchangeRegistered, () -> "Cannot register " + matcher diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/PathPatternParserServerWebExchangeMatcher.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/PathPatternParserServerWebExchangeMatcher.java index 0e0a3dbbcfe..b76b33a5a4d 100644 --- a/web/src/main/java/org/springframework/security/web/server/util/matcher/PathPatternParserServerWebExchangeMatcher.java +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/PathPatternParserServerWebExchangeMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * 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. @@ -42,8 +42,6 @@ public final class PathPatternParserServerWebExchangeMatcher implements ServerWe private static final Log logger = LogFactory.getLog(PathPatternParserServerWebExchangeMatcher.class); - private static final PathPatternParser DEFAULT_PATTERN_PARSER = new PathPatternParser(); - private final PathPattern pattern; private final HttpMethod method; @@ -60,7 +58,7 @@ public PathPatternParserServerWebExchangeMatcher(PathPattern pattern, HttpMethod public PathPatternParserServerWebExchangeMatcher(String pattern, HttpMethod method) { Assert.notNull(pattern, "pattern cannot be null"); - this.pattern = DEFAULT_PATTERN_PARSER.parse(pattern); + this.pattern = parse(pattern); this.method = method; } @@ -68,6 +66,12 @@ public PathPatternParserServerWebExchangeMatcher(String pattern) { this(pattern, null); } + private PathPattern parse(String pattern) { + PathPatternParser parser = PathPatternParser.defaultInstance; + pattern = parser.initFullPathPattern(pattern); + return parser.parse(pattern); + } + @Override public Mono matches(ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); diff --git a/web/src/main/java/org/springframework/security/web/server/util/matcher/ServerWebExchangeMatchers.java b/web/src/main/java/org/springframework/security/web/server/util/matcher/ServerWebExchangeMatchers.java index 5b7bec52c71..a858702c297 100644 --- a/web/src/main/java/org/springframework/security/web/server/util/matcher/ServerWebExchangeMatchers.java +++ b/web/src/main/java/org/springframework/security/web/server/util/matcher/ServerWebExchangeMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * 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. @@ -23,6 +23,7 @@ import org.springframework.http.HttpMethod; import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.pattern.PathPattern; /** * Provides factory methods for creating common {@link ServerWebExchangeMatcher} @@ -59,6 +60,30 @@ public static ServerWebExchangeMatcher pathMatchers(String... patterns) { return pathMatchers(null, patterns); } + /** + * Creates a matcher that matches on any of the provided {@link PathPattern}s. + * @param pathPatterns the {@link PathPattern}s to match on + * @return the matcher to use + */ + public static ServerWebExchangeMatcher pathMatchers(PathPattern... pathPatterns) { + return pathMatchers(null, pathPatterns); + } + + /** + * Creates a matcher that matches on the specific method and any of the provided + * {@link PathPattern}s. + * @param method the method to match on. If null, any method will be matched. + * @param pathPatterns the {@link PathPattern}s to match on + * @return the matcher to use + */ + public static ServerWebExchangeMatcher pathMatchers(HttpMethod method, PathPattern... pathPatterns) { + List matchers = new ArrayList<>(pathPatterns.length); + for (PathPattern pathPattern : pathPatterns) { + matchers.add(new PathPatternParserServerWebExchangeMatcher(pathPattern, method)); + } + return new OrServerWebExchangeMatcher(matchers); + } + /** * Creates a matcher that will match on any of the provided matchers * @param matchers the matchers to match on diff --git a/web/src/test/java/org/springframework/security/web/server/util/matcher/PathPatternParserServerWebExchangeMatcherTests.java b/web/src/test/java/org/springframework/security/web/server/util/matcher/PathPatternParserServerWebExchangeMatcherTests.java new file mode 100644 index 00000000000..080f8e2a70a --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/server/util/matcher/PathPatternParserServerWebExchangeMatcherTests.java @@ -0,0 +1,40 @@ +/* + * 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.web.server.util.matcher; + +import org.junit.jupiter.api.Test; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PathPatternParserServerWebExchangeMatcher} + * + * @author Marcus da Coregio + */ +class PathPatternParserServerWebExchangeMatcherTests { + + @Test + void matchesWhenConfiguredWithNoTrailingSlashAndPathContainsSlashThenMatches() { + PathPatternParserServerWebExchangeMatcher matcher = new PathPatternParserServerWebExchangeMatcher("user/**"); + MockServerHttpRequest request = MockServerHttpRequest.get("/user/test").build(); + assertThat(matcher.matches(MockServerWebExchange.from(request)).block().isMatch()).isTrue(); + } + +}