diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 9f324f6298..08b35fa8ce 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * 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. @@ -219,9 +219,14 @@ public abstract class AbstractRequestMatcherRegistry { } List matchers = new ArrayList<>(); for (String pattern : patterns) { - AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null); - MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0); - matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant)); + if (RequestMatcherFactory.usesPathPatterns()) { + matchers.add(RequestMatcherFactory.matcher(method, pattern)); + } + else { + AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null); + MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0); + matchers.add(new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant)); + } } return requestMatchers(matchers.toArray(new RequestMatcher[0])); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/RequestMatcherFactory.java b/config/src/main/java/org/springframework/security/config/annotation/web/RequestMatcherFactory.java new file mode 100644 index 0000000000..e6cca45bf1 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/web/RequestMatcherFactory.java @@ -0,0 +1,59 @@ +/* + * 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.config.annotation.web; + +import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; + +/** + * This utility exists only to facilitate applications opting into using path patterns in + * the HttpSecurity DSL. It is for internal use only. + * + * @deprecated + */ +@Deprecated(forRemoval = true) +public final class RequestMatcherFactory { + + private static PathPatternRequestMatcher.Builder builder; + + public static void setApplicationContext(ApplicationContext context) { + builder = context.getBeanProvider(PathPatternRequestMatcher.Builder.class).getIfUnique(); + } + + public static boolean usesPathPatterns() { + return builder != null; + } + + public static RequestMatcher matcher(String path) { + return matcher(null, path); + } + + public static RequestMatcher matcher(HttpMethod method, String path) { + if (builder != null) { + return builder.matcher(method, path); + } + return new AntPathRequestMatcher(path, (method != null) ? method.name() : null); + } + + private RequestMatcherFactory() { + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index f96c943d55..778a1243ad 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * 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. @@ -45,6 +45,7 @@ import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration; import org.springframework.security.config.annotation.web.configurers.AnonymousConfigurer; @@ -3684,11 +3685,17 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder matchers = new ArrayList<>(); + for (String pattern : patterns) { + if (RequestMatcherFactory.usesPathPatterns()) { + matchers.add(RequestMatcherFactory.matcher(pattern)); + } + else { + RequestMatcher matcher = mvcPresent ? createMvcMatcher(pattern) : createAntMatcher(pattern); + matchers.add(matcher); + } } - this.requestMatcher = new OrRequestMatcher(createAntMatchers(patterns)); + this.requestMatcher = new OrRequestMatcher(matchers); return this; } @@ -3717,15 +3724,11 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder createAntMatchers(String... patterns) { - List matchers = new ArrayList<>(patterns.length); - for (String pattern : patterns) { - matchers.add(new AntPathRequestMatcher(pattern)); - } - return matchers; + private RequestMatcher createAntMatcher(String pattern) { + return new AntPathRequestMatcher(pattern); } - private List createMvcMatchers(String... mvcPatterns) { + private RequestMatcher createMvcMatcher(String mvcPattern) { ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class); ObjectProvider> postProcessors = getContext().getBeanProvider(type); ObjectPostProcessor opp = postProcessors.getObject(); @@ -3736,13 +3739,9 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder matchers = new ArrayList<>(mvcPatterns.length); - for (String mvcPattern : mvcPatterns) { - MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern); - opp.postProcess(matcher); - matchers.add(matcher); - } - return matchers; + MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern); + opp.postProcess(matcher); + return matcher; } /** 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 3cd79c9967..8c103b9fee 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 @@ -35,6 +35,7 @@ import org.springframework.security.config.annotation.authentication.configurati import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.authentication.configurers.provisioning.JdbcUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.authentication.configurers.userdetails.DaoAuthenticationConfigurer; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer; @@ -104,6 +105,7 @@ class HttpSecurityConfiguration { @Bean(HTTPSECURITY_BEAN_NAME) @Scope("prototype") HttpSecurity httpSecurity() throws Exception { + RequestMatcherFactory.setApplicationContext(this.context); LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(this.context); AuthenticationManagerBuilder authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder( this.objectPostProcessor, passwordEncoder); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java index b28e57e4d3..1dfbed3036 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/FormLoginConfigurer.java @@ -16,7 +16,9 @@ package org.springframework.security.config.annotation.web.configurers; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.AuthenticationEntryPoint; @@ -26,7 +28,6 @@ import org.springframework.security.web.authentication.RememberMeServices; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; /** @@ -234,7 +235,7 @@ public final class FormLoginConfigurer> extends @Override protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) { - return new AntPathRequestMatcher(loginProcessingUrl, "POST"); + return RequestMatcherFactory.matcher(HttpMethod.POST, loginProcessingUrl); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java index 04a2ac35cd..7797a88f4b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/LogoutConfigurer.java @@ -22,8 +22,10 @@ import java.util.List; import jakarta.servlet.http.HttpSession; +import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.SecurityConfigurer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler; @@ -37,7 +39,6 @@ import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuc import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.SecurityContextRepository; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; @@ -368,7 +369,7 @@ public final class LogoutConfigurer> } private RequestMatcher createLogoutRequestMatcher(String httpMethod) { - return new AntPathRequestMatcher(this.logoutUrl, httpMethod); + return RequestMatcherFactory.matcher(HttpMethod.valueOf(httpMethod), this.logoutUrl); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurer.java index 0f9b52f657..7d5794b826 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/PasswordManagementConfigurer.java @@ -17,9 +17,9 @@ package org.springframework.security.config.annotation.web.configurers; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.web.RequestMatcherRedirectFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; /** @@ -55,7 +55,7 @@ public final class PasswordManagementConfigurer @Override public void configure(B http) throws Exception { RequestMatcherRedirectFilter changePasswordFilter = new RequestMatcherRedirectFilter( - new AntPathRequestMatcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN), this.changePasswordPage); + RequestMatcherFactory.matcher(WELL_KNOWN_CHANGE_PASSWORD_PATTERN), this.changePasswordPage); http.addFilterBefore(postProcess(changePasswordFilter), UsernamePasswordAuthenticationFilter.class); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java index 712c89073f..ca59dd5b05 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurer.java @@ -21,8 +21,10 @@ import java.util.Collections; import java.util.List; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.NullRequestCache; @@ -140,13 +142,13 @@ public final class RequestCacheConfigurer> @SuppressWarnings("unchecked") private RequestMatcher createDefaultSavedRequestMatcher(H http) { - RequestMatcher notFavIcon = new NegatedRequestMatcher(new AntPathRequestMatcher("/**/favicon.*")); + RequestMatcher notFavIcon = new NegatedRequestMatcher(getFaviconRequestMatcher()); RequestMatcher notXRequestedWith = new NegatedRequestMatcher( new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest")); boolean isCsrfEnabled = http.getConfigurer(CsrfConfigurer.class) != null; List matchers = new ArrayList<>(); if (isCsrfEnabled) { - RequestMatcher getRequests = new AntPathRequestMatcher("/**", "GET"); + RequestMatcher getRequests = RequestMatcherFactory.matcher(HttpMethod.GET, "/**"); matchers.add(0, getRequests); } matchers.add(notFavIcon); @@ -167,4 +169,13 @@ public final class RequestCacheConfigurer> return new NegatedRequestMatcher(mediaRequest); } + private RequestMatcher getFaviconRequestMatcher() { + if (RequestMatcherFactory.usesPathPatterns()) { + return RequestMatcherFactory.matcher("/favicon.*"); + } + else { + return new AntPathRequestMatcher("/**/favicon.*"); + } + } + } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java index 4c53b3293d..4dc1af29e3 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/oauth2/client/OAuth2LoginConfigurer.java @@ -38,6 +38,7 @@ import org.springframework.core.ResolvableType; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -91,7 +92,6 @@ import org.springframework.security.web.authentication.ui.DefaultLoginPageGenera import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.savedrequest.RequestCache; import org.springframework.security.web.util.matcher.AndRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; @@ -431,7 +431,7 @@ public final class OAuth2LoginConfigurer> @Override protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) { - return new AntPathRequestMatcher(loginProcessingUrl); + return RequestMatcherFactory.matcher(loginProcessingUrl); } private OAuth2AuthorizationRequestResolver getAuthorizationRequestResolver() { @@ -569,8 +569,8 @@ public final class OAuth2LoginConfigurer> } private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLoginPage) { - RequestMatcher loginPageMatcher = new AntPathRequestMatcher(this.getLoginPage()); - RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico"); + RequestMatcher loginPageMatcher = RequestMatcherFactory.matcher(this.getLoginPage()); + RequestMatcher faviconMatcher = RequestMatcherFactory.matcher("/favicon.ico"); RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(http); RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher( new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java index 6f1e02ca6e..7be52d83c1 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ott/OneTimeTokenLoginConfigurer.java @@ -31,6 +31,7 @@ import org.springframework.security.authentication.ott.OneTimeTokenAuthenticatio import org.springframework.security.authentication.ott.OneTimeTokenService; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; @@ -56,8 +57,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher; - /** * An {@link AbstractHttpConfigurer} for One-Time Token Login. * @@ -163,7 +162,7 @@ public final class OneTimeTokenLoginConfigurer> private void configureOttGenerateFilter(H http) { GenerateOneTimeTokenFilter generateFilter = new GenerateOneTimeTokenFilter(getOneTimeTokenService(), getOneTimeTokenGenerationSuccessHandler()); - generateFilter.setRequestMatcher(antMatcher(HttpMethod.POST, this.tokenGeneratingUrl)); + generateFilter.setRequestMatcher(RequestMatcherFactory.matcher(HttpMethod.POST, this.tokenGeneratingUrl)); generateFilter.setRequestResolver(getGenerateRequestResolver()); http.addFilter(postProcess(generateFilter)); http.addFilter(DefaultResourcesFilter.css()); @@ -190,7 +189,7 @@ public final class OneTimeTokenLoginConfigurer> } DefaultOneTimeTokenSubmitPageGeneratingFilter submitPage = new DefaultOneTimeTokenSubmitPageGeneratingFilter(); submitPage.setResolveHiddenInputs(this::hiddenInputs); - submitPage.setRequestMatcher(antMatcher(HttpMethod.GET, this.defaultSubmitPageUrl)); + submitPage.setRequestMatcher(RequestMatcherFactory.matcher(HttpMethod.GET, this.defaultSubmitPageUrl)); submitPage.setLoginProcessingUrl(this.getLoginProcessingUrl()); http.addFilter(postProcess(submitPage)); } @@ -207,7 +206,7 @@ public final class OneTimeTokenLoginConfigurer> @Override protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) { - return antMatcher(HttpMethod.POST, loginProcessingUrl); + return RequestMatcherFactory.matcher(HttpMethod.POST, loginProcessingUrl); } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java index b07b034d14..b5d683a3ce 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LoginConfigurer.java @@ -28,6 +28,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractAuthenticationFilterConfigurer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; @@ -56,7 +57,6 @@ import org.springframework.security.web.authentication.DelegatingAuthenticationE import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter; import org.springframework.security.web.util.matcher.AndRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.NegatedRequestMatcher; import org.springframework.security.web.util.matcher.OrRequestMatcher; import org.springframework.security.web.util.matcher.ParameterRequestMatcher; @@ -127,15 +127,11 @@ public final class Saml2LoginConfigurer> private String[] authenticationRequestParams = { "registrationId={registrationId}" }; - private RequestMatcher authenticationRequestMatcher = RequestMatchers.anyOf( - new AntPathRequestMatcher(Saml2AuthenticationRequestResolver.DEFAULT_AUTHENTICATION_REQUEST_URI), - new AntPathQueryRequestMatcher(this.authenticationRequestUri, this.authenticationRequestParams)); + private RequestMatcher authenticationRequestMatcher; private Saml2AuthenticationRequestResolver authenticationRequestResolver; - private RequestMatcher loginProcessingUrl = RequestMatchers.anyOf( - new AntPathRequestMatcher(Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI), - new AntPathRequestMatcher("/login/saml2/sso")); + private RequestMatcher loginProcessingUrl; private RelyingPartyRegistrationRepository relyingPartyRegistrationRepository; @@ -238,8 +234,8 @@ public final class Saml2LoginConfigurer> this.authenticationRequestUri = parts[0]; this.authenticationRequestParams = new String[parts.length - 1]; System.arraycopy(parts, 1, this.authenticationRequestParams, 0, parts.length - 1); - this.authenticationRequestMatcher = new AntPathQueryRequestMatcher(this.authenticationRequestUri, - this.authenticationRequestParams); + this.authenticationRequestMatcher = new PathQueryRequestMatcher( + RequestMatcherFactory.matcher(this.authenticationRequestUri), this.authenticationRequestParams); return this; } @@ -256,13 +252,13 @@ public final class Saml2LoginConfigurer> @Override public Saml2LoginConfigurer loginProcessingUrl(String loginProcessingUrl) { Assert.hasText(loginProcessingUrl, "loginProcessingUrl cannot be empty"); - this.loginProcessingUrl = new AntPathRequestMatcher(loginProcessingUrl); + this.loginProcessingUrl = RequestMatcherFactory.matcher(loginProcessingUrl); return this; } @Override protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) { - return new AntPathRequestMatcher(loginProcessingUrl); + return RequestMatcherFactory.matcher(loginProcessingUrl); } /** @@ -284,7 +280,7 @@ public final class Saml2LoginConfigurer> relyingPartyRegistrationRepository(http); this.saml2WebSsoAuthenticationFilter = new Saml2WebSsoAuthenticationFilter(getAuthenticationConverter(http)); this.saml2WebSsoAuthenticationFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy()); - this.saml2WebSsoAuthenticationFilter.setRequiresAuthenticationRequestMatcher(this.loginProcessingUrl); + this.saml2WebSsoAuthenticationFilter.setRequiresAuthenticationRequestMatcher(getLoginProcessingEndpoint()); setAuthenticationRequestRepository(http, this.saml2WebSsoAuthenticationFilter); setAuthenticationFilter(this.saml2WebSsoAuthenticationFilter); if (StringUtils.hasText(this.loginPage)) { @@ -340,8 +336,8 @@ public final class Saml2LoginConfigurer> } private AuthenticationEntryPoint getLoginEntryPoint(B http, String providerLoginPage) { - RequestMatcher loginPageMatcher = new AntPathRequestMatcher(this.getLoginPage()); - RequestMatcher faviconMatcher = new AntPathRequestMatcher("/favicon.ico"); + RequestMatcher loginPageMatcher = RequestMatcherFactory.matcher(this.getLoginPage()); + RequestMatcher faviconMatcher = RequestMatcherFactory.matcher("/favicon.ico"); RequestMatcher defaultEntryPointMatcher = this.getAuthenticationEntryPointMatcher(http); RequestMatcher defaultLoginPageMatcher = new AndRequestMatcher( new OrRequestMatcher(loginPageMatcher, faviconMatcher), defaultEntryPointMatcher); @@ -376,17 +372,38 @@ public final class Saml2LoginConfigurer> if (USE_OPENSAML_5) { OpenSaml5AuthenticationRequestResolver openSamlAuthenticationRequestResolver = new OpenSaml5AuthenticationRequestResolver( relyingPartyRegistrationRepository(http)); - openSamlAuthenticationRequestResolver.setRequestMatcher(this.authenticationRequestMatcher); + openSamlAuthenticationRequestResolver.setRequestMatcher(getAuthenticationRequestMatcher()); return openSamlAuthenticationRequestResolver; } else { OpenSaml4AuthenticationRequestResolver openSamlAuthenticationRequestResolver = new OpenSaml4AuthenticationRequestResolver( relyingPartyRegistrationRepository(http)); - openSamlAuthenticationRequestResolver.setRequestMatcher(this.authenticationRequestMatcher); + openSamlAuthenticationRequestResolver.setRequestMatcher(getAuthenticationRequestMatcher()); return openSamlAuthenticationRequestResolver; } } + private RequestMatcher getAuthenticationRequestMatcher() { + if (this.authenticationRequestMatcher == null) { + this.authenticationRequestMatcher = RequestMatchers.anyOf( + RequestMatcherFactory + .matcher(Saml2AuthenticationRequestResolver.DEFAULT_AUTHENTICATION_REQUEST_URI), + new PathQueryRequestMatcher(RequestMatcherFactory.matcher(this.authenticationRequestUri), + this.authenticationRequestParams)); + } + return this.authenticationRequestMatcher; + } + + private RequestMatcher getLoginProcessingEndpoint() { + if (this.loginProcessingUrl == null) { + this.loginProcessingUrl = RequestMatchers.anyOf( + RequestMatcherFactory.matcher(Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI), + RequestMatcherFactory.matcher("/login/saml2/sso")); + } + + return this.loginProcessingUrl; + } + private AuthenticationConverter getAuthenticationConverter(B http) { if (this.authenticationConverter != null) { return this.authenticationConverter; @@ -407,7 +424,7 @@ public final class Saml2LoginConfigurer> OpenSaml5AuthenticationTokenConverter converter = new OpenSaml5AuthenticationTokenConverter( this.relyingPartyRegistrationRepository); converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); - converter.setRequestMatcher(this.loginProcessingUrl); + converter.setRequestMatcher(getLoginProcessingEndpoint()); return converter; } authenticationConverterBean = getBeanOrNull(http, OpenSaml4AuthenticationTokenConverter.class); @@ -417,7 +434,7 @@ public final class Saml2LoginConfigurer> OpenSaml4AuthenticationTokenConverter converter = new OpenSaml4AuthenticationTokenConverter( this.relyingPartyRegistrationRepository); converter.setAuthenticationRequestRepository(getAuthenticationRequestRepository(http)); - converter.setRequestMatcher(this.loginProcessingUrl); + converter.setRequestMatcher(getLoginProcessingEndpoint()); return converter; } @@ -441,7 +458,7 @@ public final class Saml2LoginConfigurer> if (csrf == null) { return; } - csrf.ignoringRequestMatchers(this.loginProcessingUrl); + csrf.ignoringRequestMatchers(getLoginProcessingEndpoint()); } private void initDefaultLoginFilter(B http) { @@ -509,13 +526,13 @@ public final class Saml2LoginConfigurer> } } - static class AntPathQueryRequestMatcher implements RequestMatcher { + static class PathQueryRequestMatcher implements RequestMatcher { private final RequestMatcher matcher; - AntPathQueryRequestMatcher(String path, String... params) { + PathQueryRequestMatcher(RequestMatcher pathMatcher, String... params) { List matchers = new ArrayList<>(); - matchers.add(new AntPathRequestMatcher(path)); + matchers.add(pathMatcher); for (String param : params) { String[] parts = param.split("="); if (parts.length == 1) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java index 92c7cef819..e69f8c825b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2LogoutConfigurer.java @@ -23,9 +23,11 @@ import jakarta.servlet.http.HttpServletRequest; import org.opensaml.core.Version; import org.springframework.context.ApplicationContext; +import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer; @@ -64,7 +66,6 @@ import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.csrf.CsrfLogoutHandler; import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.util.matcher.AndRequestMatcher; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.ParameterRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; @@ -304,19 +305,19 @@ public final class Saml2LogoutConfigurer> } private RequestMatcher createLogoutMatcher() { - RequestMatcher logout = new AntPathRequestMatcher(this.logoutUrl, "POST"); + RequestMatcher logout = RequestMatcherFactory.matcher(HttpMethod.POST, this.logoutUrl); RequestMatcher saml2 = new Saml2RequestMatcher(getSecurityContextHolderStrategy()); return new AndRequestMatcher(logout, saml2); } private RequestMatcher createLogoutRequestMatcher() { - RequestMatcher logout = new AntPathRequestMatcher(this.logoutRequestConfigurer.logoutUrl); + RequestMatcher logout = RequestMatcherFactory.matcher(this.logoutRequestConfigurer.logoutUrl); RequestMatcher samlRequest = new ParameterRequestMatcher("SAMLRequest"); return new AndRequestMatcher(logout, samlRequest); } private RequestMatcher createLogoutResponseMatcher() { - RequestMatcher logout = new AntPathRequestMatcher(this.logoutResponseConfigurer.logoutUrl); + RequestMatcher logout = RequestMatcherFactory.matcher(this.logoutResponseConfigurer.logoutUrl); RequestMatcher samlResponse = new ParameterRequestMatcher("SAMLResponse"); return new AndRequestMatcher(logout, samlResponse); } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java index 349e3a6606..5a822c9360 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/saml2/Saml2MetadataConfigurer.java @@ -22,6 +22,7 @@ import org.opensaml.core.Version; import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; +import org.springframework.security.config.annotation.web.RequestMatcherFactory; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.saml2.provider.service.metadata.OpenSaml4MetadataResolver; @@ -32,7 +33,6 @@ import org.springframework.security.saml2.provider.service.registration.RelyingP import org.springframework.security.saml2.provider.service.web.Saml2MetadataFilter; import org.springframework.security.saml2.provider.service.web.metadata.RequestMatcherMetadataResponseResolver; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; /** @@ -111,12 +111,12 @@ public class Saml2MetadataConfigurer> if (USE_OPENSAML_5) { RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver( registrations, new OpenSaml5MetadataResolver()); - metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl)); + metadata.setRequestMatcher(RequestMatcherFactory.matcher(metadataUrl)); return metadata; } RequestMatcherMetadataResponseResolver metadata = new RequestMatcherMetadataResponseResolver(registrations, new OpenSaml4MetadataResolver()); - metadata.setRequestMatcher(new AntPathRequestMatcher(metadataUrl)); + metadata.setRequestMatcher(RequestMatcherFactory.matcher(metadataUrl)); return metadata; }; return this; diff --git a/config/src/main/java/org/springframework/security/config/web/PathPatternRequestMatcherBuilderFactoryBean.java b/config/src/main/java/org/springframework/security/config/web/PathPatternRequestMatcherBuilderFactoryBean.java new file mode 100644 index 0000000000..391b991747 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/web/PathPatternRequestMatcherBuilderFactoryBean.java @@ -0,0 +1,133 @@ +/* + * 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.config.web; + +import reactor.util.annotation.NonNull; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.web.util.pattern.PathPatternParser; + +/** + * Use this factory bean to configure the {@link PathPatternRequestMatcher.Builder} bean + * used to create request matchers in {@link AuthorizeHttpRequestsConfigurer} and other + * parts of the DSL. + * + * @author Josh Cummings + * @since 6.5 + */ +public final class PathPatternRequestMatcherBuilderFactoryBean implements + FactoryBean, ApplicationContextAware, BeanNameAware, BeanFactoryAware { + + static final String MVC_PATTERN_PARSER_BEAN_NAME = "mvcPatternParser"; + + private final PathPatternParser parser; + + private ApplicationContext context; + + private String beanName; + + private ConfigurableListableBeanFactory beanFactory; + + /** + * Construct this factory bean using the default {@link PathPatternParser} + * + *

+ * If you are using Spring MVC, it will use the Spring MVC instance. + */ + public PathPatternRequestMatcherBuilderFactoryBean() { + this(null); + } + + /** + * Construct this factory bean using this {@link PathPatternParser}. + * + *

+ * If you are using Spring MVC, it is likely incorrect to call this constructor. + * Please call the default constructor instead. + * @param parser the {@link PathPatternParser} to use + */ + public PathPatternRequestMatcherBuilderFactoryBean(PathPatternParser parser) { + this.parser = parser; + } + + @Override + public PathPatternRequestMatcher.Builder getObject() throws Exception { + if (!this.context.containsBean(MVC_PATTERN_PARSER_BEAN_NAME)) { + PathPatternParser parser = (this.parser != null) ? this.parser : PathPatternParser.defaultInstance; + return PathPatternRequestMatcher.withPathPatternParser(parser); + } + PathPatternParser mvc = this.context.getBean(MVC_PATTERN_PARSER_BEAN_NAME, PathPatternParser.class); + PathPatternParser parser = (this.parser != null) ? this.parser : mvc; + if (mvc.equals(parser)) { + return PathPatternRequestMatcher.withPathPatternParser(parser); + } + throw new IllegalArgumentException("Spring Security and Spring MVC must use the same path pattern parser. " + + "To have Spring Security use Spring MVC's [" + describe(mvc, MVC_PATTERN_PARSER_BEAN_NAME) + + "] simply publish this bean [" + describe(this, this.beanName) + "] using its default constructor"); + } + + @Override + public Class getObjectType() { + return PathPatternRequestMatcher.Builder.class; + } + + @Override + public void setApplicationContext(ApplicationContext context) throws BeansException { + this.context = context; + } + + @Override + public void setBeanName(@NonNull String name) { + this.beanName = name; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + if (beanFactory instanceof ConfigurableListableBeanFactory listable) { + this.beanFactory = listable; + } + } + + private String describe(Object bean, String name) { + String text = bean.getClass().getSimpleName(); + if (name == null) { + return text; + } + text += "defined as '" + name + "'"; + if (this.beanFactory == null) { + return text; + } + BeanDefinition bd = this.beanFactory.getBeanDefinition(name); + String description = bd.getResourceDescription(); + if (description == null) { + return text; + } + text += " in [" + description + "]"; + return text; + } + +} 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 41850d6756..57d2a92ded 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 @@ -51,6 +51,7 @@ import org.springframework.security.config.core.GrantedAuthorityDefaults; import org.springframework.security.config.observation.SecurityObservationSettings; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -64,6 +65,7 @@ import org.springframework.security.web.access.intercept.AuthorizationFilter; import org.springframework.security.web.access.intercept.RequestAuthorizationContext; import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.request.RequestPostProcessor; @@ -72,6 +74,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @@ -667,6 +670,26 @@ public class AuthorizeHttpRequestsConfigurerTests { verifyNoInteractions(handler); } + @Test + public void requestMatchersWhenMultipleDispatcherServletsAndPathBeanThenAllows() throws Exception { + this.spring.register(MvcRequestMatcherBuilderConfig.class, BasicController.class) + .postProcessor((context) -> context.getServletContext() + .addServlet("otherDispatcherServlet", DispatcherServlet.class) + .addMapping("/mvc")) + .autowire(); + this.mvc.perform(get("/mvc/path").servletPath("/mvc").with(user("user"))).andExpect(status().isOk()); + this.mvc.perform(get("/mvc/path").servletPath("/mvc").with(user("user").roles("DENIED"))) + .andExpect(status().isForbidden()); + this.mvc.perform(get("/path").with(user("user"))).andExpect(status().isForbidden()); + } + + @Test + public void requestMatchersWhenFactoryBeanThenAuthorizes() throws Exception { + this.spring.register(PathPatternFactoryBeanConfig.class).autowire(); + this.mvc.perform(get("/path/resource")).andExpect(status().isUnauthorized()); + this.mvc.perform(get("/path/resource").with(user("user").roles("USER"))).andExpect(status().isNotFound()); + } + @Configuration @EnableWebSecurity static class GrantedAuthorityDefaultHasRoleConfig { @@ -1262,6 +1285,10 @@ public class AuthorizeHttpRequestsConfigurerTests { void rootPost() { } + @GetMapping("/path") + void path() { + } + } @Configuration @@ -1317,4 +1344,50 @@ public class AuthorizeHttpRequestsConfigurerTests { } + @Configuration + @EnableWebSecurity + @EnableWebMvc + static class MvcRequestMatcherBuilderConfig { + + @Bean + SecurityFilterChain security(HttpSecurity http) throws Exception { + PathPatternRequestMatcher.Builder mvc = PathPatternRequestMatcher.withDefaults().servletPath("/mvc"); + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers(mvc.matcher("/path/**")).hasRole("USER") + ) + .httpBasic(withDefaults()); + // @formatter:on + + return http.build(); + } + + } + + @Configuration + @EnableWebSecurity + @EnableWebMvc + static class PathPatternFactoryBeanConfig { + + @Bean + SecurityFilterChain security(HttpSecurity http) throws Exception { + // @formatter:off + http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/path/**").hasRole("USER") + ) + .httpBasic(withDefaults()); + // @formatter:on + + return http.build(); + } + + @Bean + PathPatternRequestMatcherBuilderFactoryBean pathPatternFactoryBean() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java index f22e55043d..e6a597e0a8 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/RequestCacheConfigurerTests.java @@ -34,6 +34,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.test.SpringTestContext; import org.springframework.security.config.test.SpringTestContextExtension; +import org.springframework.security.config.web.PathPatternRequestMatcherBuilderFactoryBean; import org.springframework.security.core.userdetails.User; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.test.web.servlet.RequestCacheResultMatcher; @@ -291,6 +292,22 @@ public class RequestCacheConfigurerTests { this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/")); } + @Test + public void getWhenPathPatternFactoryBeanThenFaviconIcoRedirectsToRoot() throws Exception { + this.spring + .register(RequestCacheDefaultsConfig.class, DefaultSecurityConfig.class, PathPatternFactoryBeanConfig.class) + .autowire(); + // @formatter:off + MockHttpSession session = (MockHttpSession) this.mvc.perform(get("/favicon.ico")) + .andExpect(redirectedUrl("http://localhost/login")) + .andReturn() + .getRequest() + .getSession(); + // @formatter:on + // ignores favicon.ico + this.mvc.perform(formLogin(session)).andExpect(redirectedUrl("/")); + } + private static RequestBuilder formLogin(MockHttpSession session) { // @formatter:off return post("/login") @@ -470,4 +487,15 @@ public class RequestCacheConfigurerTests { } + @Configuration + @EnableWebSecurity + static class PathPatternFactoryBeanConfig { + + @Bean + PathPatternRequestMatcherBuilderFactoryBean factoryBean() { + return new PathPatternRequestMatcherBuilderFactoryBean(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/web/PathPatternRequestMatcherBuilderFactoryBeanTests.java b/config/src/test/java/org/springframework/security/config/web/PathPatternRequestMatcherBuilderFactoryBeanTests.java new file mode 100644 index 0000000000..79819e1185 --- /dev/null +++ b/config/src/test/java/org/springframework/security/config/web/PathPatternRequestMatcherBuilderFactoryBeanTests.java @@ -0,0 +1,99 @@ +/* + * 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.config.web; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; + +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; +import org.springframework.web.util.pattern.PathPatternParser; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class PathPatternRequestMatcherBuilderFactoryBeanTests { + + GenericApplicationContext context; + + @BeforeEach + void setUp() { + this.context = new GenericApplicationContext(); + } + + @Test + void getObjectWhenDefaultsThenBuilder() throws Exception { + factoryBean().getObject(); + } + + @Test + void getObjectWhenMvcPatternParserThenUses() throws Exception { + PathPatternParser mvc = registerMvcPatternParser(); + PathPatternRequestMatcher.Builder builder = factoryBean().getObject(); + builder.matcher("/path/**"); + verify(mvc).parse("/path/**"); + } + + @Test + void getObjectWhenPathPatternParserThenUses() throws Exception { + PathPatternParser parser = mock(PathPatternParser.class); + PathPatternRequestMatcher.Builder builder = factoryBean(parser).getObject(); + builder.matcher("/path/**"); + verify(parser).parse("/path/**"); + } + + @Test + void getObjectWhenMvcAndPathPatternParserConflictThenIllegalArgument() { + registerMvcPatternParser(); + PathPatternParser parser = mock(PathPatternParser.class); + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> factoryBean(parser).getObject()); + } + + @Test + void getObjectWhenMvcAndPathPatternParserAgreeThenUses() throws Exception { + PathPatternParser mvc = registerMvcPatternParser(); + PathPatternRequestMatcher.Builder builder = factoryBean(mvc).getObject(); + builder.matcher("/path/**"); + verify(mvc).parse("/path/**"); + } + + PathPatternRequestMatcherBuilderFactoryBean factoryBean() { + PathPatternRequestMatcherBuilderFactoryBean factoryBean = new PathPatternRequestMatcherBuilderFactoryBean(); + factoryBean.setApplicationContext(this.context); + return factoryBean; + } + + PathPatternRequestMatcherBuilderFactoryBean factoryBean(PathPatternParser parser) { + PathPatternRequestMatcherBuilderFactoryBean factoryBean = new PathPatternRequestMatcherBuilderFactoryBean( + parser); + factoryBean.setApplicationContext(this.context); + return factoryBean; + } + + PathPatternParser registerMvcPatternParser() { + PathPatternParser mvc = mock(PathPatternParser.class); + this.context.registerBean(PathPatternRequestMatcherBuilderFactoryBean.MVC_PATTERN_PARSER_BEAN_NAME, + PathPatternParser.class, () -> mvc); + this.context.refresh(); + return mvc; + } + +} diff --git a/docs/modules/ROOT/pages/migration/web.adoc b/docs/modules/ROOT/pages/migration/web.adoc new file mode 100644 index 0000000000..23716dbf6c --- /dev/null +++ b/docs/modules/ROOT/pages/migration/web.adoc @@ -0,0 +1,92 @@ += Web Migrations + +[[use-path-pattern]] +== Use PathPatternRequestMatcher by Default + +In Spring Security 7, `AntPathRequestMatcher` and `MvcRequestMatcher` are no longer supported and the Java DSL requires that all URIs be absolute (less any context root). +At that time, Spring Security 7 will use `PathPatternRequestMatcher` by default. + +To check how prepared you are for this change, you can publish this bean: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +PathPatternRequestMatcherBuilderFactoryBean requestMatcherBuilder() { + return new PathPatternRequestMatcherBuilderFactoryBean(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun requestMatcherBuilder(): PathPatternRequestMatcherBuilderFactoryBean { + return PathPatternRequestMatcherBuilderFactoryBean() +} +---- +====== + +This will tell the Spring Security DSL to use `PathPatternRequestMatcher` for all request matchers that it constructs. + +In the event that you are directly constructing an object (as opposed to having the DSL construct it) that has a `setRequestMatcher` method. you should also proactively specify a `PathPatternRequestMatcher` there as well. + +For example, in the case of `LogoutFilter`, it constructs an `AntPathRequestMatcher` in Spring Security 6: + +[method,java] +---- +private RequestMatcher logoutUrl = new AntPathRequestMatcher("/logout"); +---- + +and will change this to a `PathPatternRequestMatcher` in 7: + +[method,java] +---- +private RequestMatcher logoutUrl = PathPatternRequestMatcher.path().matcher("/logout"); +---- + +If you are constructing your own `LogoutFilter`, consider calling `setLogoutRequestMatcher` to provide this `PathPatternRequestMatcher` in advance. + +== Include the Servlet Path Prefix in Authorization Rules + +For many applications <> will make no difference since most commonly all URIs listed are matched by the default servlet. + +However, if you have other servlets with servlet path prefixes, xref:servlet/authorization/authorize-http-requests.adoc[then these paths now need to be supplied separately]. + +For example, if I have a Spring MVC controller with `@RequestMapping("/orders")` and my MVC application is deployed to `/mvc` (instead of the default servlet), then the URI for this endpoint is `/mvc/orders`. +Historically, the Java DSL hasn't had a simple way to specify the servlet path prefix and Spring Security attempted to infer it. + +Over time, we learned that these inference would surprise developers. +Instead of taking this responsibility away from developers, now it is simpler to specify the servlet path prefix like so: + +[method,java] +---- +PathPatternRequestParser.Builder servlet = PathPatternRequestParser.servletPath("/mvc"); +http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers(servlet.pattern("/orders/**").matcher()).authenticated() + ) +---- + + +For paths that belong to the default servlet, use `PathPatternRequestParser.path()` instead: + +[method,java] +---- +PathPatternRequestParser.Builder request = PathPatternRequestParser.path(); +http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers(request.pattern("/js/**").matcher()).authenticated() + ) +---- + +Note that this doesn't address every kind of servlet since not all servlets have a path prefix. +For example, expressions that match the JSP Servlet might use an ant pattern `/**/*.jsp`. + +There is not yet a general-purpose replacement for these, and so you are encouraged to use `RegexRequestMatcher`, like so: `regexMatcher("\\.jsp$")`. + +For many applications this will make no difference since most commonly all URIs listed are matched by the default servlet.