From 04d14fb6febcc10770d7a6903d09210f9dbb5450 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Wed, 27 Sep 2017 10:31:16 -0500 Subject: [PATCH] Lookup HandlerMappingIntrospector from Bean --- .../web/AbstractRequestMatcherRegistry.java | 11 +++- .../web/configurers/CorsConfigurer.java | 12 +++- .../config/http/CorsBeanDefinitionParser.java | 9 +-- ...HandlerMappingIntrospectorFactoryBean.java | 22 ++++++- .../security/config/http/MatcherType.java | 16 +---- .../configurers/CorsConfigurerTests.groovy | 18 +++--- .../config/http/HttpCorsConfigTests.groovy | 17 ++---- .../http/InterceptUrlConfigTests.groovy | 1 + docs/manual/src/docs/asciidoc/index.adoc | 58 +++++++++++++++++++ 9 files changed, 114 insertions(+), 50 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 610edb8fd3..d3f0c047e6 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 @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.web; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.ObjectPostProcessor; @@ -42,6 +43,8 @@ import java.util.List; * @since 3.2 */ public abstract class AbstractRequestMatcherRegistry { + private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; + private static final RequestMatcher ANY_REQUEST = AnyRequestMatcher.INSTANCE; private ApplicationContext context; @@ -160,8 +163,12 @@ public abstract class AbstractRequestMatcherRegistry { String... mvcPatterns) { boolean isServlet30 = ClassUtils.isPresent("javax.servlet.ServletRegistration", getClass().getClassLoader()); ObjectPostProcessor opp = this.context.getBean(ObjectPostProcessor.class); - HandlerMappingIntrospector introspector = new HandlerMappingIntrospector( - this.context); + if(!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 = this.context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, + HandlerMappingIntrospector.class); List matchers = new ArrayList( mvcPatterns.length); for (String mvcPattern : mvcPatterns) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java index 8a6aac75f3..cc2273ba12 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CorsConfigurer.java @@ -15,6 +15,7 @@ */ package org.springframework.security.config.annotation.web.configurers; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; @@ -98,7 +99,9 @@ public class CorsConfigurer> return null; } + static class MvcCorsFilter { + private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; /** * This needs to be isolated into a separate class as Spring MVC is an optional * dependency and will potentially cause ClassLoading issues @@ -106,9 +109,12 @@ public class CorsConfigurer> * @return */ private static CorsFilter getMvcCorsFilter(ApplicationContext context) { - HandlerMappingIntrospector mappingIntrospector = new HandlerMappingIntrospector( - context); + if(!context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { + throw new NoSuchBeanDefinitionException(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, "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 mappingIntrospector = context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class); return new CorsFilter(mappingIntrospector); } } -} \ No newline at end of file +} diff --git a/config/src/main/java/org/springframework/security/config/http/CorsBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/CorsBeanDefinitionParser.java index d0db22dfa7..e56febef9a 100644 --- a/config/src/main/java/org/springframework/security/config/http/CorsBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/CorsBeanDefinitionParser.java @@ -15,17 +15,16 @@ */ package org.springframework.security.config.http; -import org.w3c.dom.Element; - import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.filter.CorsFilter; +import org.w3c.dom.Element; /** * Parser for the {@code CorsFilter}. @@ -71,8 +70,6 @@ public class CorsBeanDefinitionParser { return null; } - BeanDefinitionBuilder configurationSourceBldr = BeanDefinitionBuilder.rootBeanDefinition(HANDLER_MAPPING_INTROSPECTOR); - configurationSourceBldr.setAutowireMode(AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR); - return configurationSourceBldr.getBeanDefinition(); + return new RootBeanDefinition(HandlerMappingIntrospectorFactoryBean.class); } } diff --git a/config/src/main/java/org/springframework/security/config/http/HandlerMappingIntrospectorFactoryBean.java b/config/src/main/java/org/springframework/security/config/http/HandlerMappingIntrospectorFactoryBean.java index d1d2d99c27..e3c8f0a826 100644 --- a/config/src/main/java/org/springframework/security/config/http/HandlerMappingIntrospectorFactoryBean.java +++ b/config/src/main/java/org/springframework/security/config/http/HandlerMappingIntrospectorFactoryBean.java @@ -17,6 +17,8 @@ package org.springframework.security.config.http; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.web.servlet.handler.HandlerMappingIntrospector; @@ -28,12 +30,22 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector; * @author Rob Winch * @since 4.1.1 */ -class HandlerMappingIntrospectorFactoryBean implements ApplicationContextAware { +class HandlerMappingIntrospectorFactoryBean implements FactoryBean, ApplicationContextAware { + private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; private ApplicationContext context; - HandlerMappingIntrospector createHandlerMappingIntrospector() { - return new HandlerMappingIntrospector(this.context); + public HandlerMappingIntrospector getObject() { + if(!this.context.containsBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { + throw new NoSuchBeanDefinitionException(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, "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."); + } + return this.context.getBean(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, HandlerMappingIntrospector.class); + } + + @Override + public Class getObjectType() { + return HandlerMappingIntrospector.class; } /* @@ -48,4 +60,8 @@ class HandlerMappingIntrospectorFactoryBean implements ApplicationContextAware { this.context = applicationContext; } + @Override + public boolean isSingleton() { + return true; + } } 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 21bca84816..125ec17e8c 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 @@ -37,8 +37,7 @@ public enum MatcherType { ant(AntPathRequestMatcher.class), regex(RegexRequestMatcher.class), ciRegex( RegexRequestMatcher.class), mvc(MvcRequestMatcher.class); - private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "org.springframework.web.servlet.handler.HandlerMappingIntrospector"; - private static final String HANDLER_MAPPING_INTROSPECTOR_FACTORY_BEAN_NAME = "org.springframework.security.config.http.HandlerMappingIntrospectorFactoryBean"; + private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector"; private static final String ATT_MATCHER_TYPE = "request-matcher"; @@ -61,18 +60,7 @@ public enum MatcherType { .rootBeanDefinition(type); if (this == mvc) { - if (!pc.getRegistry().isBeanNameInUse(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) { - BeanDefinitionBuilder hmifb = BeanDefinitionBuilder - .rootBeanDefinition(HandlerMappingIntrospectorFactoryBean.class); - pc.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_FACTORY_BEAN_NAME, - hmifb.getBeanDefinition()); - - RootBeanDefinition hmi = new RootBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME); - hmi.setFactoryBeanName(HANDLER_MAPPING_INTROSPECTOR_FACTORY_BEAN_NAME); - hmi.setFactoryMethodName("createHandlerMappingIntrospector"); - pc.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, hmi); - } - matcherBldr.addConstructorArgReference(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME); + matcherBldr.addConstructorArgValue(new RootBeanDefinition(HandlerMappingIntrospectorFactoryBean.class)); } matcherBldr.addConstructorArgValue(path); diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.groovy b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.groovy index b0f6f4f098..7cf1f8c717 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/configurers/CorsConfigurerTests.groovy @@ -15,6 +15,9 @@ */ package org.springframework.security.config.annotation.web.configurers +import org.springframework.beans.factory.BeanCreationException +import org.springframework.beans.factory.NoSuchBeanDefinitionException + import javax.servlet.http.HttpServletResponse import org.springframework.context.annotation.Bean @@ -36,19 +39,12 @@ import org.springframework.web.servlet.config.annotation.EnableWebMvc */ class CorsConfigurerTests extends BaseSpringSpec { - def "HandlerMappingIntrospector default"() { - setup: - loadConfig(DefaultCorsConfig) + def "No MVC throws meaningful error"() { when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) + loadConfig(DefaultCorsConfig) then: - responseHeaders == ['X-Content-Type-Options':'nosniff', - 'X-Frame-Options':'DENY', - 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', - 'Expires' : '0', - 'Pragma':'no-cache', - 'X-XSS-Protection' : '1; mode=block'] + BeanCreationException success = thrown() + success.message.contains("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext") } @EnableWebSecurity diff --git a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy index acc71dc54d..f18f142173 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/HttpCorsConfigTests.groovy @@ -12,6 +12,8 @@ */ package org.springframework.security.config.http +import org.springframework.beans.factory.BeanCreationException + import javax.servlet.http.HttpServletResponse import org.springframework.http.* @@ -38,24 +40,17 @@ class HttpCorsConfigTests extends AbstractHttpConfigTests { chain = new MockFilterChain() } - def "HandlerMappingIntrospector default"() { - setup: + def "No MVC throws meaningful error"() { + when: xml.http('entry-point-ref' : 'ep') { 'cors'() 'intercept-url'(pattern:'/**', access: 'authenticated') } bean('ep', Http403ForbiddenEntryPoint) createAppContext() - when: - addCors() - springSecurityFilterChain.doFilter(request,response,chain) then: - responseHeaders == ['X-Content-Type-Options':'nosniff', - 'X-Frame-Options':'DENY', - 'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate', - 'Expires' : '0', - 'Pragma':'no-cache', - 'X-XSS-Protection' : '1; mode=block'] + BeanCreationException success = thrown() + success.message.contains("Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext") } def "HandlerMappingIntrospector explicit"() { diff --git a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy index 457f09da67..2b71baf185 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/InterceptUrlConfigTests.groovy @@ -252,6 +252,7 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests { 'http-basic'() 'intercept-url'(pattern: '/user/{un}/**', access: "#un == 'user'") } + xml.'mvc:annotation-driven'() createWebAppContext(servletContext) when: 'user can access' request.servletPath = '/user/user/abc' diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index 04d12b40e1..56a7ceaefb 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -6733,6 +6733,64 @@ NOTE: Spring Security provides the configuration using Spring MVC's http://docs. Spring Security provides deep integration with how Spring MVC matches on URLs with `MvcRequestMatcher`. This is helpful to ensure your Security rules match the logic used to handle your requests. +In order 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. + +For a `web.xml` this means that you should place your configuration in the `DispatcherServlet.xml`. + +[source,xml] +---- + + org.springframework.web.context.ContextLoaderListener + + + + + contextConfigLocation + /WEB-INF/spring/*.xml + + + + spring + org.springframework.web.servlet.DispatcherServlet + + + contextConfigLocation + + + + + + spring + / + +---- + +Below `WebSecurityConfiguration` in placed in the ``DispatcherServlet``s `ApplicationContext`. + +[source,java] +---- +public class SecurityInitializer extends + AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return null; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] { RootConfiguration.class, + WebMvcConfiguration.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} +---- + [NOTE] ==== It is always recommended to provide authorization rules by matching on the `HttpServletRequest` and method security.