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.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.filter.DelegatingFilterProxy; 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 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 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; private final boolean allowConfigurersOfSameType;
@ -155,7 +156,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
*/ */
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public <C> void setSharedObject(Class<C> sharedType, C object) { 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 * Gets the shared objects
* @return * @return
*/ */
public Map<Class<Object>, Object> getSharedObjects() { public Map<Class<? extends Object>, Object> getSharedObjects() {
return Collections.unmodifiableMap(this.sharedObjects); return Collections.unmodifiableMap(this.sharedObjects);
} }
@ -300,7 +301,7 @@ public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBui
* @return the possibly modified Object to use * @return the possibly modified Object to use
*/ */
protected <P> P postProcess(P object) { 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.Arrays;
import java.util.List; import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.configurers.AbstractConfigAttributeRequestMatcherRegistry; 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.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RegexRequestMatcher; import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; 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 * 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> { public abstract class AbstractRequestMatcherRegistry<C> {
private static final RequestMatcher ANY_REQUEST = AnyRequestMatcher.INSTANCE; private static final RequestMatcher ANY_REQUEST = AnyRequestMatcher.INSTANCE;
private ApplicationContext context;
protected final void setApplicationContext(ApplicationContext context) {
this.context = context;
}
/** /**
* Maps any request. * Maps any request.
* *
@ -92,6 +101,57 @@ public abstract class AbstractRequestMatcherRegistry<C> {
return chainRequestMatchers(RequestMatchers.antMatchers(antPatterns)); 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 * Maps a {@link List} of
* {@link org.springframework.security.web.util.matcher.RegexRequestMatcher} * {@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.Filter;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder; 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.DefaultSecurityFilterChain;
import org.springframework.security.web.PortMapper; import org.springframework.security.web.PortMapper;
import org.springframework.security.web.PortMapperImpl; 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.context.AbstractSecurityWebApplicationInitializer;
import org.springframework.security.web.session.HttpSessionEventPublisher; import org.springframework.security.web.session.HttpSessionEventPublisher;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@ -113,7 +115,7 @@ public final class HttpSecurity extends
AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity> AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
implements SecurityBuilder<DefaultSecurityFilterChain>, implements SecurityBuilder<DefaultSecurityFilterChain>,
HttpSecurityBuilder<HttpSecurity> { HttpSecurityBuilder<HttpSecurity> {
private final RequestMatcherConfigurer requestMatcherConfigurer = new RequestMatcherConfigurer(); private final RequestMatcherConfigurer requestMatcherConfigurer;
private List<Filter> filters = new ArrayList<Filter>(); private List<Filter> filters = new ArrayList<Filter>();
private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE; private RequestMatcher requestMatcher = AnyRequestMatcher.INSTANCE;
private FilterComparator comparator = new FilterComparator(); 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 * @param sharedObjects the shared Objects to initialize the {@link HttpSecurity} with
* @see WebSecurityConfiguration * @see WebSecurityConfiguration
*/ */
@SuppressWarnings("unchecked")
public HttpSecurity(ObjectPostProcessor<Object> objectPostProcessor, public HttpSecurity(ObjectPostProcessor<Object> objectPostProcessor,
AuthenticationManagerBuilder authenticationBuilder, AuthenticationManagerBuilder authenticationBuilder,
Map<Class<Object>, Object> sharedObjects) { Map<Class<? extends Object>, Object> sharedObjects) {
super(objectPostProcessor); super(objectPostProcessor);
Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null"); Assert.notNull(authenticationBuilder, "authenticationBuilder cannot be null");
setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder); setSharedObject(AuthenticationManagerBuilder.class, authenticationBuilder);
for (Map.Entry<Class<Object>, Object> entry : sharedObjects.entrySet()) { for (Map.Entry<Class<? extends Object>, Object> entry : sharedObjects
setSharedObject(entry.getKey(), entry.getValue()); .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() public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests()
throws Exception { throws Exception {
return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>()) ApplicationContext context = getContext();
return getOrApply(new ExpressionUrlAuthorizationConfigurer<HttpSecurity>(context))
.getRegistry(); .getRegistry();
} }
@ -710,7 +722,8 @@ public final class HttpSecurity extends
* @throws Exception * @throws Exception
*/ */
public CsrfConfigurer<HttpSecurity> csrf() 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() public ChannelSecurityConfigurer<HttpSecurity>.ChannelRequestMatcherRegistry requiresChannel()
throws Exception { 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 public final class RequestMatcherConfigurer extends
AbstractRequestMatcherRegistry<RequestMatcherConfigurer> { AbstractRequestMatcherRegistry<RequestMatcherConfigurer> {
private List<RequestMatcher> matchers = new ArrayList<RequestMatcher>(); private List<RequestMatcher> matchers = new ArrayList<RequestMatcher>();
/**
* @param context
*/
private RequestMatcherConfigurer(ApplicationContext context) {
setApplicationContext(context);
}
protected RequestMatcherConfigurer chainRequestMatchers( protected RequestMatcherConfigurer chainRequestMatchers(
List<RequestMatcher> requestMatchers) { List<RequestMatcher> requestMatchers) {
matchers.addAll(requestMatchers); matchers.addAll(requestMatchers);
@ -1259,8 +1282,6 @@ public final class HttpSecurity extends
return HttpSecurity.this; 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.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; 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 List<SecurityBuilder<? extends SecurityFilterChain>> securityFilterChainBuilders = new ArrayList<SecurityBuilder<? extends SecurityFilterChain>>();
private final IgnoredRequestConfigurer ignoredRequestRegistry = new IgnoredRequestConfigurer(); private IgnoredRequestConfigurer ignoredRequestRegistry;
private FilterSecurityInterceptor filterSecurityInterceptor; private FilterSecurityInterceptor filterSecurityInterceptor;
@ -316,6 +317,10 @@ public final class WebSecurity extends
public final class IgnoredRequestConfigurer extends public final class IgnoredRequestConfigurer extends
AbstractRequestMatcherRegistry<IgnoredRequestConfigurer> { AbstractRequestMatcherRegistry<IgnoredRequestConfigurer> {
private IgnoredRequestConfigurer(ApplicationContext context) {
setApplicationContext(context);
}
@Override @Override
protected IgnoredRequestConfigurer chainRequestMatchers( protected IgnoredRequestConfigurer chainRequestMatchers(
List<RequestMatcher> requestMatchers) { List<RequestMatcher> requestMatchers) {
@ -329,13 +334,13 @@ public final class WebSecurity extends
public WebSecurity and() { public WebSecurity and() {
return WebSecurity.this; return WebSecurity.this;
} }
private IgnoredRequestConfigurer() {
}
} }
@Override
public void setApplicationContext(ApplicationContext applicationContext) public void setApplicationContext(ApplicationContext applicationContext)
throws BeansException { 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 // @formatter:on
/**
* Gets the ApplicationContext
* @return the context
*/
protected final ApplicationContext getApplicationContext() {
return this.context;
}
@Autowired @Autowired
public void setApplicationContext(ApplicationContext context) { public void setApplicationContext(ApplicationContext context) {
this.context = context; this.context = context;

View File

@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig; import org.springframework.security.access.SecurityConfig;
import org.springframework.security.config.annotation.ObjectPostProcessor; 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 LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>> requestMap = new LinkedHashMap<RequestMatcher, Collection<ConfigAttribute>>();
private List<ChannelProcessor> channelProcessors; private List<ChannelProcessor> channelProcessors;
private final ChannelRequestMatcherRegistry REGISTRY = new ChannelRequestMatcherRegistry(); private final ChannelRequestMatcherRegistry REGISTRY;
/** /**
* Creates a new instance * Creates a new instance
* @see HttpSecurity#requiresChannel() * @see HttpSecurity#requiresChannel()
*/ */
public ChannelSecurityConfigurer() { public ChannelSecurityConfigurer(ApplicationContext context) {
this.REGISTRY = new ChannelRequestMatcherRegistry(context);
} }
public ChannelRequestMatcherRegistry getRegistry() { public ChannelRequestMatcherRegistry getRegistry() {
@ -146,6 +148,10 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> e
public final class ChannelRequestMatcherRegistry extends public final class ChannelRequestMatcherRegistry extends
AbstractConfigAttributeRequestMatcherRegistry<RequiresChannelUrl> { AbstractConfigAttributeRequestMatcherRegistry<RequiresChannelUrl> {
private ChannelRequestMatcherRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override @Override
protected RequiresChannelUrl chainRequestMatchersInternal( protected RequiresChannelUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) { List<RequestMatcher> requestMatchers) {
@ -185,9 +191,6 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> e
public H and() { public H and() {
return ChannelSecurityConfigurer.this.and(); return ChannelSecurityConfigurer.this.and();
} }
private ChannelRequestMatcherRegistry() {
}
} }
public final class RequiresChannelUrl { public final class RequiresChannelUrl {

View File

@ -21,6 +21,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry; import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder; import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
@ -78,12 +79,14 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
new HttpSessionCsrfTokenRepository()); new HttpSessionCsrfTokenRepository());
private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER; private RequestMatcher requireCsrfProtectionMatcher = CsrfFilter.DEFAULT_CSRF_MATCHER;
private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<RequestMatcher>(); private List<RequestMatcher> ignoredCsrfProtectionMatchers = new ArrayList<RequestMatcher>();
private final ApplicationContext context;
/** /**
* Creates a new instance * Creates a new instance
* @see HttpSecurity#csrf() * @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 * @since 4.0
*/ */
public CsrfConfigurer<H> ignoringAntMatchers(String... antPatterns) { public CsrfConfigurer<H> ignoringAntMatchers(String... antPatterns) {
return new IgnoreCsrfProtectionRegistry().antMatchers(antPatterns).and(); return new IgnoreCsrfProtectionRegistry(this.context).antMatchers(antPatterns)
.and();
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -265,6 +269,13 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
private class IgnoreCsrfProtectionRegistry private class IgnoreCsrfProtectionRegistry
extends AbstractRequestMatcherRegistry<IgnoreCsrfProtectionRegistry> { extends AbstractRequestMatcherRegistry<IgnoreCsrfProtectionRegistry> {
/**
* @param context
*/
private IgnoreCsrfProtectionRegistry(ApplicationContext context) {
setApplicationContext(context);
}
public CsrfConfigurer<H> and() { public CsrfConfigurer<H> and() {
return CsrfConfigurer.this; 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 fullyAuthenticated = "fullyAuthenticated";
private static final String rememberMe = "rememberMe"; private static final String rememberMe = "rememberMe";
private final ExpressionInterceptUrlRegistry REGISTRY = new ExpressionInterceptUrlRegistry(); private final ExpressionInterceptUrlRegistry REGISTRY;
private SecurityExpressionHandler<FilterInvocation> expressionHandler; private SecurityExpressionHandler<FilterInvocation> expressionHandler;
@ -92,7 +92,8 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
* Creates a new instance * Creates a new instance
* @see HttpSecurity#authorizeRequests() * @see HttpSecurity#authorizeRequests()
*/ */
public ExpressionUrlAuthorizationConfigurer() { public ExpressionUrlAuthorizationConfigurer(ApplicationContext context) {
this.REGISTRY = new ExpressionInterceptUrlRegistry(context);
} }
public ExpressionInterceptUrlRegistry getRegistry() { public ExpressionInterceptUrlRegistry getRegistry() {
@ -103,6 +104,13 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
extends extends
ExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<ExpressionInterceptUrlRegistry, AuthorizedUrl> { ExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<ExpressionInterceptUrlRegistry, AuthorizedUrl> {
/**
* @param context
*/
private ExpressionInterceptUrlRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override @Override
protected final AuthorizedUrl chainRequestMatchersInternal( protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) { List<RequestMatcher> requestMatchers) {

View File

@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter; import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.ConfigAttribute;
@ -86,7 +87,11 @@ import org.springframework.util.Assert;
*/ */
public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>> extends public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>> extends
AbstractInterceptUrlConfigurer<UrlAuthorizationConfigurer<H>, H> { 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 * The StandardInterceptUrlRegistry is what users will interact with after applying
@ -114,6 +119,13 @@ public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
extends extends
ExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<StandardInterceptUrlRegistry, AuthorizedUrl> { ExpressionUrlAuthorizationConfigurer<H>.AbstractInterceptUrlRegistry<StandardInterceptUrlRegistry, AuthorizedUrl> {
/**
* @param context
*/
private StandardInterceptUrlRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override @Override
protected final AuthorizedUrl chainRequestMatchersInternal( protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) { List<RequestMatcher> requestMatchers) {

View File

@ -45,7 +45,7 @@ public class FilterChainBeanDefinitionParser implements BeanDefinitionParser {
if (StringUtils.hasText(path)) { if (StringUtils.hasText(path)) {
Assert.isTrue(!StringUtils.hasText(requestMatcher), ""); Assert.isTrue(!StringUtils.hasText(requestMatcher), "");
builder.addConstructorArgValue(matcherType.createMatcher(path, null)); builder.addConstructorArgValue(matcherType.createMatcher(pc, path, null));
} }
else { else {
Assert.isTrue(StringUtils.hasText(requestMatcher), ""); Assert.isTrue(StringUtils.hasText(requestMatcher), "");

View File

@ -71,7 +71,7 @@ public class FilterChainMapBeanDefinitionDecorator implements BeanDefinitionDeco
+ "'must not be empty", elt); + "'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)) { if (filters.equals(HttpSecurityBeanDefinitionParser.OPT_FILTERS_NONE)) {
securityFilterChains.add(createSecurityFilterChain(matcher, securityFilterChains.add(createSecurityFilterChain(matcher,

View File

@ -165,7 +165,8 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
method = null; method = null;
} }
BeanDefinition matcher = matcherType.createMatcher(path, method); BeanDefinition matcher = matcherType.createMatcher(parserContext, path,
method);
BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
.rootBeanDefinition(SecurityConfig.class); .rootBeanDefinition(SecurityConfig.class);
@ -194,7 +195,8 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
if (addAuthenticatedAll && filterInvocationDefinitionMap.isEmpty()) { if (addAuthenticatedAll && filterInvocationDefinitionMap.isEmpty()) {
BeanDefinition matcher = matcherType.createMatcher("/**", null); BeanDefinition matcher = matcherType.createMatcher(parserContext, "/**",
null);
BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
.rootBeanDefinition(SecurityConfig.class); .rootBeanDefinition(SecurityConfig.class);
attributeBuilder.addConstructorArgValue(new String[] { "authenticated" }); 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); String requiredChannel = urlElt.getAttribute(ATT_REQUIRES_CHANNEL);
if (StringUtils.hasText(requiredChannel)) { if (StringUtils.hasText(requiredChannel)) {
BeanDefinition matcher = matcherType.createMatcher(path, method); BeanDefinition matcher = matcherType.createMatcher(pc, path, method);
RootBeanDefinition channelAttributes = new RootBeanDefinition( RootBeanDefinition channelAttributes = new RootBeanDefinition(
ChannelAttributeFactory.class); ChannelAttributeFactory.class);

View File

@ -198,7 +198,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
} }
else if (StringUtils.hasText(filterChainPattern)) { else if (StringUtils.hasText(filterChainPattern)) {
filterChainMatcher = MatcherType.fromElement(element).createMatcher( filterChainMatcher = MatcherType.fromElement(element).createMatcher(pc,
filterChainPattern, null); filterChainPattern, null);
} }
else { 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.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.RootBeanDefinition; 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.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher;
import org.springframework.security.web.util.matcher.RegexRequestMatcher; import org.springframework.security.web.util.matcher.RegexRequestMatcher;
@ -33,17 +35,20 @@ import org.w3c.dom.Element;
*/ */
public enum MatcherType { public enum MatcherType {
ant(AntPathRequestMatcher.class), regex(RegexRequestMatcher.class), ciRegex( 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 static final String ATT_MATCHER_TYPE = "request-matcher";
private final Class<? extends RequestMatcher> type; final Class<? extends RequestMatcher> type;
MatcherType(Class<? extends RequestMatcher> type) { MatcherType(Class<? extends RequestMatcher> type) {
this.type = 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) { if (("/**".equals(path) || "**".equals(path)) && method == null) {
return new RootBeanDefinition(AnyRequestMatcher.class); return new RootBeanDefinition(AnyRequestMatcher.class);
} }
@ -51,8 +56,28 @@ public enum MatcherType {
BeanDefinitionBuilder matcherBldr = BeanDefinitionBuilder BeanDefinitionBuilder matcherBldr = BeanDefinitionBuilder
.rootBeanDefinition(type); .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(path);
matcherBldr.addConstructorArgValue(method); if (this == mvc) {
matcherBldr.addPropertyValue("method", method);
}
else {
matcherBldr.addConstructorArgValue(method);
}
if (this == ciRegex) { if (this == ciRegex) {
matcherBldr.addConstructorArgValue(true); matcherBldr.addConstructorArgValue(true);

View File

@ -12,8 +12,8 @@ base64 =
## Whether a string should be base64 encoded ## Whether a string should be base64 encoded
attribute base64 {xsd:boolean} attribute base64 {xsd:boolean}
request-matcher = 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. ## 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 {"ant" | "regex" | "ciRegex"} attribute request-matcher {"mvc" | "ant" | "regex" | "ciRegex"}
port = port =
## Specifies an IP port number. Used to configure an embedded LDAP server, for example. ## Specifies an IP port number. Used to configure an embedded LDAP server, for example.
attribute port { xsd:positiveInteger } attribute port { xsd:positiveInteger }

View File

@ -34,13 +34,14 @@
<xs:attributeGroup name="request-matcher"> <xs:attributeGroup name="request-matcher">
<xs:attribute name="request-matcher" use="required"> <xs:attribute name="request-matcher" use="required">
<xs:annotation> <xs:annotation>
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'ant' <xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
(for ant path patterns), 'regex' for regular expressions and 'ciRegex' for (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
case-insensitive regular expressions. and 'ciRegex' for case-insensitive regular expressions.
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
<xs:simpleType> <xs:simpleType>
<xs:restriction base="xs:token"> <xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/> <xs:enumeration value="ant"/>
<xs:enumeration value="regex"/> <xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/> <xs:enumeration value="ciRegex"/>
@ -1187,13 +1188,14 @@
</xs:attribute> </xs:attribute>
<xs:attribute name="request-matcher"> <xs:attribute name="request-matcher">
<xs:annotation> <xs:annotation>
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'ant' <xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
(for ant path patterns), 'regex' for regular expressions and 'ciRegex' for (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
case-insensitive regular expressions. and 'ciRegex' for case-insensitive regular expressions.
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
<xs:simpleType> <xs:simpleType>
<xs:restriction base="xs:token"> <xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/> <xs:enumeration value="ant"/>
<xs:enumeration value="regex"/> <xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/> <xs:enumeration value="ciRegex"/>
@ -1557,13 +1559,14 @@
<xs:attributeGroup name="filter-chain-map.attlist"> <xs:attributeGroup name="filter-chain-map.attlist">
<xs:attribute name="request-matcher"> <xs:attribute name="request-matcher">
<xs:annotation> <xs:annotation>
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'ant' <xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
(for ant path patterns), 'regex' for regular expressions and 'ciRegex' for (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
case-insensitive regular expressions. and 'ciRegex' for case-insensitive regular expressions.
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
<xs:simpleType> <xs:simpleType>
<xs:restriction base="xs:token"> <xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/> <xs:enumeration value="ant"/>
<xs:enumeration value="regex"/> <xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/> <xs:enumeration value="ciRegex"/>
@ -1668,13 +1671,14 @@
</xs:attribute> </xs:attribute>
<xs:attribute name="request-matcher"> <xs:attribute name="request-matcher">
<xs:annotation> <xs:annotation>
<xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'ant' <xs:documentation>Defines the strategy use for matching incoming requests. Currently the options are 'mvc'
(for ant path patterns), 'regex' for regular expressions and 'ciRegex' for (for Spring MVC matcher), 'ant' (for ant path patterns), 'regex' for regular expressions
case-insensitive regular expressions. and 'ciRegex' for case-insensitive regular expressions.
</xs:documentation> </xs:documentation>
</xs:annotation> </xs:annotation>
<xs:simpleType> <xs:simpleType>
<xs:restriction base="xs:token"> <xs:restriction base="xs:token">
<xs:enumeration value="mvc"/>
<xs:enumeration value="ant"/> <xs:enumeration value="ant"/>
<xs:enumeration value="regex"/> <xs:enumeration value="regex"/>
<xs:enumeration value="ciRegex"/> <xs:enumeration value="ciRegex"/>

View File

@ -25,7 +25,7 @@ public class DisableUseExpressionsConfig extends BaseWebConfig {
protected void configure(HttpSecurity http) throws Exception { protected void configure(HttpSecurity http) throws Exception {
// This config is also on UrlAuthorizationConfigurer javadoc // This config is also on UrlAuthorizationConfigurer javadoc
http http
.apply(new UrlAuthorizationConfigurer<HttpSecurity>()).getRegistry() .apply(new UrlAuthorizationConfigurer<HttpSecurity>(getApplicationContext())).getRegistry()
.antMatchers("/users**","/sessions/**").hasRole("USER") .antMatchers("/users**","/sessions/**").hasRole("USER")
.antMatchers("/signup").hasRole("ANONYMOUS") .antMatchers("/signup").hasRole("ANONYMOUS")
.anyRequest().hasRole("USER"); .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.access.SecurityConfig
import org.springframework.security.crypto.codec.Base64 import org.springframework.security.crypto.codec.Base64
import org.springframework.security.web.access.intercept.FilterSecurityInterceptor 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 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 static class Id {
public boolean isOne(int i) { public boolean isOne(int i) {
return i == 1; return i == 1;
@ -207,4 +276,12 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
String toEncode = username + ':' + password String toEncode = username + ':' + password
request.addHeader('Authorization','Basic ' + new String(Base64.encode(toEncode.getBytes('UTF-8')))) 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.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse; 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.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl; import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 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.FilterChainProxy;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 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.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat; 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) { public void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext(); this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs); this.context.register(configs);
this.context.setServletContext(new MockServletContext());
this.context.refresh(); this.context.refresh();
this.context.getAutowireCapableBeanFactory().autowireBean(this); 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` * Ability to add a `Filter` at a specific location in the chain using `HttpSecurity.addFilterAt`
=== Web Application Security Improvements === Web Application Security Improvements
* <<mvc-requestmatcher,MvcRequestMatcher>>
* <<headers-csp,Content Security Policy (CSP)>> * <<headers-csp,Content Security Policy (CSP)>>
* <<headers-hpkp,HTTP Public Key Pinning (HPKP)>> * <<headers-hpkp,HTTP Public Key Pinning (HPKP)>>
* <<cors,CORS>> * <<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. 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]] [[mvc-authentication-principal]]
=== @AuthenticationPrincipal === @AuthenticationPrincipal
@ -7403,7 +7475,7 @@ Sets the realm name used for basic authentication (if enabled). Corresponds to t
[[nsa-http-request-matcher]] [[nsa-http-request-matcher]]
* **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]] [[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.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.security.web.util.matcher.RequestVariablesExtractor;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -96,6 +97,10 @@ public final class ExpressionBasedFilterInvocationSecurityMetadataSource
return new AntPathMatcherEvaluationContextPostProcessor( return new AntPathMatcherEvaluationContextPostProcessor(
(AntPathRequestMatcher) request); (AntPathRequestMatcher) request);
} }
if (request instanceof RequestVariablesExtractor) {
return new RequestVariablesExtractorEvaluationContextPostProcessor(
(RequestVariablesExtractor) request);
}
return null; 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();
}
}