diff --git a/core/src/main/java/org/springframework/security/config/FilterChainMapBeanDefinitionDecorator.java b/core/src/main/java/org/springframework/security/config/FilterChainMapBeanDefinitionDecorator.java index 3cc7113709..c2966dee10 100644 --- a/core/src/main/java/org/springframework/security/config/FilterChainMapBeanDefinitionDecorator.java +++ b/core/src/main/java/org/springframework/security/config/FilterChainMapBeanDefinitionDecorator.java @@ -1,8 +1,11 @@ package org.springframework.security.config; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.BeanDefinitionDecorator; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.context.ApplicationContext; @@ -15,10 +18,7 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; import org.w3c.dom.Node; -import javax.servlet.Filter; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; +import java.util.*; /** * Sets the FilterChainMap for a FilterChainProxy bean declaration. @@ -29,82 +29,45 @@ import java.util.List; public class FilterChainMapBeanDefinitionDecorator implements BeanDefinitionDecorator { public static final String FILTER_CHAIN_ELT_NAME = "filter-chain"; - public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder definition, ParserContext parserContext) { - FilterChainMap filterChainMap = new FilterChainMap(); + public BeanDefinitionHolder decorate(Node node, BeanDefinitionHolder holder, ParserContext parserContext) { + BeanDefinition filterChainProxy = holder.getBeanDefinition(); + + Map filterChainMap = new LinkedHashMap(); Element elt = (Element)node; String pathType = elt.getAttribute(HttpSecurityBeanDefinitionParser.PATTERN_TYPE_ATTRIBUTE); if (HttpSecurityBeanDefinitionParser.PATTERN_TYPE_REGEX.equals(pathType)) { - filterChainMap.setUrlPathMatcher(new RegexUrlPathMatcher()); + filterChainProxy.getPropertyValues().addPropertyValue("matcher", new RegexUrlPathMatcher()); } - List paths = new ArrayList(); - List filterChains = new ArrayList(); - Iterator filterChainElts = DomUtils.getChildElementsByTagName(elt, FILTER_CHAIN_ELT_NAME).iterator(); while (filterChainElts.hasNext()) { Element chain = (Element) filterChainElts.next(); String path = chain.getAttribute(HttpSecurityBeanDefinitionParser.PATH_PATTERN_ATTRIBUTE); - Assert.hasText(path, "The attribute '" + HttpSecurityBeanDefinitionParser.PATH_PATTERN_ATTRIBUTE + "' must not be empty"); + Assert.hasText(path, "The attribute '" + HttpSecurityBeanDefinitionParser.PATH_PATTERN_ATTRIBUTE + + "' must not be empty"); String filters = chain.getAttribute(HttpSecurityBeanDefinitionParser.FILTERS_ATTRIBUTE); Assert.hasText(filters, "The attribute '" + HttpSecurityBeanDefinitionParser.FILTERS_ATTRIBUTE + "'must not be empty"); - paths.add(path); - filterChains.add(filters); - } - // Set the FilterChainMap on the FilterChainProxy bean. - definition.getBeanDefinition().getPropertyValues().addPropertyValue("filterChainMap", filterChainMap); + if (filters.equals(HttpSecurityBeanDefinitionParser.NO_FILTERS_VALUE)) { + filterChainMap.put(path, Collections.EMPTY_LIST); + } else { + String[] filterBeanNames = StringUtils.tokenizeToStringArray(filters, ","); + ManagedList filterChain = new ManagedList(filterBeanNames.length); - // Register the ApplicationContextAware bean which will add the filter chains to the FilterChainMap - RootBeanDefinition chainResolver = new RootBeanDefinition(FilterChainResolver.class); - chainResolver.getConstructorArgumentValues().addIndexedArgumentValue(0, filterChainMap); - chainResolver.getConstructorArgumentValues().addIndexedArgumentValue(1, paths); - chainResolver.getConstructorArgumentValues().addIndexedArgumentValue(2, filterChains); - - parserContext.getRegistry().registerBeanDefinition(definition.getBeanName() + ".filterChainMapChainResolver", - chainResolver); - - return definition; - } - - /** - * Bean which stores the filter chains as lists of bean names (e.g. - * "filter1, filter2, filter3") until the application context is available, then resolves them - * to actual Filter instances when the setApplicationContext method is called. - * It then uses them to build the secure URL configuration for the supplied FilterChainMap. - */ - static class FilterChainResolver implements ApplicationContextAware { - private List paths; - private List filterChains; - FilterChainMap filterChainMap; - - FilterChainResolver(FilterChainMap filterChainMap, List paths, List filterChains) { - this.paths = paths; - this.filterChains = filterChains; - this.filterChainMap = filterChainMap; - } - - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - for (int i=0; i < paths.size(); i++) { - String path = (String)paths.get(i); - String filterList = (String) filterChains.get(i); - - if (filterList.equals(HttpSecurityBeanDefinitionParser.NO_FILTERS_VALUE)) { - filterChainMap.addSecureUrl(path, HttpSecurityBeanDefinitionParser.EMPTY_FILTER_CHAIN); - } else { - String[] filterNames = StringUtils.tokenizeToStringArray(filterList, ","); - Filter[] filters = new Filter[filterNames.length]; - - for (int j=0; j < filterNames.length; j++) { - filters[j] = (Filter) applicationContext.getBean(filterNames[j], Filter.class); - } - - filterChainMap.addSecureUrl(path, filters); + for (int i=0; i < filterBeanNames.length; i++) { + filterChain.add(new RuntimeBeanReference(filterBeanNames[i])); } + + filterChainMap.put(path, filterChain); } } + + filterChainProxy.getPropertyValues().addPropertyValue("filterChainMap", new ManagedMap(filterChainMap)); + + return holder; } } diff --git a/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java index 3c69d07403..e323af7a14 100644 --- a/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java +++ b/core/src/main/java/org/springframework/security/config/HttpSecurityBeanDefinitionParser.java @@ -9,7 +9,10 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.security.ConfigAttributeDefinition; import org.springframework.security.ConfigAttributeEditor; import org.springframework.security.context.HttpSessionContextIntegrationFilter; -import org.springframework.security.intercept.web.*; +import org.springframework.security.intercept.web.FilterInvocationDefinitionMap; +import org.springframework.security.intercept.web.FilterSecurityInterceptor; +import org.springframework.security.intercept.web.PathBasedFilterInvocationDefinitionMap; +import org.springframework.security.intercept.web.RegExpBasedFilterInvocationDefinitionMap; import org.springframework.security.ui.ExceptionTranslationFilter; import org.springframework.security.util.FilterChainProxy; import org.springframework.security.util.RegexUrlPathMatcher; @@ -19,8 +22,7 @@ import org.springframework.util.xml.DomUtils; import org.w3c.dom.Element; import javax.servlet.Filter; -import java.util.Iterator; -import java.util.List; +import java.util.*; /** * Sets up HTTP security: filter stack and protected URLs. @@ -48,7 +50,6 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { static final String FILTERS_ATTRIBUTE = "filters"; static final String NO_FILTERS_VALUE = "none"; - static final Filter[] EMPTY_FILTER_CHAIN = new Filter[0]; private static final String ACCESS_CONFIG_ATTRIBUTE = "access"; @@ -71,14 +72,14 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { // TODO: Get path type attribute and determine FilDefInvS class - FilterChainMap filterChainMap = new FilterChainMap(); + Map filterChainMap = new LinkedHashMap(); String patternType = element.getAttribute(PATTERN_TYPE_ATTRIBUTE); FilterInvocationDefinitionMap interceptorFilterInvDefSource = new PathBasedFilterInvocationDefinitionMap(); if (patternType.equals(PATTERN_TYPE_REGEX)) { - filterChainMap.setUrlPathMatcher(new RegexUrlPathMatcher()); + filterChainProxy.getPropertyValues().addPropertyValue("matcher", new RegexUrlPathMatcher()); interceptorFilterInvDefSource = new RegExpBasedFilterInvocationDefinitionMap(); } @@ -131,10 +132,10 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { } /** - * Parses the intercept-url elements and populates the FilterChainProxy's FilterChainMap and the + * Parses the intercept-url elements and populates the FilterChainProxy's filter chain Map and the * FilterInvocationDefinitionSource used in FilterSecurityInterceptor. */ - private void parseInterceptUrls(List urlElts, FilterChainMap filterChainMap, + private void parseInterceptUrls(List urlElts, Map filterChainMap, FilterInvocationDefinitionMap interceptorFilterInvDefSource) { Iterator urlEltsIterator = urlElts.iterator(); @@ -166,7 +167,7 @@ public class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { throw new IllegalStateException("Currently only 'none' is supported as the custom filters attribute"); } - filterChainMap.addSecureUrl(path, EMPTY_FILTER_CHAIN); + filterChainMap.put(path, Collections.EMPTY_LIST); } } } diff --git a/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java b/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java index 01cee6494b..47a25cdd7e 100644 --- a/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java +++ b/core/src/main/java/org/springframework/security/config/HttpSecurityConfigPostProcessor.java @@ -79,11 +79,14 @@ public class HttpSecurityConfigPostProcessor implements BeanFactoryPostProcessor // Set the default match List defaultFilterChain = orderFilters(beanFactory); - FilterChainMap filterMap = filterChainProxy.getFilterChainMap(); + // Note that this returns a copy + Map filterMap = filterChainProxy.getFilterChainMap(); - String allUrlsMatch = filterMap.getMatcher().getUniversalMatchPattern(); + String allUrlsMatch = filterChainProxy.getMatcher().getUniversalMatchPattern(); - filterMap.addSecureUrl(allUrlsMatch, (Filter[]) defaultFilterChain.toArray(new Filter[0])); + filterMap.put(allUrlsMatch, defaultFilterChain); + + filterChainProxy.setFilterChainMap(filterMap); } private List orderFilters(ConfigurableListableBeanFactory beanFactory) { diff --git a/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java b/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java index b9efe94523..3cd6ded317 100644 --- a/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java +++ b/core/src/main/java/org/springframework/security/config/SecurityNamespaceHandler.java @@ -1,6 +1,14 @@ package org.springframework.security.config; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.NamespaceHandlerSupport; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.util.FilterChainProxy; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; /** @@ -10,6 +18,8 @@ import org.springframework.beans.factory.xml.NamespaceHandlerSupport; * @version $Id$ */ public class SecurityNamespaceHandler extends NamespaceHandlerSupport { + public static final String DEFAULT_FILTER_CHAIN_PROXY_ID = "_filterChainProxy"; + public void init() { registerBeanDefinitionParser("ldap", new LdapBeanDefinitionParser()); registerBeanDefinitionParser("http", new HttpSecurityBeanDefinitionParser()); @@ -18,4 +28,19 @@ public class SecurityNamespaceHandler extends NamespaceHandlerSupport { registerBeanDefinitionDecorator("intercept-methods", new InterceptMethodsBeanDefinitionDecorator()); registerBeanDefinitionDecorator("filter-chain-map", new FilterChainMapBeanDefinitionDecorator()); } + +// private class HttpSecurityBeanDefinitionParser implements BeanDefinitionParser { +// +// public BeanDefinition parse(Element element, ParserContext parserContext) { +// RootBeanDefinition filterChainProxy = new RootBeanDefinition(FilterChainProxy.class); +// +// Element formLoginElt = DomUtils.getChildElementByTagName(element, "form-login"); +// +// if (formLoginElt != null) { +// parse(formLoginElt, parserContext); +// } +// +// return filterChainProxy; +// } +// } } diff --git a/core/src/main/java/org/springframework/security/intercept/web/FIDSToFilterChainMapConverter.java b/core/src/main/java/org/springframework/security/intercept/web/FIDSToFilterChainMapConverter.java index 3a6eccb5df..c51ca02dbc 100644 --- a/core/src/main/java/org/springframework/security/intercept/web/FIDSToFilterChainMapConverter.java +++ b/core/src/main/java/org/springframework/security/intercept/web/FIDSToFilterChainMapConverter.java @@ -4,26 +4,24 @@ import org.springframework.context.ApplicationContext; import org.springframework.security.ConfigAttribute; import org.springframework.security.ConfigAttributeDefinition; import org.springframework.security.util.FilterChainProxy; -import org.springframework.security.util.RegexUrlPathMatcher; import javax.servlet.Filter; -import java.util.List; -import java.util.Iterator; -import java.util.ArrayList; +import java.util.*; /** * Used internally to provide backward compatibility for configuration of FilterChainProxy using a * FilterInvocationDefinitionSource. This is deprecated in favour of namespace-based configuration. * - * This class will convert a FilterInvocationDefinitionSource into a FilterChainMap, provided it is one of the - * recognised implementations (ant path or regular expression). + * This class will convert a FilterInvocationDefinitionSource into a suitable Map, provided it is one of the + * recognised implementations (ant path or regular expression). The order of the mappings will be + * preserved in the Map. * * @author Luke Taylor * @version $Id$ */ public class FIDSToFilterChainMapConverter { - private FilterChainMap filterChainMap = new FilterChainMap(); + private LinkedHashMap filterChainMap = new LinkedHashMap(); public FIDSToFilterChainMapConverter(FilterInvocationDefinitionSource fids, ApplicationContext appContext) { @@ -39,7 +37,6 @@ public class FIDSToFilterChainMapConverter { requestMap = ((PathBasedFilterInvocationDefinitionMap)fids).getRequestMap(); } else if (fids instanceof RegExpBasedFilterInvocationDefinitionMap) { requestMap = ((RegExpBasedFilterInvocationDefinitionMap)fids).getRequestMap(); - filterChainMap.setUrlPathMatcher(new RegexUrlPathMatcher()); } else { throw new IllegalArgumentException("Can't handle FilterInvocationDefinitionSource type " + fids.getClass()); } @@ -77,11 +74,11 @@ public class FIDSToFilterChainMapConverter { } } - filterChainMap.addSecureUrl(path, (Filter[]) filters.toArray(new Filter[filters.size()])); + filterChainMap.put(path, filters); } } - public FilterChainMap getFilterChainMap() { + public Map getFilterChainMap() { return filterChainMap; } } diff --git a/core/src/main/java/org/springframework/security/util/FilterChainProxy.java b/core/src/main/java/org/springframework/security/util/FilterChainProxy.java index 158cdd11d1..59b52c47ff 100644 --- a/core/src/main/java/org/springframework/security/util/FilterChainProxy.java +++ b/core/src/main/java/org/springframework/security/util/FilterChainProxy.java @@ -15,30 +15,18 @@ package org.springframework.security.util; -import org.springframework.security.intercept.web.FilterInvocation; -import org.springframework.security.intercept.web.FilterInvocationDefinitionSource; -import org.springframework.security.intercept.web.FilterChainMap; -import org.springframework.security.intercept.web.FIDSToFilterChainMapConverter; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; - import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; - +import org.springframework.security.intercept.web.*; import org.springframework.util.Assert; +import javax.servlet.*; import java.io.IOException; - -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 java.util.*; /** @@ -53,10 +41,11 @@ import javax.servlet.ServletResponse; * #doFilter(ServletRequest, ServletResponse, FilterChain)} invocations through to each Filter defined * against FilterChainProxy.

