From aceb9532bd8271b7e764d90d63aa132d551e66da Mon Sep 17 00:00:00 2001 From: Josh Cummings <3627351+jzheaux@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:43:21 -0700 Subject: [PATCH] Use PathPatternRequestMatcher by Default --- .../web/AbstractRequestMatcherRegistry.java | 48 +++++++++------ .../security/config/http/MatcherType.java | 59 ++++++++++++++++++- .../web/AuthorizeHttpRequestsDsl.kt | 21 +++++-- .../config/FilterChainProxyConfigTests.java | 4 +- .../AbstractRequestMatcherRegistryTests.java | 15 +++-- .../servlet/appendix/namespace/http.adoc | 2 +- .../ROOT/pages/servlet/integrations/mvc.adoc | 21 ++++--- etc/checkstyle/checkstyle-suppressions.xml | 2 + .../security/web/FilterInvocation.java | 10 +++- 9 files changed, 144 insertions(+), 38 deletions(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java index 2181da01aea..3719e22c88a 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistry.java @@ -43,6 +43,7 @@ import org.springframework.security.config.annotation.web.ServletRegistrationsSupport.RegistrationMapping; import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher; @@ -53,8 +54,8 @@ import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.web.util.pattern.PathPatternParser; /** * A base class for registering {@link RequestMatcher}'s. For example, it might allow for @@ -234,7 +235,7 @@ private boolean anyPathsDontStartWithLeadingSlash(String... patterns) { return false; } - private RequestMatcher resolve(AntPathRequestMatcher ant, MvcRequestMatcher mvc, ServletContext servletContext) { + private RequestMatcher resolve(AntPathRequestMatcher ant, RequestMatcher mvc, ServletContext servletContext) { ServletRegistrationsSupport registrations = new ServletRegistrationsSupport(servletContext); Collection mappings = registrations.mappings(); if (mappings.isEmpty()) { @@ -280,10 +281,10 @@ private static String computeErrorMessage(Collection - * If the {@link HandlerMappingIntrospector} is available in the classpath, maps to an - * {@link MvcRequestMatcher} that does not care which {@link HttpMethod} is used. This - * matcher will use the same rules that Spring MVC uses for matching. For example, - * often times a mapping of the path "/path" will match on "/path", "/path/", + * If the {@link HandlerMappingIntrospector} is available in the classpath, maps to a + * {@link PathPatternRequestMatcher} that does not care which {@link HttpMethod} is + * used. This matcher will use the same rules that Spring MVC uses for matching. For + * example, often times a mapping of the path "/path" will match on "/path", "/path/", * "/path.html", etc. If the {@link HandlerMappingIntrospector} is not available, maps * to an {@link AntPathRequestMatcher}. *

@@ -408,8 +409,26 @@ class DefaultRequestMatcherBuilder implements RequestMatcherBuilder { @Override public RequestMatcher pattern(HttpMethod method, String pattern) { + Assert.state(!AbstractRequestMatcherRegistry.this.anyRequestConfigured, + "Can't configure mvcMatchers after anyRequest"); AntPathRequestMatcher ant = new AntPathRequestMatcher(pattern, (method != null) ? method.name() : null); - MvcRequestMatcher mvc = createMvcMatchers(method, pattern).get(0); + RequestMatcher mvc; + if (!AbstractRequestMatcherRegistry.this.context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { + throw new NoSuchBeanDefinitionException("A Bean named " + HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME + + " of type " + HandlerMappingIntrospector.class.getName() + + " is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."); + } + HandlerMappingIntrospector introspector = AbstractRequestMatcherRegistry.this.context + .getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class); + if (introspector.allHandlerMappingsUsePathPatternParser()) { + PathPatternParser pathPatternParser = AbstractRequestMatcherRegistry.this.context + .getBeanProvider(PathPatternParser.class) + .getIfUnique(() -> PathPatternParser.defaultInstance); + mvc = PathPatternRequestMatcher.withPathPatternParser(pathPatternParser).pattern(method, pattern); + } + else { + mvc = createMvcMatchers(method, pattern).get(0); + } return new DeferredRequestMatcher((c) -> resolve(ant, mvc, c), mvc, ant); } @@ -466,13 +485,8 @@ public boolean matches(HttpServletRequest request) { ServletRegistration registration = request.getServletContext().getServletRegistration(name); Assert.notNull(registration, () -> computeErrorMessage(request.getServletContext().getServletRegistrations().values())); - try { - Class clazz = Class.forName(registration.getClassName()); - return DispatcherServlet.class.isAssignableFrom(clazz); - } - catch (ClassNotFoundException ex) { - return false; - } + return new RegistrationMapping(registration, request.getHttpServletMapping().getPattern()) + .isDispatcherServlet(); } } @@ -481,15 +495,15 @@ static class DispatcherServletDelegatingRequestMatcher implements RequestMatcher private final AntPathRequestMatcher ant; - private final MvcRequestMatcher mvc; + private final RequestMatcher mvc; private final RequestMatcher dispatcherServlet; - DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc) { + DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, RequestMatcher mvc) { this(ant, mvc, new OrRequestMatcher(new MockMvcRequestMatcher(), new DispatcherServletRequestMatcher())); } - DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, MvcRequestMatcher mvc, + DispatcherServletDelegatingRequestMatcher(AntPathRequestMatcher ant, RequestMatcher mvc, RequestMatcher dispatcherServlet) { this.ant = ant; this.mvc = mvc; diff --git a/config/src/main/java/org/springframework/security/config/http/MatcherType.java b/config/src/main/java/org/springframework/security/config/http/MatcherType.java index 3755cb84e8e..1a6448d93a0 100644 --- a/config/src/main/java/org/springframework/security/config/http/MatcherType.java +++ b/config/src/main/java/org/springframework/security/config/http/MatcherType.java @@ -16,20 +16,25 @@ package org.springframework.security.config.http; +import jakarta.servlet.http.HttpServletRequest; import org.w3c.dom.Element; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.http.HttpMethod; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RegexRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.web.util.pattern.PathPatternParser; /** * Defines the {@link RequestMatcher} types supported by the namespace. @@ -40,7 +45,7 @@ public enum MatcherType { ant(AntPathRequestMatcher.class), regex(RegexRequestMatcher.class), ciRegex(RegexRequestMatcher.class), - mvc(MvcRequestMatcher.class); + mvc(MvcRequestMatcherFactoryBean.class); private static final String HANDLER_MAPPING_INTROSPECTOR = "org.springframework.web.servlet.handler.HandlerMappingIntrospector"; @@ -100,4 +105,56 @@ static MatcherType fromElementOrMvc(Element elt) { return MatcherType.fromElement(elt); } + private static class MvcRequestMatcherFactoryBean implements FactoryBean, RequestMatcher { + + private final HandlerMappingIntrospector introspector; + + private final String pattern; + + private PathPatternParser pathPatternParser = PathPatternParser.defaultInstance; + + private String servletPath; + + private HttpMethod method; + + public MvcRequestMatcherFactoryBean(HandlerMappingIntrospector introspector, String pattern) { + this.introspector = introspector; + this.pattern = pattern; + } + + @Override + public RequestMatcher getObject() { + if (this.introspector.allHandlerMappingsUsePathPatternParser()) { + return PathPatternRequestMatcher.withPathPatternParser(this.pathPatternParser) + .servletPath(this.servletPath) + .pattern(this.method, this.pattern); + } + return new MvcRequestMatcher.Builder(this.introspector).servletPath(this.servletPath) + .pattern(this.method, this.pattern); + } + + @Override + public Class getObjectType() { + return RequestMatcher.class; + } + + @Override + public boolean matches(HttpServletRequest request) { + return getObject().matches(request); + } + + public void setPathPatternParser(PathPatternParser pathPatternParser) { + this.pathPatternParser = pathPatternParser; + } + + public void setServletPath(String servletPath) { + this.servletPath = servletPath; + } + + public void setMethod(HttpMethod method) { + this.method = method; + } + + } + } diff --git a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt index 0133670a18f..ba90c2d8c47 100644 --- a/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt +++ b/config/src/main/kotlin/org/springframework/security/config/annotation/web/AuthorizeHttpRequestsDsl.kt @@ -32,10 +32,12 @@ import org.springframework.security.web.access.IpAddressAuthorizationManager import org.springframework.security.web.access.intercept.AuthorizationFilter import org.springframework.security.web.access.intercept.RequestAuthorizationContext import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher import org.springframework.security.web.util.matcher.AnyRequestMatcher import org.springframework.security.web.util.matcher.RequestMatcher import org.springframework.util.ClassUtils import org.springframework.web.servlet.handler.HandlerMappingIntrospector +import org.springframework.web.util.pattern.PathPatternParser import java.util.function.Supplier /** @@ -292,11 +294,20 @@ class AuthorizeHttpRequestsDsl : AbstractRequestMatcherDsl { PatternType.ANT -> requests.requestMatchers(rule.httpMethod, rule.pattern).access(rule.rule) PatternType.MVC -> { val introspector = requests.applicationContext.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector::class.java) - val mvcMatcher = MvcRequestMatcher.Builder(introspector) - .servletPath(rule.servletPath) - .pattern(rule.pattern) - mvcMatcher.setMethod(rule.httpMethod) - requests.requestMatchers(mvcMatcher).access(rule.rule) + if (introspector.allHandlerMappingsUsePathPatternParser()) { + val pathPatternParser: PathPatternParser = requests.applicationContext.getBeanProvider(PathPatternParser::class.java) + .getIfUnique({PathPatternParser.defaultInstance}) + val mvcMatcher = PathPatternRequestMatcher.withPathPatternParser(pathPatternParser) + .servletPath(rule.servletPath) + .pattern(rule.httpMethod, rule.pattern) + requests.requestMatchers(mvcMatcher).access(rule.rule) + } else { + val mvcMatcher = MvcRequestMatcher.Builder(introspector) + .servletPath(rule.servletPath) + .pattern(rule.pattern) + mvcMatcher.setMethod(rule.httpMethod) + requests.requestMatchers(mvcMatcher).access(rule.rule) + } } } } diff --git a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java index e2f81e3e17d..cdeec1f3994 100644 --- a/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/FilterChainProxyConfigTests.java @@ -39,6 +39,7 @@ import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.util.pattern.PathPattern; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -120,7 +121,8 @@ public void mixingPatternsAndPlaceholdersDoesntCauseOrderingIssues() { private String getPattern(SecurityFilterChain chain) { RequestMatcher requestMatcher = ((DefaultSecurityFilterChain) chain).getRequestMatcher(); - return (String) ReflectionTestUtils.getField(requestMatcher, "pattern"); + Object pattern = ReflectionTestUtils.getField(requestMatcher, "pattern"); + return (pattern instanceof PathPattern) ? pattern.toString() : (String) pattern; } private void checkPathAndFilterOrder(FilterChainProxy filterChainProxy) { diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java index 4a7ae59d07e..b0e8fda23d0 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/AbstractRequestMatcherRegistryTests.java @@ -21,6 +21,7 @@ import jakarta.servlet.DispatcherType; import jakarta.servlet.Servlet; +import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,6 +40,7 @@ import org.springframework.security.web.servlet.MockServletContext; import org.springframework.security.web.servlet.TestMockHttpServletMappings; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.servlet.util.matcher.PathPatternRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.DispatcherTypeRequestMatcher; import org.springframework.security.web.util.matcher.RegexRequestMatcher; @@ -49,10 +51,13 @@ import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.InstanceOfAssertFactories.type; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -91,11 +96,13 @@ public void setUp() { given(this.context.getServletContext()).willReturn(MockServletContext.mvc()); ObjectProvider requestMatcherFactories = new ObjectProvider<>() { @Override - public RequestMatcherBuilder getObject() throws BeansException { + public @NotNull RequestMatcherBuilder getObject() throws BeansException { return AbstractRequestMatcherRegistryTests.this.matcherRegistry.new DefaultRequestMatcherBuilder(); } }; given(this.context.getBeanProvider(RequestMatcherBuilder.class)).willReturn(requestMatcherFactories); + HandlerMappingIntrospector introspector = mock(HandlerMappingIntrospector.class); + given(this.context.getBean(any(), eq(HandlerMappingIntrospector.class))).willReturn(introspector); this.matcherRegistry.setApplicationContext(this.context); mockMvcIntrospector(true); } @@ -231,7 +238,7 @@ public void requestMatchersWhenNoDispatcherServletMockMvcThenMvcRequestMatcherTy assertThat(requestMatchers).hasSize(1); assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class)) .extracting((matcher) -> matcher.requestMatcher(request)) - .isInstanceOf(MvcRequestMatcher.class); + .isInstanceOf(PathPatternRequestMatcher.class); servletContext.addServlet("servletOne", Servlet.class).addMapping("/one"); servletContext.addServlet("servletTwo", Servlet.class).addMapping("/two"); requestMatchers = this.matcherRegistry.requestMatchers("/**"); @@ -239,7 +246,7 @@ public void requestMatchersWhenNoDispatcherServletMockMvcThenMvcRequestMatcherTy assertThat(requestMatchers).hasSize(1); assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class)) .extracting((matcher) -> matcher.requestMatcher(request)) - .isInstanceOf(MvcRequestMatcher.class); + .isInstanceOf(PathPatternRequestMatcher.class); servletContext.addServlet("servletOne", Servlet.class); servletContext.addServlet("servletTwo", Servlet.class); requestMatchers = this.matcherRegistry.requestMatchers("/**"); @@ -247,7 +254,7 @@ public void requestMatchersWhenNoDispatcherServletMockMvcThenMvcRequestMatcherTy assertThat(requestMatchers).hasSize(1); assertThat(requestMatchers.get(0)).asInstanceOf(type(DispatcherServletDelegatingRequestMatcher.class)) .extracting((matcher) -> matcher.requestMatcher(request)) - .isInstanceOf(MvcRequestMatcher.class); + .isInstanceOf(PathPatternRequestMatcher.class); } } diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc index d49c2f12db3..ef1e188ad68 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/http.adoc @@ -124,7 +124,7 @@ Corresponds to the `realmName` property on `BasicAuthenticationEntryPoint`. Defines the `RequestMatcher` strategy used in the `FilterChainProxy` and the beans created by the `intercept-url` to match incoming requests. Options are currently `mvc`, `ant`, `regex` and `ciRegex`, for Spring MVC, ant, regular-expression and case-insensitive regular-expression respectively. A separate instance is created for each <> element using its <>, <> and <> attributes. -Ant paths are matched using an `AntPathRequestMatcher`, regular expressions are matched using a `RegexRequestMatcher` and for Spring MVC path matching the `MvcRequestMatcher` is used. +Ant paths are matched using an `AntPathRequestMatcher`, regular expressions are matched using a `RegexRequestMatcher` and for Spring MVC path matching the `PathPatternRequestMatcher` is used. See the Javadoc for these classes for more details on exactly how the matching is performed. MVC is the default strategy if Spring MVC is present in the classpath, if not, Ant paths are used. diff --git a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc index 49314e8ba83..99b5aaa4577 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc @@ -22,13 +22,13 @@ This means that, if you use more advanced options, such as integrating with `Web ==== [[mvc-requestmatcher]] -== MvcRequestMatcher +== PathPatternRequestMatcher -Spring Security provides deep integration with how Spring MVC matches on URLs with `MvcRequestMatcher`. +Spring Security provides deep integration with how Spring MVC matches on URLs with `PahtPatternRequestMatcher`. This is helpful to ensure that your Security rules match the logic used to handle your requests. -To use `MvcRequestMatcher`, you must place the Spring Security Configuration in the same `ApplicationContext` as your `DispatcherServlet`. -This is necessary because Spring Security's `MvcRequestMatcher` expects a `HandlerMappingIntrospector` bean with the name of `mvcHandlerMappingIntrospector` to be registered by your Spring MVC configuration that is used to perform the matching. +To use `PathPatternRequestMatcher`, you may need to place the Spring Security Configuration in the same `ApplicationContext` as your `DispatcherServlet`. +This is necessary when using a custom `PathPatternParser` `@Bean`. In this case, both Spring MVC and Spring Security needs to be able to see this bean. For a `web.xml` file, this means that you should place your configuration in the `DispatcherServlet.xml`: @@ -196,18 +196,18 @@ Additionally, depending on our Spring MVC configuration, the `/admin` URL also m The problem is that our security rule protects only `/admin`. We could add additional rules for all the permutations of Spring MVC, but this would be quite verbose and tedious. -Fortunately, when using the `requestMatchers` DSL method, Spring Security automatically creates a `MvcRequestMatcher` if it detects that Spring MVC is available in the classpath. +Fortunately, when using the `requestMatchers` DSL method, Spring Security automatically creates a `PathPatternRequestMatcher` if it detects that Spring MVC is available in the classpath. Therefore, it will protect the same URLs that Spring MVC will match on by using Spring MVC to match on the URL. One common requirement when using Spring MVC is to specify the servlet path property. -For Java-based Configuration, you can use the `MvcRequestMatcher.Builder` to create multiple `MvcRequestMatcher` instances that share the same servlet path: +For Java-based Configuration, you can use the `PathPatternRequestMatcher.Builder` to create multiple `PathPatternRequestMatcher` instances that share the same servlet path: [source,java,role="primary"] ---- @Bean -public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception { - MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/path"); +public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + PathPatternRequestMatcher.Builder mvcMatcherBuilder = new PathPatternRequestMatcher.Builder().servletPath("/path"); http .authorizeHttpRequests((authorize) -> authorize .requestMatchers(mvcMatcherBuilder.pattern("/admin")).hasRole("ADMIN") @@ -217,6 +217,11 @@ public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospe } ---- +[NOTE] +===== +For your convenience, you also can use `ServletRequestMatcherBuilders` which exposes an API that more directly reflects the Servlet spec. +===== + For Kotlin and XML, this happens when you specify the servlet path for each path like so: [tabs] diff --git a/etc/checkstyle/checkstyle-suppressions.xml b/etc/checkstyle/checkstyle-suppressions.xml index b368ce84e84..8fe152549db 100644 --- a/etc/checkstyle/checkstyle-suppressions.xml +++ b/etc/checkstyle/checkstyle-suppressions.xml @@ -38,6 +38,8 @@ + + diff --git a/web/src/main/java/org/springframework/security/web/FilterInvocation.java b/web/src/main/java/org/springframework/security/web/FilterInvocation.java index f9f86476c8c..9bc1a9a40b6 100644 --- a/web/src/main/java/org/springframework/security/web/FilterInvocation.java +++ b/web/src/main/java/org/springframework/security/web/FilterInvocation.java @@ -25,6 +25,7 @@ import java.lang.reflect.Proxy; import java.util.Collections; import java.util.Enumeration; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -178,6 +179,8 @@ static class DummyRequest extends HttpServletRequestWrapper { private final Map parameters = new LinkedHashMap<>(); + private final Map attributes = new HashMap<>(); + DummyRequest() { super(UNSUPPORTED_REQUEST); } @@ -189,7 +192,12 @@ public String getCharacterEncoding() { @Override public Object getAttribute(String attributeName) { - return null; + return this.attributes.get(attributeName); + } + + @Override + public void setAttribute(String name, Object value) { + this.attributes.put(name, value); } void setRequestURI(String requestURI) {