Skip to content

Commit 70dfb3d

Browse files
committed
Add HandlerMappingIntrospector Caching
Closes gh-14128
1 parent 1bb5fe4 commit 70dfb3d

File tree

10 files changed

+807
-7
lines changed

10 files changed

+807
-7
lines changed

config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java

+9
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.springframework.security.web.ObservationFilterChainDecorator;
5050
import org.springframework.security.web.SecurityFilterChain;
5151
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
52+
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer;
5253
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
5354
import org.springframework.security.web.access.RequestMatcherDelegatingWebInvocationPrivilegeEvaluator;
5455
import org.springframework.security.web.access.WebInvocationPrivilegeEvaluator;
@@ -108,6 +109,8 @@ public final class WebSecurity extends AbstractConfiguredSecurityBuilder<Filter,
108109

109110
private ObservationRegistry observationRegistry = ObservationRegistry.NOOP;
110111

112+
private HttpServletRequestTransformer privilegeEvaluatorRequestTransformer;
113+
111114
private DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
112115

113116
private SecurityExpressionHandler<FilterInvocation> expressionHandler = this.defaultWebSecurityExpressionHandler;
@@ -350,6 +353,9 @@ private RequestMatcherEntry<List<WebInvocationPrivilegeEvaluator>> getRequestMat
350353
AuthorizationManagerWebInvocationPrivilegeEvaluator evaluator = new AuthorizationManagerWebInvocationPrivilegeEvaluator(
351354
authorizationManager);
352355
evaluator.setServletContext(this.servletContext);
356+
if (this.privilegeEvaluatorRequestTransformer != null) {
357+
evaluator.setRequestTransformer(this.privilegeEvaluatorRequestTransformer);
358+
}
353359
privilegeEvaluators.add(evaluator);
354360
}
355361
}
@@ -386,6 +392,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
386392
}
387393
catch (NoSuchBeanDefinitionException ex) {
388394
}
395+
Class<HttpServletRequestTransformer> requestTransformerClass = HttpServletRequestTransformer.class;
396+
this.privilegeEvaluatorRequestTransformer = applicationContext.getBeanProvider(requestTransformerClass)
397+
.getIfUnique();
389398
}
390399

391400
@Override

config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java

+162
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,39 @@
1818

1919
import java.util.List;
2020

21+
import jakarta.servlet.Filter;
22+
import jakarta.servlet.http.HttpServletRequest;
23+
24+
import org.springframework.beans.BeanMetadataElement;
2125
import org.springframework.beans.BeansException;
26+
import org.springframework.beans.factory.FactoryBean;
27+
import org.springframework.beans.factory.config.BeanDefinition;
28+
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
29+
import org.springframework.beans.factory.config.RuntimeBeanReference;
30+
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
31+
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
32+
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
33+
import org.springframework.beans.factory.support.ManagedList;
2234
import org.springframework.context.ApplicationContext;
2335
import org.springframework.context.ApplicationContextAware;
2436
import org.springframework.context.annotation.Bean;
2537
import org.springframework.context.expression.BeanFactoryResolver;
2638
import org.springframework.expression.BeanResolver;
2739
import org.springframework.security.core.context.SecurityContextHolder;
2840
import org.springframework.security.core.context.SecurityContextHolderStrategy;
41+
import org.springframework.security.web.FilterChainProxy;
42+
import org.springframework.security.web.SecurityFilterChain;
43+
import org.springframework.security.web.access.HandlerMappingIntrospectorRequestTransformer;
44+
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
2945
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
3046
import org.springframework.security.web.method.annotation.CsrfTokenArgumentResolver;
3147
import org.springframework.security.web.method.annotation.CurrentSecurityContextArgumentResolver;
3248
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
49+
import org.springframework.web.filter.CompositeFilter;
3350
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
3451
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
3552
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
53+
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
3654
import org.springframework.web.servlet.support.RequestDataValueProcessor;
3755