* - *

As of version 2.0, FilterChainProxy is configured using a {@link FilterChainMap}. In previous + *

As of version 2.0, FilterChainProxy is configured using an ordered Map of path patterns to Lists + * of Filter objects. In previous * versions, a {@link FilterInvocationDefinitionSource} was used. This is now deprecated in favour of namespace-based - * configuration which provides a more robust and simplfied syntax. The FilterChainMap instance will be - * created while parsing the namespace configuration, so it doesn't require an explicit bean declaration. + * configuration which provides a more robust and simplfied syntax. The Map instance will normally be + * created while parsing the namespace configuration, so doesn't have to be set explicitly. * Instead the <filter-chain-map> element should be used within the FilterChainProxy bean declaration. * This in turn should have a list of child <filter-chain> elements which each define a URI pattern and the list * of filters (as comma-separated bean names) which should be applied to requests which match the pattern. @@ -92,7 +81,7 @@ import javax.servlet.ServletResponse; * container. As per {@link org.springframework.security.util.FilterToBeanProxy} JavaDocs, we recommend you allow the IoC * container to manage lifecycle instead of the servlet container. By default the FilterToBeanProxy will * never call this class' {@link #init(FilterConfig)} and {@link #destroy()} methods, meaning each of the filters - * defined in the FilterChainMap will not be called. If you do need your filters to be + * defined in the filter chain map will not be called. If you do need your filters to be * initialized and destroyed, please set the lifecycle initialization parameter against the * FilterToBeanProxy to specify servlet container lifecycle management.

