Extract a SecurityFilterChain interface and create a default implementation to facilitate other configuration options.

This commit is contained in:
Luke Taylor 2011-07-06 00:12:48 +01:00
parent 2d271666a4
commit f92589f051
10 changed files with 101 additions and 79 deletions

View File

@ -9,6 +9,7 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.AnonymousAuthenticationToken; import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
@ -24,6 +25,7 @@ import org.springframework.security.web.context.SecurityContextPersistenceFilter
import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter; import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter; import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter;
import org.springframework.security.web.session.SessionManagementFilter; import org.springframework.security.web.session.SessionManagementFilter;
import org.springframework.security.web.util.AnyRequestMatcher;
public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator { public class DefaultFilterChainValidator implements FilterChainProxy.FilterChainValidator {
private final Log logger = LogFactory.getLog(getClass()); private final Log logger = LogFactory.getLog(getClass());
@ -34,17 +36,34 @@ public class DefaultFilterChainValidator implements FilterChainProxy.FilterChain
checkFilterStack(filterChain.getFilters()); checkFilterStack(filterChain.getFilters());
} }
checkPathOrder(new ArrayList<SecurityFilterChain>(fcp.getFilterChains()));
checkForDuplicateMatchers(new ArrayList<SecurityFilterChain>(fcp.getFilterChains())); checkForDuplicateMatchers(new ArrayList<SecurityFilterChain>(fcp.getFilterChains()));
} }
private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) { private void checkPathOrder(List<SecurityFilterChain> filterChains) {
SecurityFilterChain chain = chains.remove(0); // Check that the universal pattern is listed at the end, if at all
Iterator<SecurityFilterChain> chains = filterChains.iterator();
for (SecurityFilterChain test : chains) { while (chains.hasNext()) {
if (chain.getRequestMatcher().equals(test.getRequestMatcher())) { if (((DefaultSecurityFilterChain)chains.next()).getRequestMatcher() instanceof AnyRequestMatcher && chains.hasNext()) {
throw new IllegalArgumentException("The FilterChainProxy contains two filter chains using the" + throw new IllegalArgumentException("A universal match pattern ('/**') is defined " +
" matcher " + chain.getRequestMatcher() + ". If you are using multiple <http> namespace " + " before other patterns in the filter chain, causing them to be ignored. Please check the " +
"elements, you must use a 'pattern' attribute to define the request patterns to which they apply."); "ordering in your <security:http> namespace or FilterChainProxy bean configuration");
}
}
}
private void checkForDuplicateMatchers(List<SecurityFilterChain> chains) {
while (chains.size() > 1) {
DefaultSecurityFilterChain chain = (DefaultSecurityFilterChain)chains.remove(0);
for (SecurityFilterChain test : chains) {
if (chain.getRequestMatcher().equals(((DefaultSecurityFilterChain)test).getRequestMatcher())) {
throw new IllegalArgumentException("The FilterChainProxy contains two filter chains using the" +
" matcher " + chain.getRequestMatcher() + ". If you are using multiple <http> namespace " +
"elements, you must use a 'pattern' attribute to define the request patterns to which they apply.");
}
} }
} }
} }

View File