3856
/**
@@ -50,6 +68,8 @@
5068
*/
5169
class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContextAware {
5270

71+
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
72+
5373
private BeanResolver beanResolver;
5474

5575
private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder
@@ -84,4 +104,146 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
84104
}
85105
}
86106

107+
/**
108+
* Used to ensure Spring MVC request matching is cached.
109+
*
110+
* Creates a {@link BeanDefinitionRegistryPostProcessor} that detects if a bean named
111+
* HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME is defined. If so, it moves the
112+
* AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME to another bean name
113+
* and then adds a {@link CompositeFilter} that contains
114+
* {@link HandlerMappingIntrospector#createCacheFilter()} and the original
115+
* FilterChainProxy under the original Bean name.
116+
* @return
117+
*/
118+
@Bean
119+
static BeanDefinitionRegistryPostProcessor springSecurityHandlerMappingIntrospectorBeanDefinitionRegistryPostProcessor() {
120+
return new BeanDefinitionRegistryPostProcessor() {
121+
@Override
122+
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
123+
}
124+
125+
@Override
126+
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
127+
if (!registry.containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
128+
return;
129+
}
130+
131+
BeanDefinition hmiRequestTransformer = BeanDefinitionBuilder
132+
.rootBeanDefinition(HandlerMappingIntrospectorRequestTransformer.class)
133+
.addConstructorArgReference(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)
134+
.getBeanDefinition();
135+
registry.registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + "RequestTransformer",
136+
hmiRequestTransformer);
137+
138+
String filterChainProxyBeanName = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME
139+
+ "Proxy";
140+
BeanDefinition filterChainProxy = registry
141+
.getBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
142+
registry.registerBeanDefinition(filterChainProxyBeanName, filterChainProxy);
143+
144+
BeanDefinitionBuilder hmiCacheFilterBldr = BeanDefinitionBuilder
145+
.rootBeanDefinition(HandlerMappingIntrospectorCachFilterFactoryBean.class)
146+
.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
147+
148+
ManagedList<BeanMetadataElement> filters = new ManagedList<>();
149+
filters.add(hmiCacheFilterBldr.getBeanDefinition());
150+
filters.add(new RuntimeBeanReference(filterChainProxyBeanName));
151+
BeanDefinitionBuilder compositeSpringSecurityFilterChainBldr = BeanDefinitionBuilder
152+
.rootBeanDefinition(SpringSecurityFilterCompositeFilter.class)
153+
.addConstructorArgValue(filters);
154+
155+
registry.removeBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME);
156+
registry.registerBeanDefinition(AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME,
157+
compositeSpringSecurityFilterChainBldr.getBeanDefinition());
158+
}
159+
};
160+
}
161+
162+
/**
163+
* {@link FactoryBean} to defer creation of
164+
* {@link HandlerMappingIntrospector#createCacheFilter()}
165+
*/
166+
static class HandlerMappingIntrospectorCachFilterFactoryBean
167+
implements ApplicationContextAware, FactoryBean<Filter> {
168+
169+
private ApplicationContext applicationContext;
170+
171+
@Override
172+
public void setApplicationContext(ApplicationContext applicationContext) {
173+
this.applicationContext = applicationContext;
174+
}
175+
176+
@Override
177+
public Filter getObject() throws Exception {
178+
HandlerMappingIntrospector handlerMappingIntrospector = this.applicationContext
179+
.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class);
180+
return handlerMappingIntrospector.createCacheFilter();
181+
}
182+
183+
@Override
184+
public Class<?> getObjectType() {
185+
return Filter.class;
186+
}
187+
188+
}
189+
190+
/**
191+
* Extension to {@link CompositeFilter} to expose private methods used by Spring
192+
* Security's test support
193+
*/
194+
static class SpringSecurityFilterCompositeFilter extends CompositeFilter {
195+
196+
private FilterChainProxy springSecurityFilterChain;
197+
198+
SpringSecurityFilterCompositeFilter(List<? extends Filter> filters) {
199+
setFilters(filters); // for the parent
200+
}
201+
202+
@Override
203+
public void setFilters(List<? extends Filter> filters) {
204+
super.setFilters(filters);
205+
this.springSecurityFilterChain = findFilterChainProxy(filters);
206+
}
207+
208+
/**
209+
* Used through reflection by Spring Security's Test support to lookup the
210+
* FilterChainProxy Filters for a specific HttpServletRequest.
211+
* @param request
212+
* @return
213+
*/
214+
private List<? extends Filter> getFilters(HttpServletRequest request) {
215+
List<SecurityFilterChain> filterChains = getFilterChainProxy().getFilterChains();
216+
for (SecurityFilterChain chain : filterChains) {
217+
if (chain.matches(request)) {
218+
return chain.getFilters();
219+
}
220+
}
221+
return null;
222+
}
223+
224+
/**
225+
* Used by Spring Security's Test support to find the FilterChainProxy
226+
* @return
227+
*/
228+
private FilterChainProxy getFilterChainProxy() {
229+
return this.springSecurityFilterChain;
230+
}
231+
232+
/**
233+
* Find the FilterChainProxy in a List of Filter
234+
* @param filters
235+
* @return non-null FilterChainProxy
236+
* @throws IllegalStateException if the FilterChainProxy cannot be found
237+
*/
238+
private static FilterChainProxy findFilterChainProxy(List<? extends Filter> filters) {
239+
for (Filter filter : filters) {
240+
if (filter instanceof FilterChainProxy fcp) {
241+
return fcp;
242+
}
243+
}
244+
throw new IllegalStateException("Couldn't find FilterChainProxy in " + filters);
245+
}
246+
247+
}
248+
87249
}

