Add MvcRequestMatcher

Fixes gh-3964
This commit is contained in:
Rob Winch 2016-07-01 16:30:51 -05:00
parent 13bc70f693
commit e4c13e3c0e
26 changed files with 918 additions and 54 deletions

View File

@ -25,6 +25,7 @@ import java.util.Map;
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<O, B extends SecurityBui
private final LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>> configurers = new LinkedHashMap<Class<? extends SecurityConfigurer<O, B>>, List<SecurityConfigurer<O, B>>>();
private final List<SecurityConfigurer<O, B>> configurersAddedInInitializing = new ArrayList<SecurityConfigurer<O, B>>();
private final Map<Class<Object>, Object> sharedObjects = new HashMap<Class<Object>, Object>();
private final Map<Class<? extends Object>, Object> sharedObjects = new HashMap<Class<? extends Object>, Object>();
private final boolean allowConfigurersOfSameType;
@ -155,7 +156,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
*/
@SuppressWarnings("unchecked")
public <C> void setSharedObject(Class<C> sharedType, C object) {
this.sharedObjects.put((Class<Object>) sharedType, object);
this.sharedObjects.put(sharedType, object);
}
/**
@ -173,7 +174,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
* Gets the shared objects
* @return
*/
public Map<Class<Object>, Object> getSharedObjects() {
public Map<Class<? extends Object>, Object> getSharedObjects() {
return Collections.unmodifiableMap(this.sharedObjects);
}
@ -300,7 +301,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
* @return the possibly modified Object to use
*/
protected <P> P postProcess(P object) {
return (P) this.objectPostProcessor.postProcess(object);
return this.objectPostProcessor.postProcess(object);
}
/**

View File

@ -19,12 +19,15 @@ import java.util.ArrayList;
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 @@ import org.springframework.security.web.util.matcher.RequestMatcher;
public abstract class AbstractRequestMatcherRegistry<C> {
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 abstract class AbstractRequestMatcherRegistry<C> {
return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns));
}
/**
* <p>
* 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.
* </p>
* <p>
* If the current request will not be processed by Spring MVC, a reasonable default
* using the pattern as a ant pattern will be used.
* </p>
*
* @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);
}
/**
* <p>
* 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.
* </p>
* <p>
* If the current request will not be processed by Spring MVC, a reasonable default
* using the pattern as a ant pattern will be used.
* </p>
*
* @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<RequestMatcher> matchers = new ArrayList<RequestMatcher>(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}

View File

@ -23,6 +23,7 @@ import java.util.Map;
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.core.userdetails.UserDetailsService;
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<DefaultSecurityFilterChain, HttpSecurity>
implements SecurityBuilder<DefaultSecurityFilterChain>,
HttpSecurityBuilder<HttpSecurity> {
private final RequestMatcherConfigurer requestMatcherConfigurer = new RequestMatcherConfigurer();
private final RequestMatcherConfigurer requestMatcherConfigurer;
private List<Filter> filters = new ArrayList<Filter>();
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<Object> objectPostProcessor,
AuthenticationManagerBuilder authenticationBuilder,
Map<Class<Object>, Object> sharedObjects) {
Map<Class<? extends Object>, Object> sharedObjects) {
super(objectPostProcessor);
Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null");
setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder);
for (Map.Entry<Class<Object>, Object> entry : sharedObjects.entrySet()) {
setSharedObject(entry.getKey(), entry.getValue());
for (Map.Entry<Class<? extends Object>, Object> entry : sharedObjects
.entrySet()) {
setSharedObject((Class<Object>) 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 final class HttpSecurity extends
*/
public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()
throws Exception {
return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>())
ApplicationContext context = getContext();
return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>(context))
.getRegistry();
}
@ -710,7 +722,8 @@ public final class HttpSecurity extends
* @throws Exception
*/
public CsrfConfigurer<HttpSecurity> csrf() throws Exception {
return getOrApply(new CsrfConfigurer<HttpSecurity>());
ApplicationContext context = getContext();
return getOrApply(new CsrfConfigurer<HttpSecurity>(context));
}
/**
@ -917,7 +930,9 @@ public final class HttpSecurity extends
*/
public ChannelSecurityConfigurer<HttpSecurity>.ChannelRequestMatcherRegistry requiresChannel()
throws Exception {
return getOrApply(new ChannelSecurityConfigurer<HttpSecurity>()).getRegistry();
ApplicationContext context = getContext();
return getOrApply(new ChannelSecurityConfigurer<HttpSecurity>(context))
.getRegistry();
}
/**
@ -1241,8 +1256,16 @@ public final class HttpSecurity extends
*/
public final class RequestMatcherConfigurer extends
AbstractRequestMatcherRegistry<RequestMatcherConfigurer> {
private List<RequestMatcher> matchers = new ArrayList<RequestMatcher>();
/**
* @param context
*/
private RequestMatcherConfigurer(ApplicationContext context) {
setApplicationContext(context);
}
protected RequestMatcherConfigurer chainRequestMatchers(
List<RequestMatcher> requestMatchers) {
matchers.addAll(requestMatchers);
@ -1259,8 +1282,6 @@ public final class HttpSecurity extends
return HttpSecurity.this;
}
private RequestMatcherConfigurer() {
}
}
/**

View File

@ -23,6 +23,7 @@ import javax.servlet.http.HttpServletRequest;
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<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders = new ArrayList<SecurityBuilder<? extends SecurityFilterChain>>();
private final IgnoredRequestConfigurer ignoredRequestRegistry = new IgnoredRequestConfigurer();
private IgnoredRequestConfigurer ignoredRequestRegistry;
private FilterSecurityInterceptor filterSecurityInterceptor;
@ -316,6 +317,10 @@ public final class WebSecurity extends
public final class IgnoredRequestConfigurer extends
AbstractRequestMatcherRegistry<IgnoredRequestConfigurer> {
private IgnoredRequestConfigurer(ApplicationContext context) {
setApplicationContext(context);
}
@Override
protected IgnoredRequestConfigurer chainRequestMatchers(
List<RequestMatcher> requestMatchers) {
@ -329,13 +334,13 @@ public final class WebSecurity extends
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);
}
}

View File

@ -329,6 +329,14 @@ public abstract class WebSecurityConfigurerAdapter implements
}
// @formatter:on
/**
* Gets the ApplicationContext
* @return the context
*/
protected final ApplicationContext getApplicationContext() {
return this.context;
}
@Autowired
public void setApplicationContext(ApplicationContext context) {
this.context = context;

View File

@ -20,6 +20,7 @@ import java.util.Collection;
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<H extends HttpSecurityBuilder<H>> e
private LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>();
private List<ChannelProcessor> 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 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> e
public final class ChannelRequestMatcherRegistry extends
AbstractConfigAttributeRequestMatcherRegistry<RequiresChannelUrl> {
private ChannelRequestMatcherRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override
protected RequiresChannelUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {
@ -185,9 +191,6 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> e
public H and() {
return ChannelSecurityConfigurer.this.and();
}
private ChannelRequestMatcherRegistry() {
}
}
public final class RequiresChannelUrl {

View File

@ -21,6 +21,7 @@ import java.util.List;
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<H extends HttpSecurityBuilder<H>>
new HttpSessionCsrfTokenRepository());
private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;
private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<RequestMatcher>();
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 final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
* @since 4.0
*/
public CsrfConfigurer<H> ignoringAntMatchers(String... antPatterns) {
return new IgnoreCsrfProtectionRegistry().antMatchers(antPatterns).and();
return new IgnoreCsrfProtectionRegistry(this.context).antMatchers(antPatterns)
.and();
}
@SuppressWarnings("unchecked")
@ -265,6 +269,13 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
private class IgnoreCsrfProtectionRegistry
extends AbstractRequestMatcherRegistry<IgnoreCsrfProtectionRegistry> {
/**
* @param context
*/
private IgnoreCsrfProtectionRegistry(ApplicationContext context) {
setApplicationContext(context);
}
public CsrfConfigurer<H> and() {
return CsrfConfigurer.this;
}

View File

@ -84,7 +84,7 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
private static final String fullyAuthenticated = "fullyAuthenticated";
private static final String rememberMe = "rememberMe";
private final ExpressionInterceptUrlRegistry REGISTRY = new ExpressionInterceptUrlRegistry();
private final ExpressionInterceptUrlRegistry REGISTRY;
private SecurityExpressionHandler<FilterInvocation> expressionHandler;
@ -92,7 +92,8 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
* Creates a new instance
* @see HttpSecurity#authorizeRequests()
*/
public ExpressionUrlAuthorizationConfigurer() {
public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) {
this.REGISTRY = new ExpressionInterceptUrlRegistry(context);
}
public ExpressionInterceptUrlRegistry getRegistry() {
@ -103,6 +104,13 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
extends
ExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<ExpressionInterceptUrlRegistry, AuthorizedUrl> {
/**
* @param context
*/
private ExpressionInterceptUrlRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override
protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {

View File

@ -19,6 +19,7 @@ import java.util.ArrayList;
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 @@ import org.springframework.util.Assert;
*/
public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractInterceptUrlConfigurer<UrlAuthorizationConfigurer<H>, 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 final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
extends
ExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<StandardInterceptUrlRegistry, AuthorizedUrl> {
/**
* @param context
*/
private StandardInterceptUrlRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override
protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {

View File

@ -45,7 +45,7 @@ public class FilterChainBeanDefinitionParser implements BeanDefinitionParser {
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), "");

View File

@ -71,7 +71,7 @@ public class FilterChainMapBeanDefinitionDecorator implements BeanDefinitionDeco
+ "'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,

View File

@ -165,7 +165,8 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
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 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
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" });

View File

@ -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;
}
}

View File

@ -613,7 +613,7 @@ class HttpConfigurationBuilder {
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);

View File

@ -198,7 +198,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
}
else if (StringUtils.hasText(filterChainPattern)) {
filterChainMatcher = MatcherType.fromElement(element).createMatcher(
filterChainMatcher = MatcherType.fromElement(element).createMatcher(pc,
filterChainPattern, null);
}
else {

View File

@ -18,6 +18,8 @@ package org.springframework.security.config.http;
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 @@ import org.w3c.dom.Element;
*/
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<? extends RequestMatcher> type;
final Class<? extends RequestMatcher> type;
MatcherType(Class<? extends RequestMatcher> 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 enum MatcherType {
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);
if (this == mvc) {
matcherBldr.addPropertyValue("method", method);
}
else {
matcherBldr.addConstructorArgValue(method);
}
if (this == ciRegex) {
matcherBldr.addConstructorArgValue(true);

View File

@ -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 }

View File

@ -34,13 +34,14 @@
<xs:attributeGroup name="request-matcher">
<xs:attribute name="request-matcher" use="required">
<xs:annotation>
<xs:documentation>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.
<xs:documentation>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.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/>
<xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/>
@ -1187,13 +1188,14 @@
</xs:attribute>
<xs:attribute name="request-matcher">
<xs:annotation>
<xs:documentation>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.
<xs:documentation>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.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/>
<xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/>
@ -1557,13 +1559,14 @@
<xs:attributeGroup name="filter-chain-map.attlist">
<xs:attribute name="request-matcher">
<xs:annotation>
<xs:documentation>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.
<xs:documentation>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.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/>
<xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/>
@ -1668,13 +1671,14 @@
</xs:attribute>
<xs:attribute name="request-matcher">
<xs:annotation>
<xs:documentation>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.
<xs:documentation>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.
</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/>
<xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/>

View File

@ -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<HttpSecurity>()).getRegistry()
.apply(new UrlAuthorizationConfigurer<HttpSecurity>(getApplicationContext())).getRegistry()
.antMatchers("/users**","/sessions/**").hasRole("USER")
.antMatchers("/signup").hasRole("ANONYMOUS")
.anyRequest().hasRole("USER");

View File

@ -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";
}
}
}

View File

@ -28,6 +28,7 @@ import org.springframework.http.HttpMethod;
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.core.context.SecurityContextImpl;
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 class AuthorizeRequestsTests {
}
}
@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);

View File

@ -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
* <<mvc-requestmatcher,MvcRequestMatcher>>
* <<headers-csp,Content Security Policy (CSP)>>
* <<headers-hpkp,HTTP Public Key Pinning (HPKP)>>
* <<cors,CORS>>
@ -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]
----
<http>
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
----
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]
----
<http request-matcher="mvc">
<intercept-url pattern="/admin" access="hasRole('ADMIN')"/>
</http>
----
[[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<<nsa-intercept-url,intercept-url>> element using its <<nsa-intercept-url-pattern,pattern>> and <<nsa-intercept-url-method,method>> 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<<nsa-intercept-url,intercept-url>> element using its <<nsa-intercept-url-pattern,pattern>> and <<nsa-intercept-url-method,method>> 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]]

