Deprecate HandlerMappingIntrospectorRequestTransformer

Closes gh-16536
This commit is contained in:
Josh Cummings 2025-04-03 17:09:19 -06:00
parent 1fb3fc80f9
commit f93a7a2f85
No known key found for this signature in database
GPG Key ID: 869B37A20E876129
13 changed files with 540 additions and 116 deletions

View File

@ -35,7 +35,9 @@ import org.springframework.core.ResolvableType;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.authorization.SingleResultAuthorizationManager;
import org.springframework.security.config.ObjectPostProcessor;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
import org.springframework.security.config.annotation.SecurityBuilder;
@ -58,6 +60,8 @@ import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.intercept.AuthorizationFilter;
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
import org.springframework.security.web.debug.DebugFilter;
import org.springframework.security.web.firewall.CompositeRequestRejectedHandler;
import org.springframework.security.web.firewall.HttpFirewall;
@ -65,6 +69,7 @@ import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandle
import org.springframework.security.web.firewall.ObservationMarkingRequestRejectedHandler;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.util.Assert;
@ -230,8 +235,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
/**
* Set the {@link WebInvocationPrivilegeEvaluator} to be used. If this is not
* specified, then a {@link RequestMatcherDelegatingWebInvocationPrivilegeEvaluator}
* will be created based on the list of {@link SecurityFilterChain}.
* specified, then a {@link AuthorizationManagerWebInvocationPrivilegeEvaluator} will
* be created based on the list of {@link SecurityFilterChain}.
* @param privilegeEvaluator the {@link WebInvocationPrivilegeEvaluator} to use
* @return the {@link WebSecurity} for further customizations
*/
@ -300,24 +305,33 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
+ ".addSecurityFilterChainBuilder directly");
int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
List<RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>>> requestMatcherPrivilegeEvaluatorsEntries = new ArrayList<>();
RequestMatcherDelegatingAuthorizationManager.Builder builder = RequestMatcherDelegatingAuthorizationManager
.builder();
boolean mappings = false;
for (RequestMatcher ignoredRequest : this.ignoredRequests) {
WebSecurity.this.logger.warn("You are asking Spring Security to ignore " + ignoredRequest
+ ". This is not recommended -- please use permitAll via HttpSecurity#authorizeHttpRequests instead.");
SecurityFilterChain securityFilterChain = new DefaultSecurityFilterChain(ignoredRequest);
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
builder.add(ignoredRequest, SingleResultAuthorizationManager.permitAll());
mappings = true;
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
SecurityFilterChain securityFilterChain = securityFilterChainBuilder.build();
securityFilterChains.add(securityFilterChain);
requestMatcherPrivilegeEvaluatorsEntries
.add(getRequestMatcherPrivilegeEvaluatorsEntry(securityFilterChain));
mappings = addAuthorizationManager(securityFilterChain, builder) || mappings;
}
if (this.privilegeEvaluator == null) {
AuthorizationManager<HttpServletRequest> authorizationManager = mappings ? builder.build()
: SingleResultAuthorizationManager.permitAll();
AuthorizationManagerWebInvocationPrivilegeEvaluator privilegeEvaluator = new AuthorizationManagerWebInvocationPrivilegeEvaluator(
authorizationManager);
privilegeEvaluator.setServletContext(this.servletContext);
if (this.privilegeEvaluatorRequestTransformer != null) {
privilegeEvaluator.setRequestTransformer(this.privilegeEvaluatorRequestTransformer);
}
this.privilegeEvaluator = new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(
requestMatcherPrivilegeEvaluatorsEntries);
List.of(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, List.of(privilegeEvaluator))));
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (this.httpFirewall != null) {
@ -350,30 +364,32 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
return result;
}
private RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> getRequestMatcherPrivilegeEvaluatorsEntry(
SecurityFilterChain securityFilterChain) {
List<WebInvocationPrivilegeEvaluator> privilegeEvaluators = new ArrayList<>();
private boolean addAuthorizationManager(SecurityFilterChain securityFilterChain,
RequestMatcherDelegatingAuthorizationManager.Builder builder) {
boolean mappings = false;
for (Filter filter : securityFilterChain.getFilters()) {
if (filter instanceof FilterSecurityInterceptor) {
DefaultWebInvocationPrivilegeEvaluator defaultWebInvocationPrivilegeEvaluator = new DefaultWebInvocationPrivilegeEvaluator(
(FilterSecurityInterceptor) filter);
defaultWebInvocationPrivilegeEvaluator.setServletContext(this.servletContext);
privilegeEvaluators.add(defaultWebInvocationPrivilegeEvaluator);
if (filter instanceof FilterSecurityInterceptor securityInterceptor) {
DefaultWebInvocationPrivilegeEvaluator privilegeEvaluator = new DefaultWebInvocationPrivilegeEvaluator(
securityInterceptor);
privilegeEvaluator.setServletContext(this.servletContext);
AuthorizationManager<RequestAuthorizationContext> authorizationManager = (authentication, context) -> {
HttpServletRequest request = context.getRequest();
boolean result = privilegeEvaluator.isAllowed(request.getContextPath(), request.getRequestURI(),
request.getMethod(), authentication.get());
return new AuthorizationDecision(result);
};
builder.add(securityFilterChain::matches, authorizationManager);
mappings = true;
continue;
}
if (filter instanceof AuthorizationFilter) {
AuthorizationManager<HttpServletRequest> authorizationManager = ((AuthorizationFilter) filter)
.getAuthorizationManager();
AuthorizationManagerWebInvocationPrivilegeEvaluator evaluator = new AuthorizationManagerWebInvocationPrivilegeEvaluator(
authorizationManager);
evaluator.setServletContext(this.servletContext);
if (this.privilegeEvaluatorRequestTransformer != null) {
evaluator.setRequestTransformer(this.privilegeEvaluatorRequestTransformer);
}
privilegeEvaluators.add(evaluator);
if (filter instanceof AuthorizationFilter authorization) {
AuthorizationManager<HttpServletRequest> authorizationManager = authorization.getAuthorizationManager();
builder.add(securityFilterChain::matches,
(authentication, context) -> authorizationManager.check(authentication, context.getRequest()));
mappings = true;
}
}
return new RequestMatcherEntry<>(securityFilterChain::matches, privilegeEvaluators);
return mappings;
}
@Override

View File

@ -78,6 +78,8 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
private static final String PATH_PATTERN_REQUEST_TRANSFORMER_BEAN_NAME = "pathPatternRequestTransformer";
private BeanResolver beanResolver;
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
@ -119,18 +121,8 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
}
}
/**
* Used to ensure Spring MVC request matching is cached.
*
* Creates a {@link BeanDefinitionRegistryPostProcessor} that detects if a bean named
* HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME is defined. If so, it moves the
* AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME to another bean name
* and then adds a {@link CompositeFilter} that contains
* {@link HandlerMappingIntrospector#createCacheFilter()} and the original
* FilterChainProxy under the original Bean name.
* @return
*/
@Bean
@Deprecated
static BeanDefinitionRegistryPostProcessor springSecurityHandlerMappingIntrospectorBeanDefinitionRegistryPostProcessor() {
return new BeanDefinitionRegistryPostProcessor() {
@Override
@ -144,12 +136,15 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
}
String hmiRequestTransformerBeanName = HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + "RequestTransformer";
if (!registry.containsBeanDefinition(hmiRequestTransformerBeanName)) {
BeanDefinition hmiRequestTransformer = BeanDefinitionBuilder
.rootBeanDefinition(HandlerMappingIntrospectorRequestTransformer.class)
.addConstructorArgReference(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)
.getBeanDefinition();
registry.registerBeanDefinition(hmiRequestTransformerBeanName, hmiRequestTransformer);
if (!registry.containsBeanDefinition(PATH_PATTERN_REQUEST_TRANSFORMER_BEAN_NAME)
&& !registry.containsBeanDefinition(hmiRequestTransformerBeanName)) {
if (!registry.containsBeanDefinition(hmiRequestTransformerBeanName)) {
BeanDefinition hmiRequestTransformer = BeanDefinitionBuilder
.rootBeanDefinition(HandlerMappingIntrospectorRequestTransformer.class)
.addConstructorArgReference(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)
.getBeanDefinition();
registry.registerBeanDefinition(hmiRequestTransformerBeanName, hmiRequestTransformer);
}
}
BeanDefinition filterChainProxy = registry
@ -178,7 +173,11 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
/**
* {@link FactoryBean} to defer creation of
* {@link HandlerMappingIntrospector#createCacheFilter()}
*
* @deprecated see {@link WebSecurityConfiguration} for
* {@link org.springframework.web.util.pattern.PathPattern} replacement
*/
@Deprecated
static class HandlerMappingIntrospectorCacheFilterFactoryBean
implements ApplicationContextAware, FactoryBean<Filter> {
@ -207,7 +206,11 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex
* Extends {@link FilterChainProxy} to provide as much passivity as possible but
* delegates to {@link CompositeFilter} for
* {@link #doFilter(ServletRequest, ServletResponse, FilterChain)}.
*
* @deprecated see {@link WebSecurityConfiguration} for
* {@link org.springframework.web.util.pattern.PathPattern} replacement
*/
@Deprecated
static class CompositeFilterChainProxy extends FilterChainProxy {
/**

View File

@ -16,16 +16,29 @@
package org.springframework.security.config.annotation.web.configuration;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
@ -45,11 +58,18 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor;
import org.springframework.security.context.DelegatingApplicationListener;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
import org.springframework.security.web.debug.DebugFilter;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.RequestRejectedHandler;
import org.springframework.web.filter.CompositeFilter;
import org.springframework.web.filter.ServletRequestPathFilter;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
/**
* Uses a {@link WebSecurity} to create the {@link FilterChainProxy} that performs the web
@ -186,6 +206,56 @@ public class WebSecurityConfiguration implements ImportAware {
}
}
/**
* Used to ensure Spring MVC request matching is cached.
*
* Creates a {@link BeanDefinitionRegistryPostProcessor} that detects if a bean named
* HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME is defined. If so, it moves the
* AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME to another bean name
* and then adds a {@link CompositeFilter} that contains
* {@link HandlerMappingIntrospector#createCacheFilter()} and the original
* FilterChainProxy under the original Bean name.
* @return
*/
@Bean
static BeanDefinitionRegistryPostProcessor springSecurityPathPatternParserBeanDefinitionRegistryPostProcessor() {
return new BeanDefinitionRegistryPostProcessor() {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
BeanDefinition filterChainProxy = registry
.getBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
if (filterChainProxy.getResolvableType().isAssignableFrom(CompositeFilterChainProxy.class)) {
return;
}
BeanDefinitionBuilder pppCacheFilterBldr = BeanDefinitionBuilder
.rootBeanDefinition(ServletRequestPathFilter.class)
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
ManagedList<BeanMetadataElement> filters = new ManagedList<>();
filters.add(pppCacheFilterBldr.getBeanDefinition());
filters.add(filterChainProxy);
BeanDefinitionBuilder compositeSpringSecurityFilterChainBldr = BeanDefinitionBuilder
.rootBeanDefinition(CompositeFilterChainProxy.class)
.addConstructorArgValue(filters);
if (filterChainProxy.getResolvableType().isInstance(DebugFilter.class)) {
compositeSpringSecurityFilterChainBldr = BeanDefinitionBuilder.rootBeanDefinition(DebugFilter.class)
.addConstructorArgValue(compositeSpringSecurityFilterChainBldr.getBeanDefinition());
}
registry.removeBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
registry.registerBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME,
compositeSpringSecurityFilterChainBldr.getBeanDefinition());
}
};
}
/**
* A custom version of the Spring provided AnnotationAwareOrderComparator that uses
* {@link AnnotationUtils#findAnnotation(Class, Class)} to look on super class
@ -219,4 +289,126 @@ public class WebSecurityConfiguration implements ImportAware {
}
/**
* Extends {@link FilterChainProxy} to provide as much passivity as possible but
* delegates to {@link CompositeFilter} for
* {@link #doFilter(ServletRequest, ServletResponse, FilterChain)}.
*/
static class CompositeFilterChainProxy extends FilterChainProxy {
/**
* Used for {@link #doFilter(ServletRequest, ServletResponse, FilterChain)}
*/
private final Filter doFilterDelegate;
private final FilterChainProxy springSecurityFilterChain;
/**
* Creates a new instance
* @param filters the Filters to delegate to. One of which must be
* FilterChainProxy.
*/
CompositeFilterChainProxy(List<? extends Filter> filters) {
this.doFilterDelegate = createDoFilterDelegate(filters);
this.springSecurityFilterChain = findFilterChainProxy(filters);
}
CompositeFilterChainProxy(Filter delegate, FilterChainProxy filterChain) {
this.doFilterDelegate = delegate;
this.springSecurityFilterChain = filterChain;
}
@Override
public void afterPropertiesSet() {
this.springSecurityFilterChain.afterPropertiesSet();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
this.doFilterDelegate.doFilter(request, response, chain);
}
@Override
public List<Filter> getFilters(String url) {
return this.springSecurityFilterChain.getFilters(url);
}
@Override
public List<SecurityFilterChain> getFilterChains() {
return this.springSecurityFilterChain.getFilterChains();
}
@Override
public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy securityContextHolderStrategy) {
this.springSecurityFilterChain.setSecurityContextHolderStrategy(securityContextHolderStrategy);
}
@Override
public void setFilterChainValidator(FilterChainValidator filterChainValidator) {
this.springSecurityFilterChain.setFilterChainValidator(filterChainValidator);
}
@Override
public void setFilterChainDecorator(FilterChainDecorator filterChainDecorator) {
this.springSecurityFilterChain.setFilterChainDecorator(filterChainDecorator);
}
@Override
public void setFirewall(HttpFirewall firewall) {
this.springSecurityFilterChain.setFirewall(firewall);
}
@Override
public void setRequestRejectedHandler(RequestRejectedHandler requestRejectedHandler) {
this.springSecurityFilterChain.setRequestRejectedHandler(requestRejectedHandler);
}
/**
* Used through reflection by Spring Security's Test support to lookup the
* FilterChainProxy Filters for a specific HttpServletRequest.
* @param request
* @return
*/
private List<? extends Filter> getFilters(HttpServletRequest request) {
List<SecurityFilterChain> filterChains = this.springSecurityFilterChain.getFilterChains();
for (SecurityFilterChain chain : filterChains) {
if (chain.matches(request)) {
return chain.getFilters();
}
}
return null;
}
/**
* Creates the Filter to delegate to for doFilter
* @param filters the Filters to delegate to.
* @return the Filter for doFilter
*/
private static Filter createDoFilterDelegate(List<? extends Filter> filters) {
CompositeFilter delegate = new CompositeFilter();
delegate.setFilters(filters);
return delegate;
}
/**
* Find the FilterChainProxy in a List of Filter
* @param filters
* @return non-null FilterChainProxy
* @throws IllegalStateException if the FilterChainProxy cannot be found
*/
private static FilterChainProxy findFilterChainProxy(List<? extends Filter> filters) {
for (Filter filter : filters) {
if (filter instanceof FilterChainProxy fcp) {
return fcp;
}
if (filter instanceof DebugFilter debugFilter) {
return new CompositeFilterChainProxy(debugFilter, debugFilter.getFilterChainProxy());
}
}
throw new IllegalStateException("Couldn't find FilterChainProxy in " + filters);
}
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright 2002-2023 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.aot.hint;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
/**
* Runtime hints for
* {@link org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration}
*
* @author Marcus da Coregio
*/
class WebSecurityConfigurationRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.reflection()
.registerType(TypeReference
.of("org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$CompositeFilterChainProxy"),
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
}
}

View File

@ -51,6 +51,7 @@ import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.HandlerMappingIntrospectorRequestTransformer;
import org.springframework.security.web.access.PathPatternRequestTransformer;
import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl;
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
import org.springframework.security.web.access.channel.InsecureChannelProcessor;
@ -974,10 +975,17 @@ class HttpConfigurationBuilder {
@Override
public AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer getObject()
throws Exception {
AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer requestTransformer = this.applicationContext
.getBeanProvider(
AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer.class)
.getIfUnique();
if (requestTransformer != null) {
return requestTransformer;
}
HandlerMappingIntrospector hmi = this.applicationContext.getBeanProvider(HandlerMappingIntrospector.class)
.getIfAvailable();
return (hmi != null) ? new HandlerMappingIntrospectorRequestTransformer(hmi)
: AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer.IDENTITY;
: new PathPatternRequestTransformer();
}
@Override

View File

@ -57,9 +57,12 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.PathPatternRequestTransformer;
import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.util.ClassUtils;
@ -69,8 +72,12 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ -320,6 +327,27 @@ public class WebSecurityConfigurationTests {
assertThat(privilegeEvaluator.isAllowed("/ignoring1/child", null)).isTrue();
}
@Test
public void loadConfigWhenUsePathPatternThenEvaluates() {
this.spring.register(UsePathPatternConfig.class).autowire();
WebInvocationPrivilegeEvaluator privilegeEvaluator = this.spring.getContext()
.getBean(WebInvocationPrivilegeEvaluator.class);
assertUserPermissions(privilegeEvaluator);
assertAdminPermissions(privilegeEvaluator);
assertAnotherUserPermission(privilegeEvaluator);
// null authentication
assertThat(privilegeEvaluator.isAllowed("/user", null)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/admin", null)).isFalse();
assertThat(privilegeEvaluator.isAllowed("/another", null)).isTrue();
assertThat(privilegeEvaluator.isAllowed("/ignoring1", null)).isTrue();
assertThat(privilegeEvaluator.isAllowed("/ignoring1/child", null)).isTrue();
AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer requestTransformer = this.spring
.getContext()
.getBean(AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer.class);
verify(requestTransformer, atLeastOnce()).transform(any());
}
@Test
public void loadConfigWhenTwoSecurityFilterChainsPresentAndSecondWithAnyRequestThenException() {
assertThatExceptionOfType(BeanCreationException.class)
@ -862,6 +890,53 @@ public class WebSecurityConfigurationTests {
}
@Configuration
@EnableWebSecurity
@EnableWebMvc
@Import(AuthenticationTestConfiguration.class)
static class UsePathPatternConfig {
@Bean
AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer pathPatternRequestTransformer() {
return spy(new PathPatternRequestTransformer());
}
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring().requestMatchers("/ignoring1/**");
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain notAuthorized(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers((requests) -> requests.requestMatchers(PathPatternRequestMatcher.withDefaults().matcher("/user")))
.authorizeHttpRequests((requests) -> requests.anyRequest().hasRole("USER"));
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
public SecurityFilterChain admin(HttpSecurity http) throws Exception {
// @formatter:off
http
.securityMatchers((requests) -> requests.requestMatchers(PathPatternRequestMatcher.withDefaults().matcher("/admin")))
.authorizeHttpRequests((requests) -> requests.anyRequest().hasRole("ADMIN"));
// @formatter:on
return http.build();
}
@Bean
@Order(Ordered.LOWEST_PRECEDENCE)
public SecurityFilterChain permitAll(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((requests) -> requests.anyRequest().permitAll());
return http.build();
}
}
@Configuration
@EnableWebSecurity
@EnableWebMvc

View File

@ -339,6 +339,54 @@ casAuthentication.setProxyReceptorUrl(PathPatternRequestMatcher.withDefaults().m
----
======
=== Migrate your WebInvocationPrivilegeEvaluator
If you are using Spring Security's JSP Taglibs or are using `WebInvocationPrivilegeEvaluator` directly, be aware of the following changes:
1. `RequestMatcherWebInvocationPrivilegeEvaluator` is deprecated in favor of `AuthorizationManagerWebInvocationPrivilegeEvaluator`
2. `HandlerMappingIntrospectorRequestTransformer` is deprecated in favor of `PathPatternRequestTransformer`
If you are not constructing these directly, you can opt-in to both changes in advance by publishing a `PathPatternRequestTransformer` like so:
[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Bean
HttpServletRequestTransformer pathPatternRequestTransformer() {
return new PathPatternRequestTransformer();
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Bean
fun pathPatternRequestTransformer(): HttpServletRequestTransformer {
return PathPatternRequestTransformer()
}
----
Xml::
+
[source,xml,role="secondary"]
----
<b:bean class="org.springframework.security.web.access.PathPatternRequestTransformer"/>
----
======
Spring Security will take this as a signal to use the new implementations.
[[NOTE]]
----
One difference you may notice is that `AuthorizationManagerWebPrivilegeInvocationEvaluator` allows the authentication to be `null` if the authorization rule is `permitAll`.
Test your endpoints that `permitAll` in case JSP requests using this same require should not, in fact, be permitted.
----
== 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.

View File

@ -46,7 +46,6 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.filter.GenericFilterBean;
import org.springframework.web.filter.ServletRequestPathFilter;
/**
* Delegates {@code Filter} requests to a list of Spring-managed filter beans. As of
@ -163,8 +162,6 @@ public class FilterChainProxy extends GenericFilterBean {
private FilterChainDecorator filterChainDecorator = new VirtualFilterChainDecorator();
private Filter springWebFilter = new ServletRequestPathFilter();
public FilterChainProxy() {
}
@ -213,29 +210,27 @@ public class FilterChainProxy extends GenericFilterBean {
throws IOException, ServletException {
FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
this.springWebFilter.doFilter(firewallRequest, firewallResponse, (r, s) -> {
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
return;
List<Filter> filters = getFilters(firewallRequest);
if (filters == null || filters.isEmpty()) {
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
}
firewallRequest.reset();
this.filterChainDecorator.decorate(chain).doFilter(firewallRequest, firewallResponse);
return;
}
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
}
FilterChain reset = (req, res) -> {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
}
FilterChain reset = (req, res) -> {
if (logger.isDebugEnabled()) {
logger.debug(LogMessage.of(() -> "Secured " + requestLine(firewallRequest)));
}
// Deactivate path stripping as we exit the security filter chain
firewallRequest.reset();
chain.doFilter(req, res);
};
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
});
// Deactivate path stripping as we exit the security filter chain
firewallRequest.reset();
chain.doFilter(req, res);
};
this.filterChainDecorator.decorate(reset, filters).doFilter(firewallRequest, firewallResponse);
}
/**

View File

@ -35,7 +35,9 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
* default throw {@link UnsupportedOperationException}.
*
* @author Rob Winch
* @deprecated please use {@link PathPatternRequestTransformer} instead
*/
@Deprecated(forRemoval = true)
public class HandlerMappingIntrospectorRequestTransformer
implements AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer {

View File

@ -0,0 +1,70 @@
/*
* 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.web.access;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.web.util.ServletRequestPathUtils;
/**
* Prepares the privilege evaluator's request for {@link PathPatternRequestMatcher}
* authorization rules.
*
* @author Josh Cummings
* @since 6.5
*/
public final class PathPatternRequestTransformer
implements AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer {
@Override
public HttpServletRequest transform(HttpServletRequest request) {
HttpServletRequest wrapped = new AttributesSupportingHttpServletRequest(request);
ServletRequestPathUtils.parseAndCache(wrapped);
return wrapped;
}
private static final class AttributesSupportingHttpServletRequest extends HttpServletRequestWrapper {
private final Map<String, Object> attributes = new HashMap<>();
AttributesSupportingHttpServletRequest(HttpServletRequest request) {
super(request);
}
@Override
public Object getAttribute(String name) {
return this.attributes.get(name);
}
@Override
public void setAttribute(String name, Object value) {
this.attributes.put(name, value);
}
@Override
public void removeAttribute(String name) {
this.attributes.remove(name);
}
}
}

View File

@ -17,20 +17,17 @@
package org.springframework.security.web.access;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.servlet.ServletContext;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.util.Assert;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.util.ServletRequestPathUtils;
/**
* A {@link WebInvocationPrivilegeEvaluator} which delegates to a list of
@ -39,7 +36,11 @@ import org.springframework.web.util.ServletRequestPathUtils;
*
* @author Marcus Da Coregio
* @since 5.5.5
* @deprecated please use {@link AuthorizationManagerWebInvocationPrivilegeEvaluator} and
* adapt any delegate {@link WebInvocationPrivilegeEvaluator}s into
* {@link AuthorizationManager}s
*/
@Deprecated
public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator
implements WebInvocationPrivilegeEvaluator, ServletContextAware {
@ -120,8 +121,7 @@ public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator
private List<WebInvocationPrivilegeEvaluator> getDelegate(String contextPath, String uri, String method) {
FilterInvocation filterInvocation = new FilterInvocation(contextPath, uri, method, this.servletContext);
HttpServletRequest request = new AttributesSupportingHttpServletRequest(filterInvocation.getHttpRequest());
ServletRequestPathUtils.parseAndCache(request);
HttpServletRequest request = filterInvocation.getHttpRequest();
for (RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> delegate : this.delegates) {
if (delegate.getRequestMatcher().matches(request)) {
return delegate.getEntry();
@ -135,29 +135,4 @@ public final class RequestMatcherDelegatingWebInvocationPrivilegeEvaluator
this.servletContext = servletContext;
}
private static final class AttributesSupportingHttpServletRequest extends HttpServletRequestWrapper {
private final Map<String, Object> attributes = new HashMap<>();
AttributesSupportingHttpServletRequest(HttpServletRequest request) {
super(request);
}
@Override
public Object getAttribute(String name) {
return this.attributes.get(name);
}
@Override
public void setAttribute(String name, Object value) {
this.attributes.put(name, value);
}
@Override
public void removeAttribute(String name) {
this.attributes.remove(name);
}
}
}

View File

@ -31,6 +31,8 @@ import org.springframework.security.authentication.TestAuthentication;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer;
import org.springframework.security.web.access.intercept.RequestMatcherDelegatingAuthorizationManager;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -111,4 +113,19 @@ class AuthorizationManagerWebInvocationPrivilegeEvaluatorTests {
verify(this.authorizationManager).check(any(), eq(request));
}
// gh-16771
@Test
void isAllowedWhenInvokesDelegateThenCachesRequestPath() {
RequestMatcherDelegatingAuthorizationManager authorizationManager = RequestMatcherDelegatingAuthorizationManager
.builder()
.add(PathPatternRequestMatcher.withDefaults().matcher("/test/**"),
(authentication, context) -> this.authorizationManager.check(authentication, context.getRequest()))
.build();
AuthorizationManagerWebInvocationPrivilegeEvaluator privilegeEvaluator = new AuthorizationManagerWebInvocationPrivilegeEvaluator(
authorizationManager);
privilegeEvaluator.setRequestTransformer(new PathPatternRequestTransformer());
privilegeEvaluator.isAllowed("/test", TestAuthentication.authenticatedUser());
verify(this.authorizationManager).check(any(), any());
}
}

View File

@ -22,16 +22,12 @@ import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.web.util.ServletRequestPathUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -178,19 +174,6 @@ class RequestMatcherDelegatingWebInvocationPrivilegeEvaluatorTests {
.withMessageContaining("requestMatcher cannot be null");
}
// gh-16771
@Test
void isAllowedWhenInvokesDelegateThenCachesRequestPath() {
PathPatternRequestMatcher path = PathPatternRequestMatcher.withDefaults().matcher("/path/**");
PathPatternRequestMatcher any = PathPatternRequestMatcher.withDefaults().matcher("/**");
WebInvocationPrivilegeEvaluator delegating = evaluator(deny(path), deny(any));
try (MockedStatic<ServletRequestPathUtils> utils = Mockito.mockStatic(ServletRequestPathUtils.class,
Mockito.CALLS_REAL_METHODS)) {
delegating.isAllowed("/uri", null);
utils.verify(() -> ServletRequestPathUtils.parseAndCache(any()), times(1));
}
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private RequestMatcherDelegatingWebInvocationPrivilegeEvaluator evaluator(RequestMatcherEntry... entries) {
return new RequestMatcherDelegatingWebInvocationPrivilegeEvaluator(List.of(entries));