config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

+44-3
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.w3c.dom.Element;
2525

2626
import org.springframework.beans.BeanMetadataElement;
27+
import org.springframework.beans.BeansException;
2728
import org.springframework.beans.factory.FactoryBean;
2829
import org.springframework.beans.factory.config.BeanDefinition;
2930
import org.springframework.beans.factory.config.BeanReference;
@@ -36,6 +37,8 @@
3637
import org.springframework.beans.factory.support.ManagedMap;
3738
import org.springframework.beans.factory.support.RootBeanDefinition;
3839
import org.springframework.beans.factory.xml.ParserContext;
40+
import org.springframework.context.ApplicationContext;
41+
import org.springframework.context.ApplicationContextAware;
3942
import org.springframework.security.access.vote.AffirmativeBased;
4043
import org.springframework.security.access.vote.AuthenticatedVoter;
4144
import org.springframework.security.access.vote.RoleVoter;
@@ -46,6 +49,7 @@
4649
import org.springframework.security.core.session.SessionRegistryImpl;
4750
import org.springframework.security.web.access.AuthorizationManagerWebInvocationPrivilegeEvaluator;
4851
import org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator;
52+
import org.springframework.security.web.access.HandlerMappingIntrospectorRequestTransformer;
4953
import org.springframework.security.web.access.channel.ChannelDecisionManagerImpl;
5054
import org.springframework.security.web.access.channel.ChannelProcessingFilter;
5155
import org.springframework.security.web.access.channel.InsecureChannelProcessor;
@@ -82,6 +86,7 @@
8286
import org.springframework.util.ClassUtils;
8387
import org.springframework.util.StringUtils;
8488
import org.springframework.util.xml.DomUtils;
89+
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
8590

8691
/**
8792
* Stateful class which helps HttpSecurityBDP to create the configuration for the
@@ -93,6 +98,11 @@
9398
*/
9499
class HttpConfigurationBuilder {
95100

101+
private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector";
102+
103+
private static final boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR,
104+
HttpConfigurationBuilder.class.getClassLoader());
105+
96106
private static final String ATT_CREATE_SESSION = "create-session";
97107

