mirror of
https://github.com/spring-projects/spring-security.git
synced 2025-07-12 13:23:29 +00:00
Add Opt-in PathPattern Strategy
Closes gh-16573
This commit is contained in:
parent
588220a020
commit
7d301f87d6
@ -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]));
|
||||
}
|
||||
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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.*");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
92
docs/modules/ROOT/pages/migration/web.adoc
Normal file
92
docs/modules/ROOT/pages/migration/web.adoc
Normal file
@ -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 <<use-path-pattern, the above>> 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.
|
Loading…
x
Reference in New Issue
Block a user