From 4c258b9af4df0a8b04fdb44d17cdea8e105a3efc Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Wed, 3 Sep 2025 22:07:07 +0300 Subject: [PATCH] A Root basePath No Longer Creates a Double-Slash Closes gh-17812 Signed-off-by: Andrey Litvitski --- .../servlet/util/matcher/PathPatternRequestMatcher.java | 9 ++++++--- .../util/matcher/PathPatternRequestMatcherTests.java | 8 ++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcher.java b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcher.java index bd1bb0f8294..9de4611b985 100644 --- a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcher.java +++ b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcher.java @@ -49,6 +49,7 @@ *

* * @author Josh Cummings + * @author Andrey Litvitski * @since 6.5 */ public final class PathPatternRequestMatcher implements RequestMatcher { @@ -200,14 +201,15 @@ public static final class Builder { * *

* Prefixes should be of the form {@code /my/prefix}, starting with a slash, not - * ending in a slash, and not containing and wildcards + * ending in a slash, and not containing and wildcards The special value + * {@code "/"} may be used to indicate the root context. * @param basePath the path prefix * @return the {@link Builder} for more configuration */ public Builder basePath(String basePath) { Assert.notNull(basePath, "basePath cannot be null"); Assert.isTrue(basePath.startsWith("/"), "basePath must start with '/'"); - Assert.isTrue(!basePath.endsWith("/"), "basePath must not end with a slash"); + Assert.isTrue("/".equals(basePath) || !basePath.endsWith("/"), "basePath must not end with a slash"); Assert.isTrue(!basePath.contains("*"), "basePath must not contain a star"); return new Builder(this.parser, basePath); } @@ -282,7 +284,8 @@ public PathPatternRequestMatcher matcher(String path) { public PathPatternRequestMatcher matcher(@Nullable HttpMethod method, String path) { Assert.notNull(path, "pattern cannot be null"); Assert.isTrue(path.startsWith("/"), "pattern must start with a /"); - PathPattern pathPattern = this.parser.parse(this.basePath + path); + String prefix = ("/".equals(this.basePath)) ? "" : this.basePath; + PathPattern pathPattern = this.parser.parse(prefix + path); return new PathPatternRequestMatcher(pathPattern, (method != null) ? new HttpMethodRequestMatcher(method) : AnyRequestMatcher.INSTANCE); } diff --git a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcherTests.java index 017cf3c67f5..ea1d8337c9e 100644 --- a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcherTests.java +++ b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/PathPatternRequestMatcherTests.java @@ -137,6 +137,14 @@ void servletPathWhenEndsWithSlashOrStarThenIllegalArgument() { .isThrownBy(() -> PathPatternRequestMatcher.withDefaults().basePath("/path/")); } + @Test + void matcherWhenBasePathIsRootThenNoDoubleSlash() { + PathPatternRequestMatcher.Builder builder = PathPatternRequestMatcher.withDefaults().basePath("/"); + RequestMatcher matcher = builder.matcher(HttpMethod.GET, "/path"); + MockHttpServletRequest mock = get("/path").servletPath("/path").buildRequest(null); + assertThat(matcher.matches(mock)).isTrue(); + } + MockHttpServletRequest request(String uri) { MockHttpServletRequest request = new MockHttpServletRequest("GET", uri); ServletRequestPathUtils.parseAndCache(request);