MvcRequestMatcher servletPath / JavaConfig

Issue: gh-3987
This commit is contained in:
Rob Winch 2016-07-21 00:40:43 -05:00
parent 050198e51b
commit 3befb1c8a6
22 changed files with 1302 additions and 62 deletions

View File

@ -17,16 +17,24 @@ package org.springframework.security.config.annotation.web;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.ObjectPostProcessor;
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.util.ClassUtils;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
/**
@ -48,6 +56,15 @@ public abstract class AbstractRequestMatcherRegistry<C> {
this.context = context;
}
/**
* Gets the {@link ApplicationContext}
*
* @return the {@link ApplicationContext}
*/
protected final ApplicationContext getApplicationContext() {
return this.context;
}
/**
* Maps any request.
*
@ -117,9 +134,7 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* Spring MVC
* @return the object that is chained after creating the {@link RequestMatcher}.
*/
public C mvcMatchers(String... mvcPatterns) {
return mvcMatchers(null, mvcPatterns);
}
public abstract C mvcMatchers(String... mvcPatterns);
/**
* <p>
@ -138,18 +153,37 @@ public abstract class AbstractRequestMatcherRegistry<C> {
* Spring MVC
* @return the object that is chained after creating the {@link RequestMatcher}.
*/
public C mvcMatchers(HttpMethod method, String... mvcPatterns) {
public abstract C mvcMatchers(HttpMethod method, String... mvcPatterns);
/**
* Creates {@link MvcRequestMatcher} instances for the method and patterns passed in
*
* @param method the HTTP method to use or null if any should be used
* @param mvcPatterns the Spring MVC patterns to match on
* @return a List of {@link MvcRequestMatcher} instances
*/
protected final List<MvcRequestMatcher> createMvcMatchers(HttpMethod method,
String... mvcPatterns) {
boolean isServlet30 = ClassUtils.isPresent("javax.servlet.ServletRegistration", getClass().getClassLoader());
ObjectPostProcessor<Object> opp = this.context.getBean(ObjectPostProcessor.class);
HandlerMappingIntrospector introspector = new HandlerMappingIntrospector(
this.context);
List<RequestMatcher> matchers = new ArrayList<RequestMatcher>(mvcPatterns.length);
List<MvcRequestMatcher> matchers = new ArrayList<MvcRequestMatcher>(
mvcPatterns.length);
for (String mvcPattern : mvcPatterns) {
MvcRequestMatcher matcher = new MvcRequestMatcher(introspector, mvcPattern);
MvcRequestMatcher matcher;
if(isServlet30) {
matcher = new ServletPathValidatingtMvcRequestMatcher(introspector, mvcPattern);
opp.postProcess(matcher);
} else {
matcher = new MvcRequestMatcher(introspector, mvcPattern);
}
if (method != null) {
matcher.setMethod(method);
}
matchers.add(matcher);
}
return chainRequestMatchers(matchers);
return matchers;
}
/**
@ -281,4 +315,49 @@ public abstract class AbstractRequestMatcherRegistry<C> {
private RequestMatchers() {
}
}
static class ServletPathValidatingtMvcRequestMatcher extends MvcRequestMatcher implements SmartInitializingSingleton, ServletContextAware {
private ServletContext servletContext;
/**
* @param introspector
* @param pattern
*/
public ServletPathValidatingtMvcRequestMatcher(HandlerMappingIntrospector introspector,
String pattern) {
super(introspector, pattern);
}
/* (non-Javadoc)
* @see org.springframework.beans.factory.SmartInitializingSingleton#afterSingletonsInstantiated()
*/
@Override
public void afterSingletonsInstantiated() {
if(getServletPath() != null) {
return;
}
Collection<? extends ServletRegistration> registrations = servletContext.getServletRegistrations().values();
for(ServletRegistration registration : registrations) {
Collection<String> mappings = registration.getMappings();
for(String mapping : mappings) {
if(mapping.startsWith("/") && mapping.length() > 1) {
throw new IllegalStateException(
"servletPath must not be null for mvcPattern \"" + getMvcPattern()
+ "\" when providing a servlet mapping of "
+ mapping + " for servlet "
+ registration.getClassName());
}
}
}
}
/* (non-Javadoc)
* @see org.springframework.web.context.ServletContextAware#setServletContext(javax.servlet.ServletContext)
*/
@Override
public void setServletContext(ServletContext servletContext) {
this.servletContext = servletContext;
}
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.annotation.web;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import javax.servlet.Registration;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner;
import org.mockito.stubbing.Answer;
import org.springframework.mock.web.MockServletContext;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.ServletPathValidatingtMvcRequestMatcher;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
*/
@RunWith(MockitoJUnitRunner.class)
public class AbstractRequestMatcherRegistryTests {
@Mock
HandlerMappingIntrospector introspector;
ServletPathValidatingtMvcRequestMatcher matcher;
ServletContext servletContext;
@Before
public void setup() {
servletContext = spy(new MockServletContext());
matcher = new ServletPathValidatingtMvcRequestMatcher(introspector, "/foo");
matcher.setServletContext(servletContext);
}
@Test(expected = IllegalStateException.class)
public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedFailsWithSpringServlet() {
setMappings("/spring");
matcher.afterSingletonsInstantiated();
}
@Test
public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedWithSpringServlet() {
matcher.setServletPath("/spring");
setMappings("/spring");
matcher.afterSingletonsInstantiated();
}
@Test
public void servletPathValidatingtMvcRequestMatcherAfterSingletonsIntantiatedDefaultServlet() {
setMappings("/");
matcher.afterSingletonsInstantiated();
}
private void setMappings(String... mappings) {
final ServletRegistration registration = mock(ServletRegistration.class);
when(registration.getMappings()).thenReturn(Arrays.asList(mappings));
Answer<Map<String, ? extends ServletRegistration>> answer = new Answer<Map<String, ? extends ServletRegistration>>() {
@Override
public Map<String, ? extends ServletRegistration> answer(InvocationOnMock invocation) throws Throwable {
return Collections.<String, ServletRegistration>singletonMap("spring", registration);
}
};
when(servletContext.getServletRegistrations()).thenAnswer(answer);
}
}

View File

@ -24,6 +24,7 @@ import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
@ -1269,16 +1270,45 @@ public final class HttpSecurity extends
return requestMatcher(new RegexRequestMatcher(pattern, null));
}
/**
* An extension to {@link RequestMatcherConfigurer} that allows optionally configuring
* the servlet path.
*
* @author Rob Winch
*/
public final class MvcMatchersRequestMatcherConfigurer extends RequestMatcherConfigurer {
/**
* Creates a new instance
* @param context the {@link ApplicationContext} to use
* @param matchers the {@link MvcRequestMatcher} instances to set the servlet path
* on if {@link #servletPath(String)} is set.
*/
private MvcMatchersRequestMatcherConfigurer(ApplicationContext context,
List<MvcRequestMatcher> matchers) {
super(context);
this.matchers = new ArrayList<RequestMatcher>(matchers);
}
public RequestMatcherConfigurer servletPath(String servletPath) {
for (RequestMatcher matcher : this.matchers) {
((MvcRequestMatcher) matcher).setServletPath(servletPath);
}
return this;
}
}
/**
* Allows mapping HTTP requests that this {@link HttpSecurity} will be used for
*
* @author Rob Winch
* @since 3.2
*/
public final class RequestMatcherConfigurer extends
AbstractRequestMatcherRegistry<RequestMatcherConfigurer> {
public class RequestMatcherConfigurer
extends AbstractRequestMatcherRegistry<RequestMatcherConfigurer> {
private List<RequestMatcher> matchers = new ArrayList<RequestMatcher>();
protected List<RequestMatcher> matchers = new ArrayList<RequestMatcher>();
/**
* @param context
@ -1287,13 +1317,31 @@ public final class HttpSecurity extends
setApplicationContext(context);
}
@Override
public MvcMatchersRequestMatcherConfigurer mvcMatchers(HttpMethod method,
String... mvcPatterns) {
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
setMatchers(mvcMatchers);
return new MvcMatchersRequestMatcherConfigurer(getContext(), mvcMatchers);
}
@Override
public MvcMatchersRequestMatcherConfigurer mvcMatchers(String... patterns) {
return mvcMatchers(null, patterns);
}
@Override
protected RequestMatcherConfigurer chainRequestMatchers(
List<RequestMatcher> requestMatchers) {
matchers.addAll(requestMatchers);
requestMatcher(new OrRequestMatcher(matchers));
setMatchers(requestMatchers);
return this;
}
private void setMatchers(List<? extends RequestMatcher> requestMatchers) {
this.matchers.addAll(requestMatchers);
requestMatcher(new OrRequestMatcher(this.matchers));
}
/**
* Return the {@link HttpSecurity} for further customizations
*

View File

@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder;
import org.springframework.security.config.annotation.ObjectPostProcessor;
@ -48,6 +49,7 @@ import org.springframework.security.web.access.intercept.FilterSecurityIntercept
import org.springframework.security.web.debug.DebugFilter;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.DelegatingFilterProxy;
@ -307,6 +309,30 @@ public final class WebSecurity extends
return result;
}
/**
* An {@link IgnoredRequestConfigurer} that allows optionally configuring the
* {@link MvcRequestMatcher#setMethod(HttpMethod)}
*
* @author Rob Winch
*/
public final class MvcMatchersIgnoredRequestConfigurer
extends IgnoredRequestConfigurer {
private final List<MvcRequestMatcher> mvcMatchers;
private MvcMatchersIgnoredRequestConfigurer(ApplicationContext context,
List<MvcRequestMatcher> mvcMatchers) {
super(context);
this.mvcMatchers = mvcMatchers;
}
public IgnoredRequestConfigurer servletPath(String servletPath) {
for (MvcRequestMatcher matcher : this.mvcMatchers) {
matcher.setServletPath(servletPath);
}
return this;
}
}
/**
* Allows registering {@link RequestMatcher} instances that should be ignored by
* Spring Security.
@ -314,17 +340,31 @@ public final class WebSecurity extends
* @author Rob Winch
* @since 3.2
*/
public final class IgnoredRequestConfigurer extends
AbstractRequestMatcherRegistry<IgnoredRequestConfigurer> {
public class IgnoredRequestConfigurer
extends AbstractRequestMatcherRegistry<IgnoredRequestConfigurer> {
private IgnoredRequestConfigurer(ApplicationContext context) {
setApplicationContext(context);
}
@Override
public MvcMatchersIgnoredRequestConfigurer mvcMatchers(HttpMethod method,
String... mvcPatterns) {
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
WebSecurity.this.ignoredRequests.addAll(mvcMatchers);
return new MvcMatchersIgnoredRequestConfigurer(getApplicationContext(),
mvcMatchers);
}
@Override
public MvcMatchersIgnoredRequestConfigurer mvcMatchers(String... mvcPatterns) {
return mvcMatchers(null, mvcPatterns);
}
@Override
protected IgnoredRequestConfigurer chainRequestMatchers(
List<RequestMatcher> requestMatchers) {
ignoredRequests.addAll(requestMatchers);
WebSecurity.this.ignoredRequests.addAll(requestMatchers);
return this;
}

View File

@ -21,6 +21,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.config.annotation.ObjectPostProcessor;
@ -37,6 +38,7 @@ import org.springframework.security.web.access.channel.RetryWithHttpEntryPoint;
import org.springframework.security.web.access.channel.RetryWithHttpsEntryPoint;
import org.springframework.security.web.access.channel.SecureChannelProcessor;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
/**
@ -136,7 +138,7 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> e
}
private ChannelRequestMatcherRegistry addAttribute(String attribute,
List<RequestMatcher> matchers) {
List<? extends RequestMatcher> matchers) {
for (RequestMatcher matcher : matchers) {
Collection<ConfigAttribute> attrs = Arrays
.<ConfigAttribute> asList(new SecurityConfig(attribute));
@ -145,13 +147,25 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> e
return REGISTRY;
}
public final class ChannelRequestMatcherRegistry extends
AbstractConfigAttributeRequestMatcherRegistry<RequiresChannelUrl> {
public final class ChannelRequestMatcherRegistry
extends AbstractConfigAttributeRequestMatcherRegistry<RequiresChannelUrl> {
private ChannelRequestMatcherRegistry(ApplicationContext context) {
setApplicationContext(context);
}
@Override
public MvcMatchersRequiresChannelUrl mvcMatchers(HttpMethod method,
String... mvcPatterns) {
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
return new MvcMatchersRequiresChannelUrl(mvcMatchers);
}
@Override
public MvcMatchersRequiresChannelUrl mvcMatchers(String... patterns) {
return mvcMatchers(null, patterns);
}
@Override
protected RequiresChannelUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {
@ -193,10 +207,24 @@ public final class ChannelSecurityConfigurer<H extends HttpSecurityBuilder<H>> e
}
}
public final class RequiresChannelUrl {
private List<RequestMatcher> requestMatchers;
public final class MvcMatchersRequiresChannelUrl extends RequiresChannelUrl {
private RequiresChannelUrl(List<RequestMatcher> requestMatchers) {
private MvcMatchersRequiresChannelUrl(List<MvcRequestMatcher> matchers) {
super(matchers);
}
public RequiresChannelUrl servletPath(String servletPath) {
for (RequestMatcher matcher : this.requestMatchers) {
((MvcRequestMatcher) matcher).setServletPath(servletPath);
}
return this;
}
}
public class RequiresChannelUrl {
protected List<? extends RequestMatcher> requestMatchers;
private RequiresChannelUrl(List<? extends RequestMatcher> requestMatchers) {
this.requestMatchers = requestMatchers;
}

View File

@ -22,6 +22,7 @@ import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
@ -36,6 +37,7 @@ import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository;
import org.springframework.security.web.csrf.LazyCsrfTokenRepository;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.session.InvalidSessionAccessDeniedHandler;
import org.springframework.security.web.session.InvalidSessionStrategy;
import org.springframework.security.web.util.matcher.AndRequestMatcher;
@ -276,6 +278,20 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
setApplicationContext(context);
}
@Override
public MvcMatchersIgnoreCsrfProtectionRegistry mvcMatchers(HttpMethod method,
String... mvcPatterns) {
List<MvcRequestMatcher> mvcMatchers = createMvcMatchers(method, mvcPatterns);
CsrfConfigurer.this.ignoredCsrfProtectionMatchers.addAll(mvcMatchers);
return new MvcMatchersIgnoreCsrfProtectionRegistry(getApplicationContext(),
mvcMatchers);
}
@Override
public MvcMatchersIgnoreCsrfProtectionRegistry mvcMatchers(String... mvcPatterns) {
return mvcMatchers(null, mvcPatterns);
}
public CsrfConfigurer<H> and() {
return CsrfConfigurer.this;
}
@ -287,4 +303,28 @@ public final class CsrfConfigurer<H extends HttpSecurityBuilder<H>>
return this;
}
}
/**
* An {@link IgnoreCsrfProtectionRegistry} that allows optionally configuring the
* {@link MvcRequestMatcher#setMethod(HttpMethod)}
*
* @author Rob Winch
*/
private final class MvcMatchersIgnoreCsrfProtectionRegistry
extends IgnoreCsrfProtectionRegistry {
private final List<MvcRequestMatcher> mvcMatchers;
private MvcMatchersIgnoreCsrfProtectionRegistry(ApplicationContext context,
List<MvcRequestMatcher> mvcMatchers) {
super(context);
this.mvcMatchers = mvcMatchers;
}
public IgnoreCsrfProtectionRegistry servletPath(String servletPath) {
for (MvcRequestMatcher matcher : this.mvcMatchers) {
matcher.setServletPath(servletPath);
}
return this;
}
}
}

View File

@ -21,6 +21,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
@ -34,6 +35,7 @@ import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.expression.WebExpressionVoter;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@ -111,6 +113,16 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
setApplicationContext(context);
}
@Override
public MvcMatchersAuthorizedUrl mvcMatchers(HttpMethod method, String... mvcPatterns) {
return new MvcMatchersAuthorizedUrl(createMvcMatchers(method, mvcPatterns));
}
@Override
public MvcMatchersAuthorizedUrl mvcMatchers(String... patterns) {
return mvcMatchers(null, patterns);
}
@Override
protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {
@ -241,8 +253,32 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
return "hasIpAddress('" + ipAddressExpression + "')";
}
public final class AuthorizedUrl {
private List<RequestMatcher> requestMatchers;
/**
* An {@link AuthorizedUrl} that allows optionally configuring the
* {@link MvcRequestMatcher#setMethod(HttpMethod)}
*
* @author Rob Winch
*/
public class MvcMatchersAuthorizedUrl extends AuthorizedUrl {
/**
* Creates a new instance
*
* @param requestMatchers the {@link RequestMatcher} instances to map
*/
private MvcMatchersAuthorizedUrl(List<MvcRequestMatcher> requestMatchers) {
super(requestMatchers);
}
public AuthorizedUrl servletPath(String servletPath) {
for (MvcRequestMatcher matcher : (List<MvcRequestMatcher>) getMatchers()) {
matcher.setServletPath(servletPath);
}
return this;
}
}
public class AuthorizedUrl {
private List<? extends RequestMatcher> requestMatchers;
private boolean not;
/**
@ -250,10 +286,14 @@ public final class ExpressionUrlAuthorizationConfigurer<H extends HttpSecurityBu
*
* @param requestMatchers the {@link RequestMatcher} instances to map
*/
private AuthorizedUrl(List<RequestMatcher> requestMatchers) {
private AuthorizedUrl(List<? extends RequestMatcher> requestMatchers) {
this.requestMatchers = requestMatchers;
}
protected List<? extends RequestMatcher> getMatchers() {
return this.requestMatchers;
}
/**
* Negates the following expression.
*

View File

@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.List;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
@ -28,8 +29,10 @@ import org.springframework.security.access.vote.AuthenticatedVoter;
import org.springframework.security.access.vote.RoleVoter;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer.AuthorizedUrl;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;
import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
@ -126,6 +129,17 @@ public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
setApplicationContext(context);
}
@Override
public MvcMatchersAuthorizedUrl mvcMatchers(HttpMethod method,
String... mvcPatterns) {
return new MvcMatchersAuthorizedUrl(createMvcMatchers(method, mvcPatterns));
}
@Override
public MvcMatchersAuthorizedUrl mvcMatchers(String... patterns) {
return mvcMatchers(null, patterns);
}
@Override
protected final AuthorizedUrl chainRequestMatchersInternal(
List<RequestMatcher> requestMatchers) {
@ -237,6 +251,31 @@ public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
return authorities;
}
/**
* An {@link AuthorizedUrl} that allows optionally configuring the
* {@link MvcRequestMatcher#setMethod(HttpMethod)}
*
* @author Rob Winch
*/
public final class MvcMatchersAuthorizedUrl extends AuthorizedUrl {
/**
* Creates a new instance
*
* @param requestMatchers the {@link RequestMatcher} instances to map
*/
private MvcMatchersAuthorizedUrl(List<MvcRequestMatcher> requestMatchers) {
super(requestMatchers);
}
@SuppressWarnings("unchecked")
public AuthorizedUrl servletPath(String servletPath) {
for (MvcRequestMatcher matcher : (List<MvcRequestMatcher>) getMatchers()) {
matcher.setServletPath(servletPath);
}
return this;
}
}
/**
* Maps the specified {@link RequestMatcher} instances to {@link ConfigAttribute}
* instances.
@ -244,15 +283,15 @@ public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
* @author Rob Winch
* @since 3.2
*/
public final class AuthorizedUrl {
private final List<RequestMatcher> requestMatchers;
public class AuthorizedUrl {
private final List<? extends RequestMatcher> requestMatchers;
/**
* Creates a new instance
* @param requestMatchers the {@link RequestMatcher} instances to map to some
* {@link ConfigAttribute} instances.
*/
private AuthorizedUrl(List<RequestMatcher> requestMatchers) {
private AuthorizedUrl(List<? extends RequestMatcher> requestMatchers) {
Assert.notEmpty(requestMatchers,
"requestMatchers must contain at least one value");
this.requestMatchers = requestMatchers;
@ -318,5 +357,9 @@ public final class UrlAuthorizationConfigurer<H extends HttpSecurityBuilder<H>>
addMapping(requestMatchers, SecurityConfig.createList(attributes));
return UrlAuthorizationConfigurer.this.REGISTRY;
}
protected List<? extends RequestMatcher> getMatchers() {
return this.requestMatchers;
}
}
}

View File

@ -15,8 +15,6 @@
*/
package org.springframework.security.config.http;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.config.BeanDefinition;
@ -37,6 +35,8 @@ import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
import java.util.List;
/**
* Allows for convenient creation of a {@link FilterInvocationSecurityMetadataSource} bean
* for use with a FilterSecurityInterceptor.
@ -48,12 +48,13 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
private static final String ATT_HTTP_METHOD = "method";
private static final String ATT_PATTERN = "pattern";
private static final String ATT_ACCESS = "access";
private static final String ATT_SERVLET_PATH = "servlet-path";
private static final Log logger = LogFactory
.getLog(FilterInvocationSecurityMetadataSourceParser.class);
public BeanDefinition parse(Element element, ParserContext parserContext) {
List<Element> interceptUrls = DomUtils.getChildElementsByTagName(element,
"intercept-url");
Elements.INTERCEPT_URL);
// Check for attributes that aren't allowed in this context
for (Element elt : interceptUrls) {
@ -71,6 +72,12 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
"The attribute '" + HttpSecurityBeanDefinitionParser.ATT_FILTERS
+ "' isn't allowed here.", elt);
}
if (StringUtils.hasLength(elt.getAttribute(ATT_SERVLET_PATH))) {
parserContext.getReaderContext().error(
"The attribute '" + ATT_SERVLET_PATH
+ "' isn't allowed here.", elt);
}
}
BeanDefinition mds = createSecurityMetadataSource(interceptUrls, false, element,
@ -165,8 +172,16 @@ public class FilterInvocationSecurityMetadataSourceParser implements BeanDefinit
method = null;
}
String servletPath = urlElt.getAttribute(ATT_SERVLET_PATH);
if (!StringUtils.hasText(servletPath)) {
servletPath = null;
} else if (!MatcherType.mvc.equals(matcherType)) {
parserContext.getReaderContext().error(
ATT_SERVLET_PATH + " is not applicable for request-matcher: '" + matcherType.name() + "'", urlElt);
}
BeanDefinition matcher = matcherType.createMatcher(parserContext, path,
method);
method, servletPath);
BeanDefinitionBuilder attributeBuilder = BeanDefinitionBuilder
.rootBeanDefinition(SecurityConfig.class);

View File

@ -49,6 +49,10 @@ public enum MatcherType {
}
public BeanDefinition createMatcher(ParserContext pc, String path, String method) {
return createMatcher(pc, path, method, null);
}
public BeanDefinition createMatcher(ParserContext pc, String path, String method, String servletPath) {
if (("/**".equals(path) || "**".equals(path)) && method == null) {
return new RootBeanDefinition(AnyRequestMatcher.class);
}
@ -74,6 +78,7 @@ public enum MatcherType {
matcherBldr.addConstructorArgValue(path);
if (this == mvc) {
matcherBldr.addPropertyValue("method", method);
matcherBldr.addPropertyValue("servletPath", servletPath);
}
else {
matcherBldr.addConstructorArgValue(method);

View File

@ -381,6 +381,9 @@ intercept-url.attlist &=
intercept-url.attlist &=
## Used to specify that a URL must be accessed over http or https, or that there is no preference. The value should be "http", "https" or "any", respectively.
attribute requires-channel {xsd:token}?
intercept-url.attlist &=
## The path to the servlet. This attribute is only applicable when 'request-matcher' is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more HttpServlet's registered in the ServletContext that have mappings starting with '/' and are different; 2) The pattern starts with the same value of a registered HttpServlet path, excluding the default (root) HttpServlet '/'.
attribute servlet-path {xsd:token}?
logout =
## Incorporates a logout processing filter. Most web applications require a logout filter, although you may not require one if you write a controller to provider similar logic.

View File

@ -1345,6 +1345,16 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="servlet-path" type="xs:token">
<xs:annotation>
<xs:documentation>The path to the servlet. This attribute is only applicable when 'request-matcher' is
'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are
2 or more HttpServlet's registered in the ServletContext that have mappings starting with
'/' and are different; 2) The pattern starts with the same value of a registered
HttpServlet path, excluding the default (root) HttpServlet '/'
</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:attributeGroup name="logout.attlist">

View File

@ -67,5 +67,13 @@ class AbstractConfigAttributeRequestMatcherRegistryTests extends Specification {
List<RequestMatcher> chainRequestMatchersInternal(List<RequestMatcher> requestMatchers) {
return requestMatchers;
}
List<RequestMatcher> mvcMatchers(String... mvcPatterns) {
null
}
List<RequestMatcher> mvcMatchers(HttpMethod method, String... mvcPatterns) {
null
}
}
}

View File

@ -15,19 +15,17 @@
*/
package org.springframework.security.config.http
import javax.servlet.Filter
import javax.servlet.http.HttpServletResponse
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException
import org.springframework.mock.web.MockFilterChain
import org.springframework.mock.web.MockHttpServletRequest
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;
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletResponse
/**
*
* @author Rob Winch
@ -266,6 +264,88 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
response.status == HttpServletResponse.SC_FORBIDDEN
}
def "intercept-url mvc matchers with servlet path"() {
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", 'servlet-path': "/spring")
}
bean('pathController',PathController)
xml.'mvc:annotation-driven'()
createAppContext()
when:
request.servletPath = "/spring"
request.requestURI = "/spring/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 = "/spring"
request.requestURI = "/spring/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 = "/spring"
request.requestURI = "/spring/path/"
springSecurityFilterChain.doFilter(request, response, chain)
then:
response.status == HttpServletResponse.SC_UNAUTHORIZED
}
def "intercept-url ant matcher with servlet path fails"() {
when:
xml.http('request-matcher':'ant') {
'http-basic'()
'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring")
}
createAppContext()
then:
thrown(BeanDefinitionParsingException)
}
def "intercept-url regex matcher with servlet path fails"() {
when:
xml.http('request-matcher':'regex') {
'http-basic'()
'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring")
}
createAppContext()
then:
thrown(BeanDefinitionParsingException)
}
def "intercept-url ciRegex matcher with servlet path fails"() {
when:
xml.http('request-matcher':'ciRegex') {
'http-basic'()
'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring")
}
createAppContext()
then:
thrown(BeanDefinitionParsingException)
}
def "intercept-url default matcher with servlet path fails"() {
when:
xml.http() {
'http-basic'()
'intercept-url'(pattern: '/path', access: "denyAll", 'servlet-path': "/spring")
}
createAppContext()
then:
thrown(BeanDefinitionParsingException)
}
public static class Id {
public boolean isOne(int i) {
return i == 1;
@ -284,4 +364,4 @@ class InterceptUrlConfigTests extends AbstractHttpConfigTests {
return "path";
}
}
}
}

View File

@ -0,0 +1,228 @@
/*
* 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.annotation.web.builders;
import javax.servlet.http.HttpServletResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.FilterChainProxy;
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;
/**
* @author Rob Winch
*/
public class WebSecurityTests {
AnnotationConfigWebApplicationContext context;
MockHttpServletRequest request;
MockHttpServletResponse response;
MockFilterChain chain;
@Autowired
FilterChainProxy springSecurityFilterChain;
@Before
public void setup() {
this.request = new MockHttpServletRequest();
this.request.setMethod("GET");
this.response = new MockHttpServletResponse();
this.chain = new MockFilterChain();
}
@After
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void ignoringMvcMatcher() throws Exception {
loadConfig(MvcMatcherConfig.class);
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setRequestURI("/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setRequestURI("/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setRequestURI("/other");
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
public void configure(WebSecurity web) throws Exception {
// @formatter:off
web
.ignoring()
.mvcMatchers("/path");
// @formatter:on
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic().and()
.authorizeRequests()
.anyRequest().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 ignoringMvcMatcherServletPath() throws Exception {
loadConfig(MvcMatcherServletPathConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/other");
this.request.setRequestURI("/other/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class MvcMatcherServletPathConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
// @formatter:off
web
.ignoring()
.mvcMatchers("/path").servletPath("/spring")
.mvcMatchers("/notused");
// @formatter:on
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic().and()
.authorizeRequests()
.anyRequest().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";
}
}
}
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

@ -15,11 +15,17 @@
*/
package org.springframework.security.config.annotation.web.configurers;
import java.util.Collections;
import java.util.Map;
import javax.servlet.ServletRegistration;
import javax.servlet.http.HttpServletResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -48,6 +54,9 @@ import org.springframework.web.context.support.AnnotationConfigWebApplicationCon
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
/**
* @author Rob Winch
@ -59,12 +68,14 @@ public class AuthorizeRequestsTests {
MockHttpServletRequest request;
MockHttpServletResponse response;
MockFilterChain chain;
MockServletContext servletContext;
@Autowired
FilterChainProxy springSecurityFilterChain;
@Before
public void setup() {
this.servletContext = spy(new MockServletContext());
this.request = new MockHttpServletRequest();
this.request.setMethod("GET");
this.response = new MockHttpServletResponse();
@ -257,7 +268,7 @@ public class AuthorizeRequestsTests {
public void mvcMatcher() throws Exception {
loadConfig(MvcMatcherConfig.class);
this.request.setServletPath("/path");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
@ -265,7 +276,7 @@ public class AuthorizeRequestsTests {
setup();
this.request.setServletPath("/path.html");
this.request.setRequestURI("/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
@ -311,22 +322,100 @@ public class AuthorizeRequestsTests {
}
}
@Test
public void mvcMatcherServletPath() throws Exception {
loadConfig(MvcMatcherServletPathConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/foo");
this.request.setRequestURI("/foo/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class MvcMatcherServletPathConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic().and()
.authorizeRequests()
.mvcMatchers("/path").servletPath("/spring").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.request.setRequestURI("/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.request.setRequestURI("/user/deny");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
}
@EnableWebSecurity
@ -360,10 +449,55 @@ public class AuthorizeRequestsTests {
}
}
@Test(expected = IllegalStateException.class)
public void mvcMatcherServletPathRequired() throws Exception {
final ServletRegistration registration = mock(ServletRegistration.class);
when(registration.getMappings()).thenReturn(Collections.singleton("/spring"));
Answer<Map<String, ? extends ServletRegistration>> answer = new Answer<Map<String, ? extends ServletRegistration>>() {
@Override
public Map<String, ? extends ServletRegistration> answer(InvocationOnMock invocation) throws Throwable {
return Collections.<String, ServletRegistration>singletonMap("spring", registration);
}
};
when(servletContext.getServletRegistrations()).thenAnswer(answer);
loadConfig(MvcMatcherPathServletPathRequiredConfig.class);
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class MvcMatcherPathServletPathRequiredConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic().and()
.authorizeRequests()
.mvcMatchers("/user").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";
}
}
}
public void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);
this.context.setServletContext(new MockServletContext());
this.context.setServletContext(this.servletContext);
this.context.refresh();
this.context.getAutowireCapableBeanFactory().autowireBean(this);

View File

@ -195,6 +195,71 @@ public class HttpSecurityRequestMatchersTests {
}
}
@Test
public void requestMatchersMvcMatcherServletPath() throws Exception {
loadConfig(RequestMatchersMvcMatcherServeltPathConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/other");
this.request.setRequestURI("/other/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class RequestMatchersMvcMatcherServeltPathConfig
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.requestMatchers()
.mvcMatchers("/path").servletPath("/spring")
.mvcMatchers("/never-match")
.and()
.httpBasic().and()
.authorizeRequests()
.anyRequest().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";
}
}
}
public void loadConfig(Class<?>... configs) {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(configs);

View File

@ -0,0 +1,214 @@
/*
* Copyright 2002-2015 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.annotation.web.configurers;
import javax.servlet.http.HttpServletResponse;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.FilterChainProxy;
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;
/**
* @author Rob Winch
*
*/
public class UrlAuthorizationConfigurerTests {
AnnotationConfigWebApplicationContext context;
MockHttpServletRequest request;
MockHttpServletResponse response;
MockFilterChain chain;
@Autowired
FilterChainProxy springSecurityFilterChain;
@Before
public void setup() {
this.request = new MockHttpServletRequest();
this.request.setMethod("GET");
this.response = new MockHttpServletResponse();
this.chain = new MockFilterChain();
}
@After
public void cleanup() {
if (this.context != null) {
this.context.close();
}
}
@Test
public void mvcMatcher() throws Exception {
loadConfig(MvcMatcherConfig.class);
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setRequestURI("/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()
.apply(new UrlAuthorizationConfigurer(getApplicationContext())).getRegistry()
.mvcMatchers("/path").hasRole("ADMIN");
// @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 mvcMatcherServletPath() throws Exception {
loadConfig(MvcMatcherServletPathConfig.class);
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path.html");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/spring");
this.request.setRequestURI("/spring/path/");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus())
.isEqualTo(HttpServletResponse.SC_UNAUTHORIZED);
setup();
this.request.setServletPath("/foo");
this.request.setRequestURI("/foo/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
setup();
this.request.setServletPath("/");
this.request.setRequestURI("/path");
this.springSecurityFilterChain.doFilter(this.request, this.response, this.chain);
assertThat(this.response.getStatus()).isEqualTo(HttpServletResponse.SC_OK);
}
@EnableWebSecurity
@Configuration
@EnableWebMvc
static class MvcMatcherServletPathConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// @formatter:off
http
.httpBasic().and()
.apply(new UrlAuthorizationConfigurer(getApplicationContext())).getRegistry()
.mvcMatchers("/path").servletPath("/spring").hasRole("ADMIN");
// @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

@ -15,11 +15,9 @@
*/
package org.springframework.security.config.http;
import java.util.Collection;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
import org.springframework.context.support.AbstractXmlApplicationContext;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
@ -33,6 +31,8 @@ import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.ExpressionBasedFilterInvocationSecurityMetadataSource;
import org.springframework.security.web.access.intercept.DefaultFilterInvocationSecurityMetadataSource;
import java.util.Collection;
import static org.assertj.core.api.Assertions.assertThat;
/**
@ -118,6 +118,13 @@ public class FilterSecurityMetadataSourceBeanDefinitionParserTests {
+ ConfigTestUtils.AUTH_PROVIDER_XML);
}
@Test(expected = BeanDefinitionParsingException.class)
public void parsingInterceptUrlServletPathFails() {
setContext("<filter-security-metadata-source id='fids' use-expressions='false'>"
+ " <intercept-url pattern='/secure' access='ROLE_USER' servlet-path='/spring' />"
+ "</filter-security-metadata-source>");
}
private FilterInvocation createFilterInvocation(String path, String method) {
MockHttpServletRequest request = new MockHttpServletRequest();
request.setRequestURI(null);

View File

@ -4166,7 +4166,7 @@ Additionally, if the https://www.w3.org/TR/CSP2/#directive-report-uri[*_report-u
then the violation will be reported by the user-agent to the declared URL.
For example, if a web application violates the declared security policy,
the following response header will instruct the user-agent to send violation reports to the URL specified in the policys _report-uri_ directive.
the following response header will instruct the user-agent to send violation reports to the URL specified in the policy's _report-uri_ directive.
[source]
----
@ -5786,7 +5786,7 @@ If CSRF protection is enabled, this tag inserts a hidden form field with the cor
Normally Spring Security automatically inserts a CSRF form field for any `<form:form>` tags you use, but if for some reason you cannot use `<form:form>`, `csrfInput` is a handy replacement.
You should place this tag within an HTML `<form></form>` block, where you would normally place other input fields. Do NOT place this tag within a Spring `<form:form></form:form>` blockSpring Security handles Spring forms automatically.
You should place this tag within an HTML `<form></form>` block, where you would normally place other input fields. Do NOT place this tag within a Spring `<form:form></form:form>` block. Spring Security handles Spring forms automatically.
[source,xml]
----
@ -7475,12 +7475,12 @@ 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 `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.
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>>, <<nsa-intercept-url-method,method>> and <<nsa-intercept-url-servlet-path,servlet-path>> attributes. Ant paths are matched using an `AntPathRequestMatcher`, regular expressions are matched using a `RegexRequestMatcher` and for Spring MVC path matching the `MvcRequestMatcher` is used. 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]]
* **request-matcher-ref**
A referenece to a bean that implements `RequestMatcher` that will determine if this `FilterChain` should be used. This is a more powerful alternative to <<nsa-http-pattern,pattern>>.
A reference to a bean that implements `RequestMatcher` that will determine if this `FilterChain` should be used. This is a more powerful alternative to <<nsa-http-pattern,pattern>>.
[[nsa-http-security]]
@ -8150,7 +8150,7 @@ Defines a reference to a Spring bean that implements `HttpFirewall`.
[[nsa-intercept-url]]
==== <intercept-url>
This element is used to define the set of URL patterns that the application is interested in and to configure how they should be handled. It is used to construct the `FilterInvocationSecurityMetadataSource` used by the `FilterSecurityInterceptor`. It is also responsible for configuring a `ChannelProcessingFilter` if particular URLs need to be accessed by HTTPS, for example. When matching the specified patterns against an incoming request, the matching is done in the order in which the elements are declared. So the most specific matches patterns should come first and the most general should come last.
This element is used to define the set of URL patterns that the application is interested in and to configure how they should be handled. It is used to construct the `FilterInvocationSecurityMetadataSource` used by the `FilterSecurityInterceptor`. It is also responsible for configuring a `ChannelProcessingFilter` if particular URLs need to be accessed by HTTPS, for example. When matching the specified patterns against an incoming request, the matching is done in the order in which the elements are declared. So the most specific patterns should come first and the most general should come last.
[[nsa-intercept-url-parents]]
@ -8179,7 +8179,7 @@ NOTE: This property is invalid for <<nsa-filter-security-metadata-source,filter-
[[nsa-intercept-url-method]]
* **method**
The HTTP Method which will be used in combination with the pattern to match an incoming request. If omitted, any method will match. If an identical pattern is specified with and without a method, the method-specific match will take precedence.
The HTTP Method which will be used in combination with the pattern and servlet path (optional) to match an incoming request. If omitted, any method will match. If an identical pattern is specified with and without a method, the method-specific match will take precedence.
[[nsa-intercept-url-pattern]]
@ -8195,6 +8195,12 @@ If a `<port-mappings>` configuration is added, this will be used to by the `Secu
NOTE: This property is invalid for <<nsa-filter-security-metadata-source,filter-security-metadata-source>>
[[nsa-intercept-url-servlet-path]]
* **servlet-path**
The servlet path which will be used in combination with the pattern and HTTP method to match an incoming request. This attribute is only applicable when <<nsa-http-request-matcher,request-matcher>> is 'mvc'. In addition, the value is only required in the following 2 use cases: 1) There are 2 or more `HttpServlet` 's registered in the `ServletContext` that have mappings starting with `'/'` and are different; 2) The pattern starts with the same value of a registered `HttpServlet` path, excluding the default (root) `HttpServlet` `'/'`.
NOTE: This property is invalid for <<nsa-filter-security-metadata-source,filter-security-metadata-source>>
[[nsa-jee]]
==== <jee>
@ -8699,7 +8705,7 @@ Used to explicitly configure a FilterChainProxy instance with a FilterChainMap
[[nsa-filter-chain-map-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 to 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.
[[nsa-filter-chain-map-children]]
@ -8734,7 +8740,7 @@ A comma separated list of references to Spring beans that implement `Filter`. Th
[[nsa-filter-chain-pattern]]
* **pattern**
A-pattern that creates RequestMatcher in combination with the <<nsa-filter-chain-map-request-matcher,request-matcher>>
A pattern that creates RequestMatcher in combination with the <<nsa-filter-chain-map-request-matcher,request-matcher>>
[[nsa-filter-chain-request-matcher-ref]]

View File

@ -16,11 +16,6 @@
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;
@ -31,20 +26,32 @@ import org.springframework.web.servlet.handler.MatchableHandlerMapping;
import org.springframework.web.servlet.handler.RequestMatchResult;
import org.springframework.web.util.UrlPathHelper;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.Map;
/**
* A {@link RequestMatcher} that uses Spring MVC's {@link HandlerMappingIntrospector} to
* match the path and extract variables.
*
* <p>
* It is important to understand that Spring MVC's matching is relative to the servlet
* path. This means if you have mapped any servlet to a path that starts with "/" and is
* greater than one, you should also specify the {@link #setServletPath(String)}
* attribute to differentiate mappings.
* </p>
*
* @author Rob Winch
* @since 4.1.1
*/
public final class MvcRequestMatcher
public class MvcRequestMatcher
implements RequestMatcher, RequestVariablesExtractor {
private final DefaultMatcher defaultMatcher = new DefaultMatcher();
private final HandlerMappingIntrospector introspector;
private final String pattern;
private HttpMethod method;
private String servletPath;
public MvcRequestMatcher(HandlerMappingIntrospector introspector, String pattern) {
this.introspector = introspector;
@ -56,6 +63,9 @@ public final class MvcRequestMatcher
if (this.method != null && !this.method.name().equals(request.getMethod())) {
return false;
}
if (this.servletPath != null && !this.servletPath.equals(request.getServletPath())) {
return false;
}
MatchableHandlerMapping mapping = getMapping(request);
if (mapping == null) {
return this.defaultMatcher.matches(request);
@ -97,6 +107,24 @@ public final class MvcRequestMatcher
this.method = method;
}
/**
* The servlet path to match on. The default is undefined which means any servlet
* path.
*
* @param servletPath the servletPath to set
*/
public void setServletPath(String servletPath) {
this.servletPath = servletPath;
}
protected final String getServletPath() {
return this.servletPath;
}
protected final String getMvcPattern() {
return this.pattern;
}
private class DefaultMatcher implements RequestMatcher, RequestVariablesExtractor {
private final UrlPathHelper pathHelper = new UrlPathHelper();
@ -124,4 +152,4 @@ public final class MvcRequestMatcher
return Collections.emptyMap();
}
}
}
}

View File

@ -108,6 +108,31 @@ public class MvcRequestMatcherTests {
assertThat(this.matcher.extractUriTemplateVariables(this.request)).isEmpty();
}
@Test
public void matchesServletPathTrue() throws Exception {
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
when(this.mapping.match(eq(this.request), this.pattern.capture()))
.thenReturn(this.result);
this.matcher.setServletPath("/spring");
this.request.setServletPath("/spring");
assertThat(this.matcher.matches(this.request)).isTrue();
assertThat(this.pattern.getValue()).isEqualTo("/path");
}
@Test
public void matchesServletPathFalse() throws Exception {
when(this.introspector.getMatchableHandlerMapping(this.request))
.thenReturn(this.mapping);
when(this.mapping.match(eq(this.request), this.pattern.capture()))
.thenReturn(this.result);
this.matcher.setServletPath("/spring");
this.request.setServletPath("/");
assertThat(this.matcher.matches(this.request)).isFalse();
}
@Test
public void matchesPathOnlyTrue() throws Exception {
when(this.introspector.getMatchableHandlerMapping(this.request))