* @@ -111,22 +100,41 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo //~ Instance fields ================================================================================================ private ApplicationContext applicationContext; - private FilterChainMap filterChainMap; + /** Map of the original pattern Strings to filter chains */ + private Map uncompiledFilterChainMap; + /** Compiled pattern version of the filter chain map */ + private Map filterChainMap; + private UrlMatcher matcher = new AntUrlPathMatcher(); private FilterInvocationDefinitionSource fids; + //~ Methods ======================================================================================================== public void afterPropertiesSet() throws Exception { // Convert the FilterDefinitionSource to a filterChainMap if set if (fids != null) { - Assert.isNull(filterChainMap, "Set the FilterChainMap or FilterInvocationDefinitionSource but not both"); + Assert.isNull(uncompiledFilterChainMap, "Set the FilterChainMap or FilterInvocationDefinitionSource but not both"); setFilterChainMap(new FIDSToFilterChainMapConverter(fids, applicationContext).getFilterChainMap()); } - Assert.notNull(filterChainMap, "A FilterChainMap must be supplied"); + Assert.notNull(uncompiledFilterChainMap, "A FilterChainMap must be supplied"); } + public void init(FilterConfig filterConfig) throws ServletException { + Filter[] filters = obtainAllDefinedFilters(); + + for (int i = 0; i < filters.length; i++) { + if (filters[i] != null) { + if (logger.isDebugEnabled()) { + logger.debug("Initializing Filter defined in ApplicationContext: '" + filters[i].toString() + "'"); + } + + filters[i].init(filterConfig); + } + } + } + public void destroy() { - Filter[] filters = filterChainMap.getAllDefinedFilters(); + Filter[] filters = obtainAllDefinedFilters(); for (int i = 0; i < filters.length; i++) { if (filters[i] != null) { @@ -143,10 +151,9 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); + List filters = getFilters(fi.getRequestUrl()); - Filter[] filters = filterChainMap.getFilters(fi.getRequestUrl()); - - if (filters == null || filters.length == 0) { + if (filters == null || filters.size() == 0) { if (logger.isDebugEnabled()) { logger.debug(fi.getRequestUrl() + filters == null ? " has no matching filters" : " has an empty filter list"); @@ -161,34 +168,54 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse()); } - public void init(FilterConfig filterConfig) throws ServletException { - Filter[] filters = filterChainMap.getAllDefinedFilters(); + /** + * Returns the first filter chain matching the supplied URL. + * TODO: Change tests and make package protected access. + * + * @param url the request URL + * @return an ordered array of Filters defining the filter chain + */ + public List getFilters(String url) { + Iterator filterChains = filterChainMap.entrySet().iterator(); - for (int i = 0; i < filters.length; i++) { - if (filters[i] != null) { - if (logger.isDebugEnabled()) { - logger.debug("Initializing Filter defined in ApplicationContext: '" + filters[i].toString() + "'"); - } + while (filterChains.hasNext()) { + Map.Entry entry = (Map.Entry) filterChains.next(); + Object path = entry.getKey(); - filters[i].init(filterConfig); + boolean matched = matcher.pathMatchesUrl(path, url); + + if (logger.isDebugEnabled()) { + logger.debug("Candidate is: '" + url + "'; pattern is " + path + "; matched=" + matched); + } + + if (matched) { + return (List) entry.getValue(); } } + + return null; } /** - * Obtains all of the uniqueFilter instances registered in the - * FilterChainMap. - *