View File

@ -33,6 +33,7 @@ import org.springframework.security.web.FilterInvocation;
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 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource
return new AntPathMatcherEvaluationContextPostProcessor(
(AntPathRequestMatcher) request);
}
if (request instanceof RequestVariablesExtractor) {
return new RequestVariablesExtractorEvaluationContextPostProcessor(
(RequestVariablesExtractor) request);
}
return null;
}
@ -119,4 +124,24 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource
}
}
static class RequestVariablesExtractorEvaluationContextPostProcessor
extends AbstractVariableEvaluationContextPostProcessor {
private final RequestVariablesExtractor matcher;
public RequestVariablesExtractorEvaluationContextPostProcessor(
RequestVariablesExtractor matcher) {
this.matcher = matcher;
}
@Override
Map<String, String> extractVariables(HttpServletRequest request) {
return this.matcher.extractUriTemplateVariables(request);
}
@Override
String postProcessVariableName(String variableName) {
return variableName;
}
}
}

View File

@ -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<String, String> 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.<String, String>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<String, String> extractUriTemplateVariables(
HttpServletRequest request) {
String lookupPath = this.pathHelper.getLookupPathForRequest(request);
if (matches(lookupPath)) {
return this.pathMatcher.extractUriTemplateVariables(
MvcRequestMatcher.this.pattern, lookupPath);
}
return Collections.emptyMap();
}
}
}

View File

@ -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<String, String> extractUriTemplateVariables(HttpServletRequest request);
}

View File

@ -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<String> 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.<String, String>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();
}
}