From 438c783c7d01883569b558cfe27bd10a6f3b3cf6 Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Wed, 15 Apr 2026 16:54:51 -0600 Subject: [PATCH] securityMatchers uses PathPatternRequestMatcher.Builder Bean Signed-off-by: Josh Cummings <3627351+jzheaux@users.noreply.github.com> --- .../HttpSecurityConfiguration.java | 4 +- .../AuthorizeHttpRequestsConfigurerTests.java | 38 +++++++ .../HttpSecuritySecurityMatchersTests.java | 105 ++++++++++++++++++ 3 files changed, 146 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java index efde27ad3f..e84e4f1910 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/HttpSecurityConfiguration.java @@ -238,7 +238,9 @@ class HttpSecurityConfiguration { Map, Object> sharedObjects = new HashMap<>(); sharedObjects.put(ApplicationContext.class, this.context); sharedObjects.put(ContentNegotiationStrategy.class, this.contentNegotiationStrategy); - sharedObjects.put(PathPatternRequestMatcher.Builder.class, constructRequestMatcherBuilder(this.context)); + sharedObjects.put(PathPatternRequestMatcher.Builder.class, + this.context.getBeanProvider(PathPatternRequestMatcher.Builder.class) + .getIfUnique(() -> constructRequestMatcherBuilder(this.context))); return sharedObjects; } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index 15fdd19522..a40fd0d614 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -452,6 +452,18 @@ public class AuthorizeHttpRequestsConfigurerTests { this.mvc.perform(requestWithAdmin).andExpect(status().isOk()); } + @Test + public void requestMatchersWhenBuilderBeanWithBasePathAndRawStringThenHonorsBasePath() throws Exception { + this.spring.register(RequestMatchersRawStringServletPathConfig.class, BasicController.class).autowire(); + // @formatter:off + MockHttpServletRequestBuilder matchedByBasePath = get("/spring/path") + .servletPath("/spring") + .with(user("user").roles("USER")); + // @formatter:on + this.mvc.perform(matchedByBasePath).andExpect(status().isForbidden()); + this.mvc.perform(get("/path").with(user("user").roles("USER"))).andExpect(status().isOk()); + } + @Test public void getWhenAnyRequestAuthenticatedConfiguredAndNoUserThenRespondsWithUnauthorized() throws Exception { this.spring.register(AuthenticatedConfig.class, BasicController.class).autowire(); @@ -1359,6 +1371,32 @@ public class AuthorizeHttpRequestsConfigurerTests { } + @Configuration + @EnableWebMvc + @EnableWebSecurity + static class RequestMatchersRawStringServletPathConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + return http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/path").hasRole("ADMIN") + .anyRequest().permitAll() + ) + .build(); + // @formatter:on + } + + } + @Configuration @EnableWebSecurity static class AuthenticatedConfig { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java index 5502a6c1ab..46d1305643 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/HttpSecuritySecurityMatchersTests.java @@ -125,6 +125,34 @@ public class HttpSecuritySecurityMatchersTests { assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); } + @Test + public void securityMatcherWhenBuilderBeanWithBasePathThenHonorsBasePath() throws Exception { + loadConfig(SecurityMatcherBuilderBeanConfig.class); + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setServletPath(""); + this.request.setRequestURI("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + + @Test + public void securityMatchersWhenBuilderBeanWithBasePathAndRawStringsThenHonorsBasePath() throws Exception { + loadConfig(SecurityMatchersBuilderBeanConfig.class); + this.request.setServletPath("/spring"); + this.request.setRequestURI("/spring/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + setup(); + this.request.setServletPath(""); + this.request.setRequestURI("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + } + @Test public void securityMatchersWhenMultiMvcMatcherInLambdaThenAllPathsAreDenied() throws Exception { loadConfig(MultiMvcMatcherInLambdaConfig.class); @@ -430,6 +458,83 @@ public class HttpSecuritySecurityMatchersTests { } + @EnableWebSecurity + @Configuration + @EnableWebMvc + @Import(UsersConfig.class) + static class SecurityMatcherBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain appSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .securityMatcher("/path") + .httpBasic(withDefaults()) + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().denyAll()); + // @formatter:on + return http.build(); + } + + @RestController + static class PathController { + + @RequestMapping("/path") + String path() { + return "path"; + } + + } + + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + @Import(UsersConfig.class) + static class SecurityMatchersBuilderBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + PathPatternRequestMatcherBuilderFactoryBean bean = new PathPatternRequestMatcherBuilderFactoryBean(); + bean.setBasePath("/spring"); + return bean; + } + + @Bean + SecurityFilterChain appSecurity(HttpSecurity http) throws Exception { + // @formatter:off + http + .securityMatchers((matchers) -> matchers + .requestMatchers("/path") + ) + .httpBasic(withDefaults()) + .authorizeHttpRequests((authorize) -> authorize + .anyRequest().denyAll() + ); + // @formatter:on + return http.build(); + } + + @RestController + static class PathController { + + @RequestMapping("/path") + String path() { + return "path"; + } + + } + + } + @Configuration static class UsersConfig {