This is useful in ensuring a Filter is not - * initialized or destroyed twice.

+ * Obtains all of the uniqueFilter instances registered in the map of + * filter chains. + *

This is useful in ensuring a Filter is not initialized or destroyed twice.

* - * @deprecated * @return all of the Filter instances in the application context which have an entry - * in the FilterChainMap (only one entry is included in the array for + * in the map (only one entry is included in the array for * each Filter that actually exists in application context, even if a given * Filter is defined multiples times by the FilterChainMap) */ protected Filter[] obtainAllDefinedFilters() { - return filterChainMap.getAllDefinedFilters(); + Set allFilters = new HashSet(); + + Iterator it = filterChainMap.values().iterator(); + + while (it.hasNext()) { + allFilters.addAll((List) it.next()); + } + + return (Filter[]) new ArrayList(allFilters).toArray(new Filter[0]); } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { @@ -200,15 +227,69 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo * @deprecated Use namespace configuration or call setFilterChainMap instead. */ public void setFilterInvocationDefinitionSource(FilterInvocationDefinitionSource fids) { + if( fids instanceof RegExpBasedFilterInvocationDefinitionMap) { + matcher = new RegexUrlPathMatcher(); + } this.fids = fids; } - public void setFilterChainMap(FilterChainMap filterChainMap) { - this.filterChainMap = filterChainMap; + /** + * Sets the mapping of URL patterns to filter chains. + * + * The map keys should be the paths and the values should be arrays of Filter objects. + * It's VERY important that the type of map used preserves ordering - the order in which the iterator + * returns the entries must be the same as the order they were added to the map, otherwise you have no way + * of guaranteeing that the most specific patterns are returned before the more general ones. So make sure + * the Map used is an instance of LinkedHashMap or an equivalent, rather than a plain HashMap, for + * example. + * + * @param filterChainMap the map of path Strings to Filter[]s. + */ + public void setFilterChainMap(Map filterChainMap) { + uncompiledFilterChainMap = new LinkedHashMap(filterChainMap); + createCompiledMap(); } - public FilterChainMap getFilterChainMap() { - return filterChainMap; + private void createCompiledMap() { + Iterator paths = uncompiledFilterChainMap.keySet().iterator(); + filterChainMap = new LinkedHashMap(uncompiledFilterChainMap.size()); + + while (paths.hasNext()) { + Object path = paths.next(); + Assert.isInstanceOf(String.class, path, "Path pattern must be a String"); + Object compiledPath = matcher.compile((String)path); + Object filters = uncompiledFilterChainMap.get(path); + + Assert.isInstanceOf(List.class, filters); + // Check the contents + Iterator filterIterator = ((List)filters).iterator(); + + while (filterIterator.hasNext()) { + Object filter = filterIterator.next(); + Assert.isInstanceOf(Filter.class, filter, "Objects in filter chain must be of type Filter. "); + } + + filterChainMap.put(compiledPath, filters); + } + } + + + /** + * Returns a copy of the underlying filter chain map. Modifications to the map contents + * will not affect the FilterChainProxy state - to change the map call setFilterChainMap. + * + * @return the map of path pattern Strings to filter chain arrays (with ordering guaranteed). + */ + public Map getFilterChainMap() { + return new LinkedHashMap(uncompiledFilterChainMap); + } + + public void setMatcher(UrlMatcher matcher) { + this.matcher = matcher; + } + + public UrlMatcher getMatcher() { + return matcher; } //~ Inner Classes ================================================================================================== @@ -221,17 +302,17 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo */ private static class VirtualFilterChain implements FilterChain { private FilterInvocation fi; - private Filter[] additionalFilters; + private List additionalFilters; private int currentPosition = 0; - public VirtualFilterChain(FilterInvocation filterInvocation, Filter[] additionalFilters) { + private VirtualFilterChain(FilterInvocation filterInvocation, List additionalFilters) { this.fi = filterInvocation; this.additionalFilters = additionalFilters; } public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { - if (currentPosition == additionalFilters.length) { + if (currentPosition == additionalFilters.size()) { if (logger.isDebugEnabled()) { logger.debug(fi.getRequestUrl() + " reached end of additional filter chain; proceeding with original chain"); @@ -241,13 +322,15 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo } else { currentPosition++; + Filter nextFilter = (Filter) additionalFilters.get(currentPosition - 1); + if (logger.isDebugEnabled()) { logger.debug(fi.getRequestUrl() + " at position " + currentPosition + " of " - + additionalFilters.length + " in additional filter chain; firing Filter: '" - + additionalFilters[currentPosition - 1] + "'"); + + additionalFilters.size() + " in additional filter chain; firing Filter: '" + + nextFilter + "'"); } - additionalFilters[currentPosition - 1].doFilter(request, response, this); + nextFilter.doFilter(request, response, this); } } } diff --git a/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java b/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java index e8888b1e22..aa45a57e3f 100644 --- a/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java +++ b/core/src/test/java/org/springframework/security/config/HttpSecurityBeanDefinitionParserTests.java @@ -6,7 +6,6 @@ import org.junit.BeforeClass; import org.junit.Test; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.security.context.HttpSessionContextIntegrationFilter; -import org.springframework.security.intercept.web.FilterChainMap; import org.springframework.security.intercept.web.FilterSecurityInterceptor; import org.springframework.security.ui.ExceptionTranslationFilter; import org.springframework.security.ui.basicauth.BasicProcessingFilter; @@ -15,7 +14,8 @@ import org.springframework.security.ui.webapp.AuthenticationProcessingFilter; import org.springframework.security.ui.webapp.DefaultLoginPageGeneratingFilter; import org.springframework.security.util.FilterChainProxy; -import javax.servlet.Filter; +import java.util.Iterator; +import java.util.List; /** * @author Luke Taylor @@ -41,11 +41,9 @@ public class HttpSecurityBeanDefinitionParserTests { FilterChainProxy filterChainProxy = (FilterChainProxy) appContext.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_FILTER_CHAIN_PROXY_ID); - FilterChainMap filterChainMap = filterChainProxy.getFilterChainMap(); + List filters = filterChainProxy.getFilters("/unprotected"); - Filter[] filters = filterChainMap.getFilters("/unprotected"); - - assertTrue(filters.length == 0); + assertTrue(filters.size() == 0); } @Test @@ -53,20 +51,18 @@ public class HttpSecurityBeanDefinitionParserTests { FilterChainProxy filterChainProxy = (FilterChainProxy) appContext.getBean(HttpSecurityBeanDefinitionParser.DEFAULT_FILTER_CHAIN_PROXY_ID); - FilterChainMap filterChainMap = filterChainProxy.getFilterChainMap(); - - Filter[] filters = filterChainMap.getFilters("/someurl"); - - + List filterList = filterChainProxy.getFilters("/someurl"); - assertTrue("Expected 7 filters in chain", filters.length == 7); + assertTrue("Expected 7 filterList in chain", filterList.size() == 7); - assertTrue(filters[0] instanceof HttpSessionContextIntegrationFilter); - assertTrue(filters[1] instanceof LogoutFilter); - assertTrue(filters[2] instanceof AuthenticationProcessingFilter); - assertTrue(filters[3] instanceof DefaultLoginPageGeneratingFilter); - assertTrue(filters[4] instanceof BasicProcessingFilter); - assertTrue(filters[5] instanceof ExceptionTranslationFilter); - assertTrue(filters[6] instanceof FilterSecurityInterceptor); + Iterator filters = filterList.iterator(); + + assertTrue(filters.next() instanceof HttpSessionContextIntegrationFilter); + assertTrue(filters.next() instanceof LogoutFilter); + assertTrue(filters.next() instanceof AuthenticationProcessingFilter); + assertTrue(filters.next() instanceof DefaultLoginPageGeneratingFilter); + assertTrue(filters.next() instanceof BasicProcessingFilter); + assertTrue(filters.next() instanceof ExceptionTranslationFilter); + assertTrue(filters.next() instanceof FilterSecurityInterceptor); } } diff --git a/core/src/test/java/org/springframework/security/util/FilterChainProxyTests.java b/core/src/test/java/org/springframework/security/util/FilterChainProxyTests.java index 744874707d..0efd25ce4b 100644 --- a/core/src/test/java/org/springframework/security/util/FilterChainProxyTests.java +++ b/core/src/test/java/org/springframework/security/util/FilterChainProxyTests.java @@ -15,10 +15,7 @@ package org.springframework.security.util; -import org.junit.After; -import static org.junit.Assert.*; -import org.junit.Before; -import org.junit.Test; + import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -26,9 +23,17 @@ import org.springframework.security.ConfigAttribute; import org.springframework.security.ConfigAttributeDefinition; import org.springframework.security.MockApplicationContext; import org.springframework.security.MockFilterConfig; +import org.springframework.security.context.HttpSessionContextIntegrationFilter; import org.springframework.security.intercept.web.MockFilterInvocationDefinitionSource; import org.springframework.security.intercept.web.PathBasedFilterInvocationDefinitionMap; +import org.springframework.security.ui.webapp.AuthenticationProcessingFilter; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import static org.junit.Assert.*; + +import java.util.List; /** * Tests {@link FilterChainProxy}. @@ -123,17 +128,50 @@ public class FilterChainProxyTests { @Test public void normalOperation() throws Exception { - doNormalOperation((FilterChainProxy) appCtx.getBean("filterChain", FilterChainProxy.class)); + FilterChainProxy filterChainProxy = (FilterChainProxy) appCtx.getBean("filterChain", FilterChainProxy.class); + doNormalOperation(filterChainProxy); } @Test public void normalOperationWithNewConfig() throws Exception { - doNormalOperation((FilterChainProxy) appCtx.getBean("newFilterChainProxy", FilterChainProxy.class)); + FilterChainProxy filterChainProxy = (FilterChainProxy) appCtx.getBean("newFilterChainProxy", FilterChainProxy.class); + checkPathAndFilterOrder(filterChainProxy); + doNormalOperation(filterChainProxy); } @Test public void normalOperationWithNewConfigRegex() throws Exception { - doNormalOperation((FilterChainProxy) appCtx.getBean("newFilterChainProxyRegex", FilterChainProxy.class)); + FilterChainProxy filterChainProxy = (FilterChainProxy) appCtx.getBean("newFilterChainProxyRegex", FilterChainProxy.class); + checkPathAndFilterOrder(filterChainProxy); + doNormalOperation(filterChainProxy); + } + + @Test + public void normalOperationWithNewConfigNonNamespace() throws Exception { + FilterChainProxy filterChainProxy = (FilterChainProxy) appCtx.getBean("newFilterChainProxyNonNamespace", FilterChainProxy.class); + checkPathAndFilterOrder(filterChainProxy); + doNormalOperation(filterChainProxy); + } + + private void checkPathAndFilterOrder(FilterChainProxy filterChainProxy) throws Exception { + List filters = filterChainProxy.getFilters("/foo/blah"); + assertEquals(1, filters.size()); + assertTrue(filters.get(0) instanceof MockFilter); + + filters = filterChainProxy.getFilters("/some/other/path/blah"); + assertEquals(3, filters.size()); + assertTrue(filters.get(0) instanceof HttpSessionContextIntegrationFilter); + assertTrue(filters.get(1) instanceof MockFilter); + assertTrue(filters.get(2) instanceof MockFilter); + + filters = filterChainProxy.getFilters("/do/not/filter"); + assertEquals(0, filters.size()); + + filters = filterChainProxy.getFilters("/another/nonspecificmatch"); + assertEquals(3, filters.size()); + assertTrue(filters.get(0) instanceof HttpSessionContextIntegrationFilter); + assertTrue(filters.get(1) instanceof AuthenticationProcessingFilter); + assertTrue(filters.get(2) instanceof MockFilter); } private void doNormalOperation(FilterChainProxy filterChainProxy) throws Exception { @@ -165,7 +203,6 @@ public class FilterChainProxyTests { assertTrue(filter.isWasInitialized()); assertTrue(filter.isWasDoFiltered()); assertTrue(filter.isWasDestroyed()); - } //~ Inner Classes ================================================================================================== diff --git a/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml b/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml index 082bb95f52..0866bc180e 100644 --- a/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml +++ b/core/src/test/resources/org/springframework/security/util/filtertest-valid.xml @@ -28,7 +28,18 @@ http://www.springframework.org/schema/security http://www.springframework.org/sc - + + + + + + + + + + + + @@ -47,6 +58,7 @@ http://www.springframework.org/schema/security http://www.springframework.org/sc + @@ -55,9 +67,40 @@ http://www.springframework.org/schema/security http://www.springframework.org/sc + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +