Add Opt-in PathPattern Strategy

Closes gh-16573
This commit is contained in:
Josh Cummings
2025-02-21 13:27:19 -07:00
parent 588220a020
commit 7d301f87d6
18 changed files with 589 additions and 69 deletions

View File

@@ -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<C> {
}
List<RequestMatcher> 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]));
}

View File

@@ -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() {
}
}

View File

@@ -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<Defaul
* @see MvcRequestMatcher
*/
public HttpSecurity securityMatcher(String... patterns) {
if (mvcPresent) {
this.requestMatcher = new OrRequestMatcher(createMvcMatchers(patterns));
return this;
List<RequestMatcher> 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<Defaul
return HttpSecurity.this;
}
private List<RequestMatcher> createAntMatchers(String... patterns) {
List<RequestMatcher> 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<RequestMatcher> createMvcMatchers(String... mvcPatterns) {
private RequestMatcher createMvcMatcher(String mvcPattern) {
ResolvableType type = ResolvableType.forClassWithGenerics(ObjectPostProcessor.class, Object.class);
ObjectProvider<ObjectPostProcessor<Object>> postProcessors = getContext().getBeanProvider(type);
ObjectPostProcessor<Object> opp = postProcessors.getObject();
@@ -3736,13 +3739,9 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<Defaul
}
HandlerMappingIntrospector introspector = getContext().getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME,
HandlerMappingIntrospector.class);
List<RequestMatcher> 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;
}
/**

View File

@@ -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);

View File

@@ -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<H extends HttpSecurityBuilder<H>> extends
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return new AntPathRequestMatcher(loginProcessingUrl, "POST");
return RequestMatcherFactory.matcher(HttpMethod.POST, loginProcessingUrl);
}
/**

View File

@@ -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<H extends HttpSecurityBuilder<H>>
}
private RequestMatcher createLogoutRequestMatcher(String httpMethod) {
return new AntPathRequestMatcher(this.logoutUrl, httpMethod);
return RequestMatcherFactory.matcher(HttpMethod.valueOf(httpMethod), this.logoutUrl);
}
}

View File

@@ -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<B extends HttpSecurityBuilder<B>
@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);
}

View File

@@ -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<H extends HttpSecurityBuilder<H>>
@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<RequestMatcher> 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<H extends HttpSecurityBuilder<H>>
return new NegatedRequestMatcher(mediaRequest);
}
private RequestMatcher getFaviconRequestMatcher() {
if (RequestMatcherFactory.usesPathPatterns()) {
return RequestMatcherFactory.matcher("/favicon.*");
}
else {
return new AntPathRequestMatcher("/**/favicon.*");
}
}
}

View File

@@ -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<B extends HttpSecurityBuilder<B>>
@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<B extends HttpSecurityBuilder<B>>
}
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);

View File

@@ -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<H extends HttpSecurityBuilder<H>>
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<H extends HttpSecurityBuilder<H>>
}
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<H extends HttpSecurityBuilder<H>>
@Override
protected RequestMatcher createLoginProcessingUrlMatcher(String loginProcessingUrl) {
return antMatcher(HttpMethod.POST, loginProcessingUrl);
return RequestMatcherFactory.matcher(HttpMethod.POST, loginProcessingUrl);
}
/**

View File

@@ -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<B extends HttpSecurityBuilder<B>>
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<B extends HttpSecurityBuilder<B>>
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<B extends HttpSecurityBuilder<B>>
@Override
public Saml2LoginConfigurer<B> 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<B extends HttpSecurityBuilder<B>>
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<B extends HttpSecurityBuilder<B>>
}
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<B extends HttpSecurityBuilder<B>>
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<B extends HttpSecurityBuilder<B>>
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<B extends HttpSecurityBuilder<B>>
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<B extends HttpSecurityBuilder<B>>
if (csrf == null) {
return;
}
csrf.ignoringRequestMatchers(this.loginProcessingUrl);
csrf.ignoringRequestMatchers(getLoginProcessingEndpoint());
}
private void initDefaultLoginFilter(B http) {
@@ -509,13 +526,13 @@ public final class Saml2LoginConfigurer<B extends HttpSecurityBuilder<B>>
}
}
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<RequestMatcher> matchers = new ArrayList<>();
matchers.add(new AntPathRequestMatcher(path));
matchers.add(pathMatcher);
for (String param : params) {
String[] parts = param.split("=");
if (parts.length == 1) {

View File

@@ -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<H extends HttpSecurityBuilder<H>>
}
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);
}

View File

@@ -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<H extends HttpSecurityBuilder<H>>
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;

View File

@@ -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<PathPatternRequestMatcher.Builder>, 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}
*
* <p>
* 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}.
*
* <p>
* 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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}