98108
private static final String ATT_SESSION_FIXATION_PROTECTION = "session-fixation-protection";
@@ -744,10 +754,14 @@ private void createAuthorizationFilter() {
744754
// Create and register a AuthorizationManagerWebInvocationPrivilegeEvaluator for
745755
// use with
746756
// taglibs etc.
747-
BeanDefinition wipe = BeanDefinitionBuilder
757+
BeanDefinitionBuilder wipeBldr = BeanDefinitionBuilder
748758
.rootBeanDefinition(AuthorizationManagerWebInvocationPrivilegeEvaluator.class)
749-
.addConstructorArgReference(authorizationFilterParser.getAuthorizationManagerRef())
750-
.getBeanDefinition();
759+
.addConstructorArgReference(authorizationFilterParser.getAuthorizationManagerRef());
760+
if (mvcPresent) {
761+
wipeBldr.addPropertyValue("requestTransformer",
762+
new RootBeanDefinition(HandlerMappingIntrospectorRequestTransformerFactoryBean.class));
763+
}
764+
BeanDefinition wipe = wipeBldr.getBeanDefinition();
751765
this.pc.registerBeanComponent(
752766
new BeanComponentDefinition(wipe, this.pc.getReaderContext().generateBeanName(wipe)));
753767
this.fsi = new RuntimeBeanReference(fsiId);
@@ -913,6 +927,33 @@ private static BeanMetadataElement getObservationRegistry(Element httpElmt) {
913927
return BeanDefinitionBuilder.rootBeanDefinition(ObservationRegistryFactory.class).getBeanDefinition();
914928
}
915929

930+
static class HandlerMappingIntrospectorRequestTransformerFactoryBean
931+
implements FactoryBean<AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer>,
932+
ApplicationContextAware {
933+
934+
private ApplicationContext applicationContext;
935+
936+
@Override
937+
public AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer getObject()
938+
throws Exception {
939+
HandlerMappingIntrospector hmi = this.applicationContext.getBeanProvider(HandlerMappingIntrospector.class)
940+
.getIfAvailable();
941+
return (hmi != null) ? new HandlerMappingIntrospectorRequestTransformer(hmi)
942+
: AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer.IDENTITY;
943+
}
944+
945+
@Override
946+
public Class<?> getObjectType() {
947+
return AuthorizationManagerWebInvocationPrivilegeEvaluator.HttpServletRequestTransformer.class;
948+
}
949+
950+
@Override
951+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
952+
this.applicationContext = applicationContext;
953+
}
954+
955+
}
956+
916957
static class RoleVoterBeanFactory extends AbstractGrantedAuthorityDefaultsBeanFactory {
917958

918959
private RoleVoter voter = new RoleVoter();

config/src/test/java/org/springframework/security/config/annotation/web/builders/WebSecurityTests.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import io.micrometer.observation.ObservationRegistry;
2222
import io.micrometer.observation.ObservationTextPublisher;
23+
import jakarta.servlet.Filter;
2324
import jakarta.servlet.ServletException;
2425
import jakarta.servlet.http.HttpServletResponse;
2526
import org.junit.jupiter.api.AfterEach;
@@ -39,7 +40,6 @@
3940
import org.springframework.security.core.userdetails.PasswordEncodedUser;
4041
import org.springframework.security.core.userdetails.UserDetailsService;
4142
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
42-
import org.springframework.security.web.FilterChainProxy;
4343
import org.springframework.security.web.SecurityFilterChain;
4444
import org.springframework.security.web.firewall.HttpStatusRequestRejectedHandler;
4545
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
@@ -67,7 +67,7 @@ public class WebSecurityTests {
6767
MockFilterChain chain;
6868

6969
@Autowired
70-
FilterChainProxy springSecurityFilterChain;
70+
Filter springSecurityFilterChain;
7171

7272
@BeforeEach
7373
public void setup() {

0 commit comments

Comments
 (0)