@ -7,6 +7,7 @@ import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext; import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -26,7 +27,7 @@ public class FilterChainBeanDefinitionParser implements BeanDefinitionParser {
String requestMatcher = elt.getAttribute(ATT_REQUEST_MATCHER_REF); String requestMatcher = elt.getAttribute(ATT_REQUEST_MATCHER_REF);
String filters = elt.getAttribute(HttpSecurityBeanDefinitionParser.ATT_FILTERS); String filters = elt.getAttribute(HttpSecurityBeanDefinitionParser.ATT_FILTERS);
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(SecurityFilterChain.class); BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(DefaultSecurityFilterChain.class);
if (StringUtils.hasText(path)) { if (StringUtils.hasText(path)) {
Assert.isTrue(!StringUtils.hasText(requestMatcher), ""); Assert.isTrue(!StringUtils.hasText(requestMatcher), "");

View File

@ -21,6 +21,7 @@ import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.BeanIds; import org.springframework.security.config.BeanIds;
import org.springframework.security.config.Elements; import org.springframework.security.config.Elements;
import org.springframework.security.config.authentication.AuthenticationManagerFactoryBean; import org.springframework.security.config.authentication.AuthenticationManagerFactoryBean;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.AnyRequestMatcher; import org.springframework.security.web.util.AnyRequestMatcher;
@ -147,7 +148,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser {
filterChainMatcher = new RootBeanDefinition(AnyRequestMatcher.class); filterChainMatcher = new RootBeanDefinition(AnyRequestMatcher.class);
} }
BeanDefinitionBuilder filterChainBldr = BeanDefinitionBuilder.rootBeanDefinition(SecurityFilterChain.class); BeanDefinitionBuilder filterChainBldr = BeanDefinitionBuilder.rootBeanDefinition(DefaultSecurityFilterChain.class);
filterChainBldr.addConstructorArgValue(filterChainMatcher); filterChainBldr.addConstructorArgValue(filterChainMatcher);
filterChainBldr.addConstructorArgValue(filterChain); filterChainBldr.addConstructorArgValue(filterChain);

View File

@ -42,7 +42,7 @@ class MultiHttpBlockConfigTests extends AbstractHttpConfigTests {
createAppContext() createAppContext()
then: then:
BeanCreationException e = thrown() BeanCreationException e = thrown()
e.cause.cause instanceof IllegalArgumentException e.cause instanceof IllegalArgumentException
} }
def duplicatePatternsAreRejected () { def duplicatePatternsAreRejected () {

View File

@ -33,6 +33,7 @@ import org.springframework.beans.factory.BeanCreationException;
import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext;
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.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.FilterChainProxy; import org.springframework.security.web.FilterChainProxy;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@ -67,11 +68,6 @@ public class FilterChainProxyConfigTests {
} }
} }
@Test(expected=BeanCreationException.class)
public void misplacedUniversalPathShouldBeDetected() throws Exception {
appCtx.getBean("newFilterChainProxyWrongPathOrder", FilterChainProxy.class);
}
@Test @Test
public void normalOperation() throws Exception { public void normalOperation() throws Exception {
FilterChainProxy filterChainProxy = appCtx.getBean("filterChain", FilterChainProxy.class); FilterChainProxy filterChainProxy = appCtx.getBean("filterChain", FilterChainProxy.class);
@ -111,9 +107,13 @@ public class FilterChainProxyConfigTests {
FilterChainProxy fcp = appCtx.getBean("sec1235FilterChainProxy", FilterChainProxy.class); FilterChainProxy fcp = appCtx.getBean("sec1235FilterChainProxy", FilterChainProxy.class);
List<SecurityFilterChain> chains = fcp.getFilterChains(); List<SecurityFilterChain> chains = fcp.getFilterChains();
assertEquals("/login*", ((AntPathRequestMatcher)chains.get(0).getRequestMatcher()).getPattern()); assertEquals("/login*", getPattern(chains.get(0)));
assertEquals("/logout", ((AntPathRequestMatcher)chains.get(1).getRequestMatcher()).getPattern()); assertEquals("/logout", getPattern(chains.get(1)));
assertTrue(chains.get(2).getRequestMatcher() instanceof AnyRequestMatcher); assertTrue(((DefaultSecurityFilterChain)chains.get(2)).getRequestMatcher() instanceof AnyRequestMatcher);
}
private String getPattern(SecurityFilterChain chain) {
return ((AntPathRequestMatcher)((DefaultSecurityFilterChain)chain).getRequestMatcher()).getPattern();
} }
private void checkPathAndFilterOrder(FilterChainProxy filterChainProxy) throws Exception { private void checkPathAndFilterOrder(FilterChainProxy filterChainProxy) throws Exception {

View File

@ -98,8 +98,11 @@
<sec:filter-chain pattern="/some/other/path/**" filters="sif,mockFilter,mockFilter2"/> <sec:filter-chain pattern="/some/other/path/**" filters="sif,mockFilter,mockFilter2"/>
</util:list> </util:list>
</constructor-arg> </constructor-arg>
<property name="filterChainValidator" ref="fcv" />
</bean> </bean>
<bean id="fcv" class="org.springframework.security.config.http.DefaultFilterChainValidator" />
<bean id="newFilterChainProxyRegex" class="org.springframework.security.web.FilterChainProxy"> <bean id="newFilterChainProxyRegex" class="org.springframework.security.web.FilterChainProxy">
<sec:filter-chain-map path-type="regex"> <sec:filter-chain-map path-type="regex">
<sec:filter-chain pattern="\A/foo/.*\Z" filters="mockFilter"/> <sec:filter-chain pattern="\A/foo/.*\Z" filters="mockFilter"/>
@ -124,7 +127,7 @@
<bean id="newFilterChainProxyNonNamespace" class="org.springframework.security.web.FilterChainProxy"> <bean id="newFilterChainProxyNonNamespace" class="org.springframework.security.web.FilterChainProxy">
<constructor-arg> <constructor-arg>
<list> <list>
<bean class="org.springframework.security.web.SecurityFilterChain"> <bean class="org.springframework.security.web.DefaultSecurityFilterChain">
<constructor-arg ref="fooMatcher"/> <constructor-arg ref="fooMatcher"/>
<constructor-arg> <constructor-arg>
<list> <list>
@ -132,7 +135,7 @@
</list> </list>
</constructor-arg> </constructor-arg>
</bean> </bean>
<bean class="org.springframework.security.web.SecurityFilterChain"> <bean class="org.springframework.security.web.DefaultSecurityFilterChain">
<constructor-arg> <constructor-arg>
<bean class="org.springframework.security.web.util.AntPathRequestMatcher"> <bean class="org.springframework.security.web.util.AntPathRequestMatcher">
<constructor-arg value="/some/other/path/**"/> <constructor-arg value="/some/other/path/**"/>
@ -146,7 +149,7 @@
</list> </list>
</constructor-arg> </constructor-arg>
</bean> </bean>
<bean class="org.springframework.security.web.SecurityFilterChain"> <bean class="org.springframework.security.web.DefaultSecurityFilterChain">
<constructor-arg> <constructor-arg>
<bean class="org.springframework.security.web.util.AntPathRequestMatcher"> <bean class="org.springframework.security.web.util.AntPathRequestMatcher">
<constructor-arg value="/do/not/filter*"/> <constructor-arg value="/do/not/filter*"/>
@ -156,7 +159,7 @@
<list /> <list />
</constructor-arg> </constructor-arg>
</bean> </bean>
<bean class="org.springframework.security.web.SecurityFilterChain"> <bean class="org.springframework.security.web.DefaultSecurityFilterChain">
<constructor-arg> <constructor-arg>
<bean class="org.springframework.security.web.util.AntPathRequestMatcher"> <bean class="org.springframework.security.web.util.AntPathRequestMatcher">
<constructor-arg value="/**"/> <constructor-arg value="/**"/>

View File

@ -0,0 +1,45 @@
package org.springframework.security.web;
import org.springframework.security.web.util.RequestMatcher;
import javax.servlet.Filter;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* Standard implementation of {@code SecurityFilterChain}.
*
* @author Luke Taylor
*
* @since 3.1
*/
public final class DefaultSecurityFilterChain implements SecurityFilterChain {
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) {
this(requestMatcher, Arrays.asList(filters));
}
public DefaultSecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) {
this.requestMatcher = requestMatcher;
this.filters = new ArrayList<Filter>(filters);
}
public RequestMatcher getRequestMatcher() {
return requestMatcher;
}
public List<Filter> getFilters() {
return filters;
}
public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
@Override
public String toString() {
return "[ " + requestMatcher + ", " + filters + "]";
}
}

View File

@ -142,7 +142,6 @@ public class FilterChainProxy extends GenericFilterBean {
public FilterChainProxy(List<SecurityFilterChain> filterChains) { public FilterChainProxy(List<SecurityFilterChain> filterChains) {
this.filterChains = filterChains; this.filterChains = filterChains;
checkPathOrder();
} }
@Override @Override
@ -219,10 +218,8 @@ public class FilterChainProxy extends GenericFilterBean {
filterChains = new ArrayList<SecurityFilterChain>(filterChainMap.size()); filterChains = new ArrayList<SecurityFilterChain>(filterChainMap.size());
for (Map.Entry<RequestMatcher,List<Filter>> entry : filterChainMap.entrySet()) { for (Map.Entry<RequestMatcher,List<Filter>> entry : filterChainMap.entrySet()) {
filterChains.add(new SecurityFilterChain(entry.getKey(), entry.getValue())); filterChains.add(new DefaultSecurityFilterChain(entry.getKey(), entry.getValue()));
} }
checkPathOrder();
} }
/** /**
@ -238,25 +235,12 @@ public class FilterChainProxy extends GenericFilterBean {
LinkedHashMap<RequestMatcher, List<Filter>> map = new LinkedHashMap<RequestMatcher, List<Filter>>(); LinkedHashMap<RequestMatcher, List<Filter>> map = new LinkedHashMap<RequestMatcher, List<Filter>>();
for (SecurityFilterChain chain : filterChains) { for (SecurityFilterChain chain : filterChains) {
map.put(chain.getRequestMatcher(), chain.getFilters()); map.put(((DefaultSecurityFilterChain)chain).getRequestMatcher(), chain.getFilters());
} }
return map; return map;
} }
private void checkPathOrder() {
// Check that the universal pattern is listed at the end, if at all
Iterator<SecurityFilterChain> chains = filterChains.iterator();
while(chains.hasNext()) {
if ((chains.next().getRequestMatcher() instanceof AnyRequestMatcher && chains.hasNext())) {
throw new IllegalArgumentException("A universal match pattern ('/**') is defined " +
" before other patterns in the filter chain, causing them to be ignored. Please check the " +
"ordering in your <security:http> namespace or FilterChainProxy bean configuration");
}
}
}
/** /**
* @return the list of {@code SecurityFilterChain}s which will be matched against and * @return the list of {@code SecurityFilterChain}s which will be matched against and
* applied to incoming requests. * applied to incoming requests.

View File

@ -1,54 +1,23 @@
package org.springframework.security.web; package org.springframework.security.web;
import org.springframework.security.web.util.RequestMatcher;
import javax.servlet.Filter; import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.*; import java.util.*;
/** /**
* Bean which defines a filter chain which is capable of being matched against an {@code HttpServletRequest}. * Defines a filter chain which is capable of being matched against an {@code HttpServletRequest}.
* in order to decide whether it applies to that request. * in order to decide whether it applies to that request.
* <p> * <p>
* Used to configure a {@code FilterChainProxy}. * Used to configure a {@code FilterChainProxy}.
* *
*
* @author Luke Taylor * @author Luke Taylor
* *
* @since 3.1 * @since 3.1
*/ */
public final class SecurityFilterChain { public interface SecurityFilterChain {
private final RequestMatcher requestMatcher;
private final List<Filter> filters;
public SecurityFilterChain(RequestMatcher requestMatcher, Filter... filters) { boolean matches(HttpServletRequest request);
this(requestMatcher, Arrays.asList(filters));
}
public SecurityFilterChain(RequestMatcher requestMatcher, List<Filter> filters) { List<Filter> getFilters();
this.requestMatcher = requestMatcher;
this.filters = new ArrayList<Filter>(filters);
}
public RequestMatcher getRequestMatcher() {
return requestMatcher;
}
public List<Filter> getFilters() {
return filters;
}
public boolean matches(HttpServletRequest request) {
return requestMatcher.matches(request);
}
@Override
public String toString() {
return "[ " + requestMatcher + ", " + filters + "]";
}
} }

View File

@ -47,7 +47,7 @@ public class FilterChainProxyTests {
return null; return null;
} }
}).when(filter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), any(FilterChain.class)); }).when(filter).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class), any(FilterChain.class));
fcp = new FilterChainProxy(new SecurityFilterChain(matcher, Arrays.asList(filter))); fcp = new FilterChainProxy(new DefaultSecurityFilterChain(matcher, Arrays.asList(filter)));
fcp.setFilterChainValidator(mock(FilterChainProxy.FilterChainValidator.class)); fcp.setFilterChainValidator(mock(FilterChainProxy.FilterChainValidator.class));
request = new MockHttpServletRequest(); request = new MockHttpServletRequest();
request.setServletPath("/path"); request.setServletPath("/path");
@ -94,7 +94,7 @@ public class FilterChainProxyTests {
@Test @Test
public void originalFilterChainIsInvokedIfMatchingSecurityChainIsEmpty() throws Exception { public void originalFilterChainIsInvokedIfMatchingSecurityChainIsEmpty() throws Exception {
List<Filter> noFilters = Collections.emptyList(); List<Filter> noFilters = Collections.emptyList();
fcp = new FilterChainProxy(new SecurityFilterChain(matcher, noFilters)); fcp = new FilterChainProxy(new DefaultSecurityFilterChain(matcher, noFilters));
when(matcher.matches(any(HttpServletRequest.class))).thenReturn(true); when(matcher.matches(any(HttpServletRequest.class))).thenReturn(true);
fcp.doFilter(request, response, chain); fcp.doFilter(request, response, chain);
@ -137,7 +137,7 @@ public class FilterChainProxyTests {
@Test @Test
public void bothWrappersAreResetWithNestedFcps() throws Exception { public void bothWrappersAreResetWithNestedFcps() throws Exception {
HttpFirewall fw = mock(HttpFirewall.class); HttpFirewall fw = mock(HttpFirewall.class);
FilterChainProxy firstFcp = new FilterChainProxy(new SecurityFilterChain(matcher, fcp)); FilterChainProxy firstFcp = new FilterChainProxy(new DefaultSecurityFilterChain(matcher, fcp));
firstFcp.setFirewall(fw); firstFcp.setFirewall(fw);
fcp.setFirewall(fw); fcp.setFirewall(fw);
FirewalledRequest firstFwr = mock(FirewalledRequest.class, "firstFwr"); FirewalledRequest firstFwr = mock(FirewalledRequest.class, "firstFwr");