diff --git a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java index cade4d9a021..12d0e5d70eb 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java +++ b/config/src/main/java/org/springframework/security/config/annotation/AbstractConfiguredSecurityBuilder.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.util.Assert; import org.springframework.web.filter.DelegatingFilterProxy; @@ -57,7 +58,7 @@ public abstract class AbstractConfiguredSecurityBuilder>, List>> configurers = new LinkedHashMap>, List>>(); private final List> configurersAddedInInitializing = new ArrayList>(); - private final Map, Object> sharedObjects = new HashMap, Object>(); + private final Map, Object> sharedObjects = new HashMap, Object>(); private final boolean allowConfigurersOfSameType; @@ -155,7 +156,7 @@ public > C apply(C configurer) throws Excepti */ @SuppressWarnings("unchecked") public void setSharedObject(Class sharedType, C object) { - this.sharedObjects.put((Class) sharedType, object); + this.sharedObjects.put(sharedType, object); } /** @@ -173,7 +174,7 @@ public C getSharedObject(Class sharedType) { * Gets the shared objects * @return */ - public Map, Object> getSharedObjects() { + public Map, Object> getSharedObjects() { return Collections.unmodifiableMap(this.sharedObjects); } @@ -300,7 +301,7 @@ public O objectPostProcessor(ObjectPostProcessor objectPostProcessor) { * @return the possibly modified Object to use */ protected

P postProcess(P object) { - return (P) this.objectPostProcessor.postProcess(object); + return this.objectPostProcessor.postProcess(object); } /** 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 fe2c81114d7..c31fe95f7d3 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 @@ -19,12 +19,15 @@ import java.util.Arrays; import java.util.List; +import org.springframework.context.ApplicationContext; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry; +import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; 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.web.servlet.handler.HandlerMappingIntrospector; /** * A base class for registering {@link RequestMatcher}'s. For example, it might allow for @@ -39,6 +42,12 @@ public abstract class AbstractRequestMatcherRegistry { private static final RequestMatcher ANY_REQUEST = AnyRequestMatcher.INSTANCE; + private ApplicationContext context; + + protected final void setApplicationContext(ApplicationContext context) { + this.context = context; + } + /** * Maps any request. * @@ -92,6 +101,57 @@ public C antMatchers(String... antPatterns) { return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns)); } + /** + *

+ * Maps 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/", + * "/path.html", etc. + *

+ *

+ * If the current request will not be processed by Spring MVC, a reasonable default + * using the pattern as a ant pattern will be used. + *

+ * + * @param mvcPatterns the patterns to match on. The rules for matching are defined by + * Spring MVC + * @return the object that is chained after creating the {@link RequestMatcher}. + */ + public C mvcMatchers(String... mvcPatterns) { + return mvcMatchers(null, mvcPatterns); + } + + /** + *

+ * Maps an {@link MvcRequestMatcher} that also specifies a specific {@link HttpMethod} + * to match on. 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 current request will not be processed by Spring MVC, a reasonable default + * using the pattern as a ant pattern will be used. + *

+ * + * @param method the HTTP method to match on + * @param mvcPatterns the patterns to match on. The rules for matching are defined by + * Spring MVC + * @return the object that is chained after creating the {@link RequestMatcher}. + */ + public C mvcMatchers(HttpMethod method, String... mvcPatterns) { + HandlerMappingIntrospector introspector = new HandlerMappingIntrospector( + this.context); + List matchers = new ArrayList(mvcPatterns.length); + for (String mvcPattern : mvcPatterns) { + MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern); + if (method != null) { + matcher.setMethod(method); + } + matchers.add(matcher); + } + return chainRequestMatchers(matchers); + } + /** * Maps a {@link List} of * {@link org.springframework.security.web.util.matcher.RegexRequestMatcher} diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java index f99fb6753a1..61200331c4c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/HttpSecurity.java @@ -23,6 +23,7 @@ import javax.servlet.Filter; import javax.servlet.http.HttpServletRequest; +import org.springframework.context.ApplicationContext; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; @@ -62,6 +63,7 @@ import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.PortMapper; import org.springframework.security.web.PortMapperImpl; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer; import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; @@ -113,7 +115,7 @@ public final class HttpSecurity extends AbstractConfiguredSecurityBuilder implements SecurityBuilder, HttpSecurityBuilder { - private final RequestMatcherConfigurer requestMatcherConfigurer = new RequestMatcherConfigurer(); + private final RequestMatcherConfigurer requestMatcherConfigurer; private List filters = new ArrayList(); private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE; private FilterComparator comparator = new FilterComparator(); @@ -126,15 +128,24 @@ public final class HttpSecurity extends * @param sharedObjects the shared Objects to initialize the {@link HttpSecurity} with * @see WebSecurityConfiguration */ + @SuppressWarnings("unchecked") public HttpSecurity(ObjectPostProcessor objectPostProcessor, AuthenticationManagerBuilder authenticationBuilder, - Map, Object> sharedObjects) { + Map, Object> sharedObjects) { super(objectPostProcessor); Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null"); setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder); - for (Map.Entry, Object> entry : sharedObjects.entrySet()) { - setSharedObject(entry.getKey(), entry.getValue()); + for (Map.Entry, Object> entry : sharedObjects + .entrySet()) { + setSharedObject((Class) entry.getKey(), entry.getValue()); } + ApplicationContext context = (ApplicationContext) sharedObjects + .get(ApplicationContext.class); + this.requestMatcherConfigurer = new RequestMatcherConfigurer(context); + } + + private ApplicationContext getContext() { + return getSharedObject(ApplicationContext.class); } /** @@ -634,7 +645,8 @@ public RememberMeConfigurer rememberMe() throws Exception { */ public ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception { - return getOrApply(new ExpressionUrlAuthorizationConfigurer()) + ApplicationContext context = getContext(); + return getOrApply(new ExpressionUrlAuthorizationConfigurer(context)) .getRegistry(); } @@ -710,7 +722,8 @@ public ServletApiConfigurer servletApi() throws Exception { * @throws Exception */ public CsrfConfigurer csrf() throws Exception { - return getOrApply(new CsrfConfigurer()); + ApplicationContext context = getContext(); + return getOrApply(new CsrfConfigurer(context)); } /** @@ -917,7 +930,9 @@ public FormLoginConfigurer formLogin() throws Exception { */ public ChannelSecurityConfigurer.ChannelRequestMatcherRegistry requiresChannel() throws Exception { - return getOrApply(new ChannelSecurityConfigurer()).getRegistry(); + ApplicationContext context = getContext(); + return getOrApply(new ChannelSecurityConfigurer(context)) + .getRegistry(); } /** @@ -1241,8 +1256,16 @@ public HttpSecurity regexMatcher(String pattern) { */ public final class RequestMatcherConfigurer extends AbstractRequestMatcherRegistry { + private List matchers = new ArrayList(); + /** + * @param context + */ + private RequestMatcherConfigurer(ApplicationContext context) { + setApplicationContext(context); + } + protected RequestMatcherConfigurer chainRequestMatchers( List requestMatchers) { matchers.addAll(requestMatchers); @@ -1259,8 +1282,6 @@ public HttpSecurity and() { return HttpSecurity.this; } - private RequestMatcherConfigurer() { - } } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java index c666a9fbcb2..53f55acd55b 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/builders/WebSecurity.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -80,7 +81,7 @@ public final class WebSecurity extends private final List> securityFilterChainBuilders = new ArrayList>(); - private final IgnoredRequestConfigurer ignoredRequestRegistry = new IgnoredRequestConfigurer(); + private IgnoredRequestConfigurer ignoredRequestRegistry; private FilterSecurityInterceptor filterSecurityInterceptor; @@ -316,6 +317,10 @@ protected Filter performBuild() throws Exception { public final class IgnoredRequestConfigurer extends AbstractRequestMatcherRegistry { + private IgnoredRequestConfigurer(ApplicationContext context) { + setApplicationContext(context); + } + @Override protected IgnoredRequestConfigurer chainRequestMatchers( List requestMatchers) { @@ -329,13 +334,13 @@ protected IgnoredRequestConfigurer chainRequestMatchers( public WebSecurity and() { return WebSecurity.this; } - - private IgnoredRequestConfigurer() { - } } + @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - defaultWebSecurityExpressionHandler.setApplicationContext(applicationContext); + this.defaultWebSecurityExpressionHandler + .setApplicationContext(applicationContext); + this.ignoredRequestRegistry = new IgnoredRequestConfigurer(applicationContext); } } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java index b6010de8595..7c1093e332c 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebSecurityConfigurerAdapter.java @@ -329,6 +329,14 @@ protected void configure(HttpSecurity http) throws Exception { } // @formatter:on + /** + * Gets the ApplicationContext + * @return the context + */ + protected final ApplicationContext getApplicationContext() { + return this.context; + } + @Autowired public void setApplicationContext(ApplicationContext context) { this.context = context; diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java index fe28944d7f0..f5cc231a7a0 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ChannelSecurityConfigurer.java @@ -20,6 +20,7 @@ import java.util.LinkedHashMap; import java.util.List; +import org.springframework.context.ApplicationContext; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.config.annotation.ObjectPostProcessor; @@ -80,13 +81,14 @@ public final class ChannelSecurityConfigurer> e private LinkedHashMap> requestMap = new LinkedHashMap>(); private List channelProcessors; - private final ChannelRequestMatcherRegistry REGISTRY = new ChannelRequestMatcherRegistry(); + private final ChannelRequestMatcherRegistry REGISTRY; /** * Creates a new instance * @see HttpSecurity#requiresChannel() */ - public ChannelSecurityConfigurer() { + public ChannelSecurityConfigurer(ApplicationContext context) { + this.REGISTRY = new ChannelRequestMatcherRegistry(context); } public ChannelRequestMatcherRegistry getRegistry() { @@ -146,6 +148,10 @@ private ChannelRequestMatcherRegistry addAttribute(String attribute, public final class ChannelRequestMatcherRegistry extends AbstractConfigAttributeRequestMatcherRegistry { + private ChannelRequestMatcherRegistry(ApplicationContext context) { + setApplicationContext(context); + } + @Override protected RequiresChannelUrl chainRequestMatchersInternal( List requestMatchers) { @@ -185,9 +191,6 @@ public ChannelRequestMatcherRegistry channelProcessors( public H and() { return ChannelSecurityConfigurer.this.and(); } - - private ChannelRequestMatcherRegistry() { - } } public final class RequiresChannelUrl { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java index b0a146c10d7..09c9fab3e55 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/CsrfConfigurer.java @@ -21,6 +21,7 @@ import javax.servlet.http.HttpServletRequest; +import org.springframework.context.ApplicationContext; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.HttpSecurityBuilder; @@ -78,12 +79,14 @@ public final class CsrfConfigurer> new HttpSessionCsrfTokenRepository()); private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER; private List ignoredCsrfProtectionMatchers = new ArrayList(); + private final ApplicationContext context; /** * Creates a new instance * @see HttpSecurity#csrf() */ - public CsrfConfigurer() { + public CsrfConfigurer(ApplicationContext context) { + this.context = context; } /** @@ -141,7 +144,8 @@ public CsrfConfigurer requireCsrfProtectionMatcher( * @since 4.0 */ public CsrfConfigurer ignoringAntMatchers(String... antPatterns) { - return new IgnoreCsrfProtectionRegistry().antMatchers(antPatterns).and(); + return new IgnoreCsrfProtectionRegistry(this.context).antMatchers(antPatterns) + .and(); } @SuppressWarnings("unchecked") @@ -265,6 +269,13 @@ private AccessDeniedHandler createAccessDeniedHandler(H http) { private class IgnoreCsrfProtectionRegistry extends AbstractRequestMatcherRegistry { + /** + * @param context + */ + private IgnoreCsrfProtectionRegistry(ApplicationContext context) { + setApplicationContext(context); + } + public CsrfConfigurer and() { return CsrfConfigurer.this; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java index 634ae6b14c5..41781a8b01d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/ExpressionUrlAuthorizationConfigurer.java @@ -84,7 +84,7 @@ public final class ExpressionUrlAuthorizationConfigurer expressionHandler; @@ -92,7 +92,8 @@ public final class ExpressionUrlAuthorizationConfigurer.AbstractInterceptUrlRegistry { + /** + * @param context + */ + private ExpressionInterceptUrlRegistry(ApplicationContext context) { + setApplicationContext(context); + } + @Override protected final AuthorizedUrl chainRequestMatchersInternal( List requestMatchers) { diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java index c21272e5e3c..5a9a275584d 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/UrlAuthorizationConfigurer.java @@ -19,6 +19,7 @@ import java.util.Collection; import java.util.List; +import org.springframework.context.ApplicationContext; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.ConfigAttribute; @@ -86,7 +87,11 @@ */ public final class UrlAuthorizationConfigurer> extends AbstractInterceptUrlConfigurer, H> { - private final StandardInterceptUrlRegistry REGISTRY = new StandardInterceptUrlRegistry(); + private final StandardInterceptUrlRegistry REGISTRY; + + public UrlAuthorizationConfigurer(ApplicationContext context) { + this.REGISTRY = new StandardInterceptUrlRegistry(context); + } /** * The StandardInterceptUrlRegistry is what users will interact with after applying @@ -114,6 +119,13 @@ public class StandardInterceptUrlRegistry extends ExpressionUrlAuthorizationConfigurer.AbstractInterceptUrlRegistry { + /** + * @param context + */ + private StandardInterceptUrlRegistry(ApplicationContext context) { + setApplicationContext(context); + } + @Override protected final AuthorizedUrl chainRequestMatchersInternal( List requestMatchers) { diff --git a/config/src/main/java/org/springframework/security/config/http/FilterChainBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/FilterChainBeanDefinitionParser.java index c51c6583610..0ca383f1522 100644 --- a/config/src/main/java/org/springframework/security/config/http/FilterChainBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/FilterChainBeanDefinitionParser.java @@ -45,7 +45,7 @@ public BeanDefinition parse(Element elt, ParserContext pc) { if (StringUtils.hasText(path)) { Assert.isTrue(!StringUtils.hasText(requestMatcher), ""); - builder.addConstructorArgValue(matcherType.createMatcher(path, null)); + builder.addConstructorArgValue(matcherType.createMatcher(pc, path, null)); } else { Assert.isTrue(StringUtils.hasText(requestMatcher), ""); diff --git a/config/src/main/java/org/springframework/security/config/http/FilterChainMapBeanDefinitionDecorator.java b/config/src/main/java/org/springframework/security/config/http/FilterChainMapBeanDefinitionDecorator.java index 7911c8f7c72..0305a695c73 100644 --- a/config/src/main/java/org/springframework/security/config/http/FilterChainMapBeanDefinitionDecorator.java +++ b/config/src/main/java/org/springframework/security/config/http/FilterChainMapBeanDefinitionDecorator.java @@ -71,7 +71,7 @@ public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder holder, + "'must not be empty", elt); } - BeanDefinition matcher = matcherType.createMatcher(path, null); + BeanDefinition matcher = matcherType.createMatcher(parserContext, path, null); if (filters.equals(HttpSecurityBeanDefinitionParser.OPT_FILTERS_NONE)) { securityFilterChains.add(createSecurityFilterChain(matcher, diff --git a/config/src/main/java/org/springframework/security/config/http/FilterInvocationSecurityMetadataSourceParser.java b/config/src/main/java/org/springframework/security/config/http/FilterInvocationSecurityMetadataSourceParser.java index 32781393a36..15652403974 100644 --- a/config/src/main/java/org/springframework/security/config/http/FilterInvocationSecurityMetadataSourceParser.java +++ b/config/src/main/java/org/springframework/security/config/http/FilterInvocationSecurityMetadataSourceParser.java @@ -165,7 +165,8 @@ private static ManagedMap parseInterceptUrlsForF method = null; } - BeanDefinition matcher = matcherType.createMatcher(path, method); + BeanDefinition matcher = matcherType.createMatcher(parserContext, path, + method); BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder .rootBeanDefinition(SecurityConfig.class); @@ -194,7 +195,8 @@ private static ManagedMap parseInterceptUrlsForF if (addAuthenticatedAll && filterInvocationDefinitionMap.isEmpty()) { - BeanDefinition matcher = matcherType.createMatcher("/**", null); + BeanDefinition matcher = matcherType.createMatcher(parserContext, "/**", + null); BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder .rootBeanDefinition(SecurityConfig.class); attributeBuilder.addConstructorArgValue(new String[] { "authenticated" }); 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 new file mode 100644 index 00000000000..d1d2d99c272 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/http/HandlerMappingIntrospectorFactoryBean.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.http; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; + +/** + * Used for creating an instance of {@link HandlerMappingIntrospector} and autowiring the + * {@link ApplicationContext}. + * + * @author Rob Winch + * @since 4.1.1 + */ +class HandlerMappingIntrospectorFactoryBean implements ApplicationContextAware { + + private ApplicationContext context; + + HandlerMappingIntrospector createHandlerMappingIntrospector() { + return new HandlerMappingIntrospector(this.context); + } + + /* + * (non-Javadoc) + * + * @see org.springframework.context.ApplicationContextAware#setApplicationContext(org. + * springframework.context.ApplicationContext) + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + this.context = applicationContext; + } + +} diff --git a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java index 2f95c52967d..9a75f8d8d5a 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java @@ -613,7 +613,7 @@ private ManagedMap parseInterceptUrlsForChannelS String requiredChannel = urlElt.getAttribute(ATT_REQUIRES_CHANNEL); if (StringUtils.hasText(requiredChannel)) { - BeanDefinition matcher = matcherType.createMatcher(path, method); + BeanDefinition matcher = matcherType.createMatcher(pc, path, method); RootBeanDefinition channelAttributes = new RootBeanDefinition( ChannelAttributeFactory.class); diff --git a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java index 004eb44908d..4f8796746f1 100644 --- a/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/HttpSecurityBeanDefinitionParser.java @@ -198,7 +198,7 @@ private BeanReference createSecurityFilterChainBean(Element element, } else if (StringUtils.hasText(filterChainPattern)) { - filterChainMatcher = MatcherType.fromElement(element).createMatcher( + filterChainMatcher = MatcherType.fromElement(element).createMatcher(pc, filterChainPattern, null); } else { 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 20203983839..efe1d9f460a 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 @@ -18,6 +18,8 @@ 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.security.web.servlet.util.matcher.MvcRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RegexRequestMatcher; @@ -33,17 +35,20 @@ */ public enum MatcherType { ant(AntPathRequestMatcher.class), regex(RegexRequestMatcher.class), ciRegex( - RegexRequestMatcher.class); + 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 ATT_MATCHER_TYPE = "request-matcher"; - private final Class type; + final Class type; MatcherType(Class type) { this.type = type; } - public BeanDefinition createMatcher(String path, String method) { + public BeanDefinition createMatcher(ParserContext pc, String path, String method) { if (("/**".equals(path) || "**".equals(path)) && method == null) { return new RootBeanDefinition(AnyRequestMatcher.class); } @@ -51,8 +56,28 @@ public BeanDefinition createMatcher(String path, String method) { BeanDefinitionBuilder matcherBldr = BeanDefinitionBuilder .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(path); - matcherBldr.addConstructorArgValue(method); + if (this == mvc) { + matcherBldr.addPropertyValue("method", method); + } + else { + matcherBldr.addConstructorArgValue(method); + } if (this == ciRegex) { matcherBldr.addConstructorArgValue(true); diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc index fbadd168488..a4ea4a5b7b6 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.rnc @@ -12,8 +12,8 @@ base64 = ## Whether a string should be base64 encoded attribute base64 {xsd:boolean} request-matcher = - ## Defines the strategy use for matching incoming requests. Currently the options are 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. - attribute request-matcher {"ant" | "regex" | "ciRegex"} + ## Defines the strategy use for matching incoming requests. Currently the options are 'mvc' (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for case-insensitive regular expressions. + attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"} port = ## Specifies an IP port number. Used to configure an embedded LDAP server, for example. attribute port { xsd:positiveInteger } diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd index 1c429cff2b3..fdcba48e9a5 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-4.1.xsd @@ -34,13 +34,14 @@ - Defines the strategy use for matching incoming requests. Currently the options are 'ant' - (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for - case-insensitive regular expressions. + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + @@ -1187,13 +1188,14 @@ - Defines the strategy use for matching incoming requests. Currently the options are 'ant' - (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for - case-insensitive regular expressions. + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + @@ -1557,13 +1559,14 @@ - Defines the strategy use for matching incoming requests. Currently the options are 'ant' - (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for - case-insensitive regular expressions. + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + @@ -1668,13 +1671,14 @@ - Defines the strategy use for matching incoming requests. Currently the options are 'ant' - (for ant path patterns), 'regex' for regular expressions and 'ciRegex' for - case-insensitive regular expressions. + Defines the strategy use for matching incoming requests. Currently the options are 'mvc' + (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions + and 'ciRegex' for case-insensitive regular expressions. + diff --git a/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/DisableUseExpressionsConfig.java b/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/DisableUseExpressionsConfig.java index 944c67b1913..35cc4b20b0a 100644 --- a/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/DisableUseExpressionsConfig.java +++ b/config/src/test/groovy/org/springframework/security/config/annotation/web/builders/DisableUseExpressionsConfig.java @@ -25,7 +25,7 @@ public class DisableUseExpressionsConfig extends BaseWebConfig { protected void configure(HttpSecurity http) throws Exception { // This config is also on UrlAuthorizationConfigurer javadoc http - .apply(new UrlAuthorizationConfigurer()).getRegistry() + .apply(new UrlAuthorizationConfigurer(getApplicationContext())).getRegistry() .antMatchers("/users**","/sessions/**").hasRole("USER") .antMatchers("/signup").hasRole("ANONYMOUS") .anyRequest().hasRole("USER"); 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 56b3d9ff622..0d8e3e95df9 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 @@ -24,6 +24,8 @@ import org.springframework.mock.web.MockHttpServletResponse import org.springframework.security.access.SecurityConfig import org.springframework.security.crypto.codec.Base64 import org.springframework.security.web.access.intercept.FilterSecurityInterceptor +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; /** @@ -197,6 +199,73 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests { response.status == HttpServletResponse.SC_FORBIDDEN } + def "intercept-url supports mvc matchers"() { + setup: + MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') + MockHttpServletResponse response = new MockHttpServletResponse() + MockFilterChain chain = new MockFilterChain() + xml.http('request-matcher':'mvc') { + 'http-basic'() + 'intercept-url'(pattern: '/path', access: "denyAll") + } + bean('pathController',PathController) + xml.'mvc:annotation-driven'() + + createAppContext() + when: + request.servletPath = "/path" + springSecurityFilterChain.doFilter(request, response, chain) + then: + response.status == HttpServletResponse.SC_UNAUTHORIZED + when: + request = new MockHttpServletRequest(method:'GET') + response = new MockHttpServletResponse() + chain = new MockFilterChain() + request.servletPath = "/path.html" + springSecurityFilterChain.doFilter(request, response, chain) + then: + response.status == HttpServletResponse.SC_UNAUTHORIZED + when: + request = new MockHttpServletRequest(method:'GET') + response = new MockHttpServletResponse() + chain = new MockFilterChain() + request.servletPath = "/path/" + springSecurityFilterChain.doFilter(request, response, chain) + then: + response.status == HttpServletResponse.SC_UNAUTHORIZED + } + + def "intercept-url mvc supports path variables"() { + setup: + MockHttpServletRequest request = new MockHttpServletRequest(method:'GET') + MockHttpServletResponse response = new MockHttpServletResponse() + MockFilterChain chain = new MockFilterChain() + xml.http('request-matcher':'mvc') { + 'http-basic'() + 'intercept-url'(pattern: '/user/{un}/**', access: "#un == 'user'") + } + createAppContext() + when: 'user can access' + request.servletPath = '/user/user/abc' + springSecurityFilterChain.doFilter(request,response,chain) + then: 'The response is OK' + response.status == HttpServletResponse.SC_OK + when: 'cannot access otheruser' + request = new MockHttpServletRequest(method:'GET', servletPath : '/user/otheruser/abc') + login(request, 'user', 'password') + chain.reset() + springSecurityFilterChain.doFilter(request,response,chain) + then: 'The response is OK' + response.status == HttpServletResponse.SC_FORBIDDEN + when: 'user can access case insensitive URL' + request = new MockHttpServletRequest(method:'GET', servletPath : '/USER/user/abc') + login(request, 'user', 'password') + chain.reset() + springSecurityFilterChain.doFilter(request,response,chain) + then: 'The response is OK' + response.status == HttpServletResponse.SC_FORBIDDEN + } + public static class Id { public boolean isOne(int i) { return i == 1; @@ -207,4 +276,12 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests { String toEncode = username + ':' + password request.addHeader('Authorization','Basic ' + new String(Base64.encode(toEncode.getBytes('UTF-8')))) } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } } \ No newline at end of file diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java index ec8808caf9f..8e3865e2206 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeRequestsTests.java @@ -28,6 +28,7 @@ import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -41,7 +42,10 @@ import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.assertj.core.api.Assertions.assertThat; @@ -249,9 +253,117 @@ public RoleHierarchy roleHiearchy() { } } + @Test + public void mvcMatcher() throws Exception { + loadConfig(MvcMatcherConfig.class); + + this.request.setServletPath("/path"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/path.html"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + + setup(); + + this.request.setServletPath("/path/"); + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()) + .isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MvcMatcherConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic().and() + .authorizeRequests() + .mvcMatchers("/path").denyAll(); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication(); + // @formatter:on + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } + } + + @Test + public void mvcMatcherPathVariables() throws Exception { + loadConfig(MvcMatcherPathVariablesConfig.class); + + this.request.setServletPath("/user/user"); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK); + + this.setup(); + this.request.setServletPath("/user/deny"); + + this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain); + + assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED); + } + + @EnableWebSecurity + @Configuration + @EnableWebMvc + static class MvcMatcherPathVariablesConfig extends WebSecurityConfigurerAdapter { + @Override + protected void configure(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic().and() + .authorizeRequests() + .mvcMatchers("/user/{userName}").access("#userName == 'user'"); + // @formatter:on + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .inMemoryAuthentication(); + // @formatter:on + } + + @RestController + static class PathController { + @RequestMapping("/path") + public String path() { + return "path"; + } + } + } + public void loadConfig(Class... configs) { this.context = new AnnotationConfigWebApplicationContext(); this.context.register(configs); + this.context.setServletContext(new MockServletContext()); this.context.refresh(); this.context.getAutowireCapableBeanFactory().autowireBean(this); diff --git a/docs/manual/src/docs/asciidoc/index.adoc b/docs/manual/src/docs/asciidoc/index.adoc index 17e350c312a..47e2c36f704 100644 --- a/docs/manual/src/docs/asciidoc/index.adoc +++ b/docs/manual/src/docs/asciidoc/index.adoc @@ -389,6 +389,7 @@ Here is the list of improvements: * Ability to add a `Filter` at a specific location in the chain using `HttpSecurity.addFilterAt` === Web Application Security Improvements +* <> * <> * <> * <> @@ -6726,6 +6727,77 @@ To enable Spring Security integration with Spring MVC add the `@EnableWebSecurit NOTE: Spring Security provides the configuration using Spring MVC's http://docs.spring.io/spring-framework/docs/4.1.x/spring-framework-reference/htmlsingle/#mvc-config-customize[WebMvcConfigurerAdapter]. This means that if you are using more advanced options, like integrating with `WebMvcConfigurationSupport` directly, then you will need to manually provide the Spring Security configuration. +[[mvc-requestmatcher]] +=== MvcRequestMatcher + +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. + +[NOTE] +==== +It is always recommended to provide authorization rules by matching on the `HttpServletRequest` and method security. + +Providing authorization rules by matching on `HttpServletRequest` is good because it happens very early in the code path and helps reduce the https://en.wikipedia.org/wiki/Attack_surface[attack surface]. +Method security ensures that if someone has bypassed the web authorization rules, that your application is still secured. +This is what is known as https://en.wikipedia.org/wiki/Defense_in_depth_(computing)[Defence in Depth] +==== + +Consider a controller that is mapped as follows: + +[source,java] +---- +@RequestMapping("/admin") +public String admin() { +---- + +If we wanted to restrict access to this controller method to admin users, a developer can provide authorization rules by matching on the `HttpServletRequest` with the following: + +[source,java] +---- +protected configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .antMatchers("/admin").hasRole("ADMIN"); +} +---- + +or in XML + +[source,xml] +---- + + + +---- + +With either configuration, the URL `/admin` will require the authenticated user to be an admin user. +However, depending on our Spring MVC configuration, the URL `/admin.html` will also map to our `admin()` method. +Additionally, depending on our Spring MVC configuration, the URL `/admin/` will also map to our `admin()` method. + +The problem is that our security rule is only protecting `/admin`. +We could add additional rules for all the permutations of Spring MVC, but this would be quite verbose and tedious. + +Instead, we can leverage Spring Security's `MvcRequestMatcher`. +The following configuration will protect the same URLs that Spring MVC will match on by using Spring MVC to match on the URL. + + +[source,java] +---- +protected configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .mvcMatchers("/admin").hasRole("ADMIN"); +} +---- + +or in XML + +[source,xml] +---- + + + +---- [[mvc-authentication-principal]] === @AuthenticationPrincipal @@ -7403,7 +7475,7 @@ Sets the realm name used for basic authentication (if enabled). Corresponds to t [[nsa-http-request-matcher]] * **request-matcher** -Defines the `RequestMatcher` strategy used in the `FilterChainProxy` and the beans created by the `intercept-url` to match incoming requests. Options are currently `ant`, `regex` and `ciRegex`, for ant, regular-expression and case-insensitive regular-expression repsectively. A separate instance is created for each<> element using its <> and <> attributes. Ant paths are matched using an `AntPathRequestMatcher` and regular expressions are matched using a `RegexRequestMatcher`. See the Javadoc for these classes for more details on exactly how the matching is preformed. Ant paths are the default strategy. +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` and regular expressions are matched using a `RegexRequestMatcher`. See the Javadoc for these classes for more details on exactly how the matching is performed. Ant paths are the default strategy. [[nsa-http-request-matcher-ref]] diff --git a/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java b/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java index 8f77a946c20..d988a14ef9d 100644 --- a/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java +++ b/web/src/main/java/org/springframework/security/web/access/expression/ExpressionBasedFilterInvocationSecurityMetadataSource.java @@ -33,6 +33,7 @@ import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestVariablesExtractor; import org.springframework.util.Assert; /** @@ -96,6 +97,10 @@ private static AbstractVariableEvaluationContextPostProcessor createPostProcesso return new AntPathMatcherEvaluationContextPostProcessor( (AntPathRequestMatcher) request); } + if (request instanceof RequestVariablesExtractor) { + return new RequestVariablesExtractorEvaluationContextPostProcessor( + (RequestVariablesExtractor) request); + } return null; } @@ -119,4 +124,24 @@ String postProcessVariableName(String variableName) { } } + static class RequestVariablesExtractorEvaluationContextPostProcessor + extends AbstractVariableEvaluationContextPostProcessor { + private final RequestVariablesExtractor matcher; + + public RequestVariablesExtractorEvaluationContextPostProcessor( + RequestVariablesExtractor matcher) { + this.matcher = matcher; + } + + @Override + Map extractVariables(HttpServletRequest request) { + return this.matcher.extractUriTemplateVariables(request); + } + + @Override + String postProcessVariableName(String variableName) { + return variableName; + } + } + } diff --git a/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java new file mode 100644 index 00000000000..c326bdef4a6 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcher.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.servlet.util.matcher; + +import java.util.Collections; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.http.HttpMethod; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.security.web.util.matcher.RequestVariablesExtractor; +import org.springframework.util.AntPathMatcher; +import org.springframework.util.PathMatcher; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.web.servlet.handler.MatchableHandlerMapping; +import org.springframework.web.servlet.handler.RequestMatchResult; +import org.springframework.web.util.UrlPathHelper; + +/** + * A {@link RequestMatcher} that uses Spring MVC's {@link HandlerMappingIntrospector} to + * match the path and extract variables. + * + * @author Rob Winch + * @since 4.1.1 + */ +public final class MvcRequestMatcher + implements RequestMatcher, RequestVariablesExtractor { + private final DefaultMatcher defaultMatcher = new DefaultMatcher(); + + private final HandlerMappingIntrospector introspector; + private final String pattern; + private HttpMethod method; + + public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) { + this.introspector = introspector; + this.pattern = pattern; + } + + @Override + public boolean matches(HttpServletRequest request) { + if (this.method != null && !this.method.name().equals(request.getMethod())) { + return false; + } + MatchableHandlerMapping mapping = getMapping(request); + if (mapping == null) { + return this.defaultMatcher.matches(request); + } + RequestMatchResult matchResult = mapping.match(request, this.pattern); + return matchResult != null; + } + + private MatchableHandlerMapping getMapping(HttpServletRequest request) { + try { + return this.introspector.getMatchableHandlerMapping(request); + } + catch (Throwable t) { + return null; + } + } + + /* + * (non-Javadoc) + * + * @see org.springframework.security.web.util.matcher.RequestVariablesExtractor# + * extractUriTemplateVariables(javax.servlet.http.HttpServletRequest) + */ + @Override + public Map extractUriTemplateVariables(HttpServletRequest request) { + MatchableHandlerMapping mapping = getMapping(request); + if (mapping == null) { + return this.defaultMatcher.extractUriTemplateVariables(request); + } + RequestMatchResult result = mapping.match(request, this.pattern); + return result == null ? Collections.emptyMap() + : result.extractUriTemplateVariables(); + } + + /** + * @param method the method to set + */ + public void setMethod(HttpMethod method) { + this.method = method; + } + + private class DefaultMatcher implements RequestMatcher, RequestVariablesExtractor { + + private final UrlPathHelper pathHelper = new UrlPathHelper(); + + private final PathMatcher pathMatcher = new AntPathMatcher(); + + @Override + public boolean matches(HttpServletRequest request) { + String lookupPath = this.pathHelper.getLookupPathForRequest(request); + return matches(lookupPath); + } + + private boolean matches(String lookupPath) { + return this.pathMatcher.match(MvcRequestMatcher.this.pattern, lookupPath); + } + + @Override + public Map extractUriTemplateVariables( + HttpServletRequest request) { + String lookupPath = this.pathHelper.getLookupPathForRequest(request); + if (matches(lookupPath)) { + return this.pathMatcher.extractUriTemplateVariables( + MvcRequestMatcher.this.pattern, lookupPath); + } + return Collections.emptyMap(); + } + } +} \ No newline at end of file diff --git a/web/src/main/java/org/springframework/security/web/util/matcher/RequestVariablesExtractor.java b/web/src/main/java/org/springframework/security/web/util/matcher/RequestVariablesExtractor.java new file mode 100644 index 00000000000..d119ac76642 --- /dev/null +++ b/web/src/main/java/org/springframework/security/web/util/matcher/RequestVariablesExtractor.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.util.matcher; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +/** + * An interface for extracting URI variables from the {@link HttpServletRequest}. + * + * @author Rob Winch + * @since 4.1.1 + */ +public interface RequestVariablesExtractor { + + /** + * Extract URL template variables from the request. + * + * @param request the HttpServletRequest to obtain a URL to extract the variables from + * @return the URL variables or empty if no variables are found + */ + Map extractUriTemplateVariables(HttpServletRequest request); +} diff --git a/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java new file mode 100644 index 00000000000..26f481e6f33 --- /dev/null +++ b/web/src/test/java/org/springframework/security/web/servlet/util/matcher/MvcRequestMatcherTests.java @@ -0,0 +1,202 @@ +/* + * Copyright 2012-2016 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 + * + * http://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.servlet.util.matcher; + +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import org.springframework.http.HttpMethod; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.servlet.handler.HandlerMappingIntrospector; +import org.springframework.web.servlet.handler.MatchableHandlerMapping; +import org.springframework.web.servlet.handler.RequestMatchResult; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +/** + * @author Rob Winch + */ +@RunWith(MockitoJUnitRunner.class) +public class MvcRequestMatcherTests { + @Mock + HandlerMappingIntrospector introspector; + @Mock + MatchableHandlerMapping mapping; + @Mock + RequestMatchResult result; + @Captor + ArgumentCaptor pattern; + MockHttpServletRequest request; + + MvcRequestMatcher matcher; + + @Before + public void setup() throws Exception { + this.request = new MockHttpServletRequest(); + this.request.setMethod("GET"); + this.request.setServletPath("/path"); + this.matcher = new MvcRequestMatcher(this.introspector, "/path"); + } + + @Test + public void extractUriTemplateVariablesSuccess() throws Exception { + when(this.result.extractUriTemplateVariables()) + .thenReturn(Collections.singletonMap("p", "path")); + when(this.introspector.getMatchableHandlerMapping(this.request)) + .thenReturn(this.mapping); + when(this.mapping.match(eq(this.request), this.pattern.capture())) + .thenReturn(this.result); + + this.matcher = new MvcRequestMatcher(this.introspector, "/{p}"); + when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null); + + assertThat(this.matcher.extractUriTemplateVariables(this.request)) + .containsEntry("p", "path"); + } + + @Test + public void extractUriTemplateVariablesFail() throws Exception { + when(this.result.extractUriTemplateVariables()) + .thenReturn(Collections.emptyMap()); + when(this.introspector.getMatchableHandlerMapping(this.request)) + .thenReturn(this.mapping); + when(this.mapping.match(eq(this.request), this.pattern.capture())) + .thenReturn(this.result); + + assertThat(this.matcher.extractUriTemplateVariables(this.request)).isEmpty(); + } + + @Test + public void extractUriTemplateVariablesDefaultSuccess() throws Exception { + this.matcher = new MvcRequestMatcher(this.introspector, "/{p}"); + when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null); + + assertThat(this.matcher.extractUriTemplateVariables(this.request)) + .containsEntry("p", "path"); + } + + @Test + public void extractUriTemplateVariablesDefaultFail() throws Exception { + this.matcher = new MvcRequestMatcher(this.introspector, "/nomatch/{p}"); + when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null); + + assertThat(this.matcher.extractUriTemplateVariables(this.request)).isEmpty(); + } + + @Test + public void matchesPathOnlyTrue() throws Exception { + when(this.introspector.getMatchableHandlerMapping(this.request)) + .thenReturn(this.mapping); + when(this.mapping.match(eq(this.request), this.pattern.capture())) + .thenReturn(this.result); + + assertThat(this.matcher.matches(this.request)).isTrue(); + assertThat(this.pattern.getValue()).isEqualTo("/path"); + } + + @Test + public void matchesDefaultMatches() throws Exception { + when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null); + + assertThat(this.matcher.matches(this.request)).isTrue(); + } + + @Test + public void matchesDefaultDoesNotMatch() throws Exception { + this.request.setServletPath("/other"); + when(this.introspector.getMatchableHandlerMapping(this.request)).thenReturn(null); + + assertThat(this.matcher.matches(this.request)).isFalse(); + } + + @Test + public void matchesPathOnlyFalse() throws Exception { + when(this.introspector.getMatchableHandlerMapping(this.request)) + .thenReturn(this.mapping); + + assertThat(this.matcher.matches(this.request)).isFalse(); + } + + @Test + public void matchesMethodAndPathTrue() throws Exception { + this.matcher.setMethod(HttpMethod.GET); + when(this.introspector.getMatchableHandlerMapping(this.request)) + .thenReturn(this.mapping); + when(this.mapping.match(eq(this.request), this.pattern.capture())) + .thenReturn(this.result); + + assertThat(this.matcher.matches(this.request)).isTrue(); + assertThat(this.pattern.getValue()).isEqualTo("/path"); + } + + @Test + public void matchesMethodAndPathFalseMethod() throws Exception { + this.matcher.setMethod(HttpMethod.POST); + + assertThat(this.matcher.matches(this.request)).isFalse(); + // method compare should be done first since faster + verifyZeroInteractions(this.introspector); + } + + /** + * Malicious users can specify any HTTP Method to create a stacktrace and try to + * expose useful information about the system. We should ensure we ignore invalid HTTP + * methods. + * @throws Exception if an error occurs + */ + @Test + public void matchesInvalidMethodOnRequest() throws Exception { + this.matcher.setMethod(HttpMethod.GET); + this.request.setMethod("invalid"); + + assertThat(this.matcher.matches(this.request)).isFalse(); + // method compare should be done first since faster + verifyZeroInteractions(this.introspector); + } + + @Test + public void matchesMethodAndPathFalsePath() throws Exception { + this.matcher.setMethod(HttpMethod.GET); + when(this.introspector.getMatchableHandlerMapping(this.request)) + .thenReturn(this.mapping); + + assertThat(this.matcher.matches(this.request)).isFalse(); + } + + @Test + public void matchesGetMatchableHandlerMappingNull() throws Exception { + assertThat(this.matcher.matches(this.request)).isTrue(); + } + + @Test + public void matchesGetMatchableHandlerMappingThrows() throws Exception { + when(this.introspector.getMatchableHandlerMapping(this.request)).thenThrow( + new HttpRequestMethodNotSupportedException(this.request.getMethod())); + assertThat(this.matcher.matches(this.request)).isTrue(); + } +} \ No newline at end of file