Просмотр исходного кода

SEC-568: Introduced FilterChainMap as a simpler option for configuring FilterChainProxy and introduced a namespace-based for configuring it. The Url pattern matching is factored out into a separate strategy with ant and regex versions.

Luke Taylor 18 лет назад
Родитель
Сommit
9b8c06e9f6

+ 110 - 0
core/src/main/java/org/springframework/security/config/FilterChainMapBeanDefinitionDecorator.java

@@ -0,0 +1,110 @@
+package org.springframework.security.config;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanDefinitionHolder;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.beans.factory.xml.BeanDefinitionDecorator;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.security.intercept.web.FilterChainMap;
+import org.springframework.security.util.RegexUrlPathMatcher;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+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;
+
+/**
+ * Sets the FilterChainMap for a FilterChainProxy bean declaration.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+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();
+        Element elt = (Element)node;
+
+        String pathType = elt.getAttribute(HttpSecurityBeanDefinitionParser.PATTERN_TYPE_ATTRIBUTE);
+
+        if (HttpSecurityBeanDefinitionParser.PATTERN_TYPE_REGEX.equals(pathType)) {
+            filterChainMap.setUrlPathMatcher(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");
+            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);
+
+        // 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 <tt>setApplicationContext</tt> 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);
+                }
+            }
+        }
+    }
+}

+ 87 - 0
core/src/main/java/org/springframework/security/intercept/web/FIDSToFilterChainMapConverter.java

@@ -0,0 +1,87 @@
+package org.springframework.security.intercept.web;
+
+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;
+
+/**
+ * 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).
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ */
+public class FIDSToFilterChainMapConverter {
+
+    private FilterChainMap filterChainMap = new FilterChainMap();
+
+    public FIDSToFilterChainMapConverter(FilterInvocationDefinitionSource fids, ApplicationContext appContext) {
+
+        List requestMap;
+
+        // TODO: Check if this is necessary. Retained from refactoring of FilterChainProxy 
+        if (fids.getConfigAttributeDefinitions() == null) {
+            throw new IllegalArgumentException("FilterChainProxy requires the FilterInvocationDefinitionSource to " +
+                    "return a non-null response to getConfigAttributeDefinitions()");
+        }
+
+        if (fids instanceof PathBasedFilterInvocationDefinitionMap) {
+            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());
+        }
+
+        Iterator entries = requestMap.iterator();
+
+        while (entries.hasNext()) {
+            Object entry = entries.next();
+            String path;
+            ConfigAttributeDefinition configAttributeDefinition;
+
+            if (entry instanceof PathBasedFilterInvocationDefinitionMap.EntryHolder) {
+                path = ((PathBasedFilterInvocationDefinitionMap.EntryHolder)entry).getAntPath();
+                configAttributeDefinition = ((PathBasedFilterInvocationDefinitionMap.EntryHolder)entry).getConfigAttributeDefinition();
+            } else {
+                path = ((RegExpBasedFilterInvocationDefinitionMap.EntryHolder)entry).getCompiledPattern().pattern();
+                configAttributeDefinition = ((RegExpBasedFilterInvocationDefinitionMap.EntryHolder)entry).getConfigAttributeDefinition();
+            }
+
+            List filters = new ArrayList();
+
+            Iterator attributes = configAttributeDefinition.getConfigAttributes();
+
+            while (attributes.hasNext()) {
+                ConfigAttribute attr = (ConfigAttribute) attributes.next();
+                String filterName = attr.getAttribute();
+
+                if (filterName == null) {
+                    throw new IllegalArgumentException("Configuration attribute: '" + attr
+                        + "' returned null to the getAttribute() method, which is invalid when used with FilterChainProxy");
+                }
+
+                if (!filterName.equals(FilterChainProxy.TOKEN_NONE)) {
+                    filters.add(appContext.getBean(filterName, Filter.class));
+                }
+            }
+
+            filterChainMap.addSecureUrl(path, (Filter[]) filters.toArray(new Filter[filters.size()]));
+        }
+    }
+
+    public FilterChainMap getFilterChainMap() {
+        return filterChainMap;
+    }
+}

+ 107 - 0
core/src/main/java/org/springframework/security/intercept/web/FilterChainMap.java

@@ -0,0 +1,107 @@
+package org.springframework.security.intercept.web;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.security.util.AntUrlPathMatcher;
+import org.springframework.security.util.UrlMatcher;
+import org.springframework.util.Assert;
+
+import javax.servlet.Filter;
+import java.util.*;
+
+/**
+ * Maps filter invocations to filter chains. Used to configure FilterChainProxy.
+ *
+ * @see org.springframework.security.util.FilterChainProxy
+ *
+ * @author luke
+ * @version $Id$
+ * @since 2.0
+ */
+public class FilterChainMap implements InitializingBean {
+    private static final Log logger = LogFactory.getLog(FilterChainMap.class);
+
+    private List paths = new ArrayList();
+    private List compiledPaths = new ArrayList();
+    private List filterChains = new ArrayList();
+
+    private UrlMatcher matcher = new AntUrlPathMatcher();
+
+    public FilterChainMap() {
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notEmpty(paths, "No secure URL paths defined");
+    }
+
+    public void addSecureUrl(String path, Filter[] filters) {
+        Assert.hasText(path, "The Path must not be empty or null");
+        Assert.notNull(filters, "The Filter array must not be null");
+        paths.add(path);
+        compiledPaths.add(matcher.compile(path));
+        filterChains.add(filters);
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Added pattern: " + path + "; filters: " + Arrays.asList(filters));
+        }
+    }
+
+    public void setUrlPathMatcher(UrlMatcher matcher) {
+        this.matcher = matcher;
+    }
+
+    public UrlMatcher getMatcher() {
+        return matcher;
+    }
+
+    /**
+     * Returns the first filter chain matching the supplied URL.
+     *
+     * @param url the request URL
+     * @return an ordered array of Filters defining the filter chain
+     */
+    public Filter[] getFilters(String url) {
+
+        for (int i=0; i < compiledPaths.size(); i++) {
+            Object path = compiledPaths.get(i);
+
+            boolean matched = matcher.pathMatchesUrl(path, url);
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Candidate is: '" + url + "'; pattern is " + paths.get(i) + "; matched=" + matched);
+            }
+
+            if (matched) {
+                return (Filter[]) filterChains.get(i);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Obtains all of the <b>unique</b><code>Filter</code> instances registered in the
+     * <code>FilterChainMap</code>.
+     * <p>This is useful in ensuring a <code>Filter</code> is not
+     * initialized or destroyed twice.</p>
+     * @return all of the <code>Filter</code> instances which have an entry
+     *         in the <code>FilterChainMap</code> (only one entry is included in the array for
+     *         each <code>Filter</code> instance, even if a given
+     *         <code>Filter</code> is used multiples times by the <code>FilterChainMap</code>)
+     */
+    public Filter[] getAllDefinedFilters() {
+        Set allFilters = new HashSet();
+
+        Iterator it = filterChains.iterator();
+        while (it.hasNext()) {
+            Filter[] filterChain = (Filter[])it.next();
+
+            for(int i=0; i < filterChain.length; i++) {
+                allFilters.add(filterChain[i]);
+            }
+        }
+
+        return (Filter[]) new ArrayList(allFilters).toArray(new Filter[0]);
+    }
+}

+ 4 - 0
core/src/main/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMap.java

@@ -131,6 +131,10 @@ public class PathBasedFilterInvocationDefinitionMap extends AbstractFilterInvoca
         this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison;
     }
 
+    List getRequestMap() {
+        return requestMap;
+    }    
+
     //~ Inner Classes ==================================================================================================
 
     protected class EntryHolder {

+ 4 - 0
core/src/main/java/org/springframework/security/intercept/web/RegExpBasedFilterInvocationDefinitionMap.java

@@ -118,6 +118,10 @@ public class RegExpBasedFilterInvocationDefinitionMap extends AbstractFilterInvo
         this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison;
     }
 
+    List getRequestMap() {
+        return requestMap;
+    }
+
     //~ Inner Classes ==================================================================================================
 
     protected class EntryHolder {

+ 46 - 0
core/src/main/java/org/springframework/security/util/AntUrlPathMatcher.java

@@ -0,0 +1,46 @@
+package org.springframework.security.util;
+
+import org.springframework.util.PathMatcher;
+import org.springframework.util.AntPathMatcher;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Ant path strategy for URL matching.
+ *
+ * @author luke
+ * @version $Id$
+ */
+public class AntUrlPathMatcher implements UrlMatcher {
+    private static final Log logger = LogFactory.getLog(AntUrlPathMatcher.class);
+
+    private boolean convertToLowercaseBeforeComparison = true;
+    private PathMatcher pathMatcher = new AntPathMatcher();
+
+    public Object compile(String path) {
+        if (convertToLowercaseBeforeComparison) {
+            return path.toLowerCase();
+        }
+
+        return path;
+    }
+
+    public void setConvertToLowercaseBeforeComparison(boolean convertToLowercaseBeforeComparison) {
+        this.convertToLowercaseBeforeComparison = convertToLowercaseBeforeComparison;
+    }
+
+    public boolean pathMatchesUrl(Object path, String url) {
+        if (convertToLowercaseBeforeComparison) {
+            url = url.toLowerCase();
+            if (logger.isDebugEnabled()) {
+                logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
+            }
+        }
+
+        return pathMatcher.match((String)path, url);
+    }
+
+    public String getUniversalMatchPattern() {
+        return "/**";
+    }
+}

+ 78 - 102
core/src/main/java/org/springframework/security/util/FilterChainProxy.java

@@ -15,11 +15,10 @@
 
 package org.springframework.security.util;
 
-import org.springframework.security.ConfigAttribute;
-import org.springframework.security.ConfigAttributeDefinition;
-
 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;
@@ -34,12 +33,6 @@ import org.springframework.util.Assert;
 
 import java.io.IOException;
 
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Set;
-import java.util.Vector;
-
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -49,22 +42,48 @@ import javax.servlet.ServletResponse;
 
 
 /**
- * Delegates <code>Filter</code> requests to a list of Spring-managed beans.<p>The <code>FilterChainProxy</code> is
- * loaded via a standard {@link org.springframework.security.util.FilterToBeanProxy} declaration in <code>web.xml</code>.
+ * Delegates <code>Filter</code> requests to a list of Spring-managed beans.
+ * As of version 2.0, you shouldn't need to explicitly configure a <tt>FilterChainProxy</tt> bean in your application
+ * context unless you need very fine control over the filter chain contents. Most cases should be adequately covered
+ * by the default <tt>&lt;security:http /&gt</tt> namespace configuration options.
+ *
+ * <p>The <code>FilterChainProxy</code> is loaded via a standard
+ * {@link org.springframework.security.util.FilterToBeanProxy} declaration in <code>web.xml</code>.
  * <code>FilterChainProxy</code> will then pass {@link #init(FilterConfig)}, {@link #destroy()} and {@link
  * #doFilter(ServletRequest, ServletResponse, FilterChain)} invocations through to each <code>Filter</code> defined
  * against <code>FilterChainProxy</code>.</p>
- *  <p><code>FilterChainProxy</code> is configured using a standard {@link
- * org.springframework.security.intercept.web.FilterInvocationDefinitionSource}. Each possible URI pattern that
- * <code>FilterChainProxy</code> should service must be entered. The first matching URI pattern located by
- * <code>FilterInvocationDefinitionSource</code> for a given request will be used to define all of the
+ *
+ * <p>As of version 2.0, <tt>FilterChainProxy</tt> is configured using a {@link FilterChainMap}. 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 <tt>FilterChainMap</tt> instance will be
+ * created while parsing the namespace configuration, so it doesn't require an explicit bean declaration.
+ * Instead the &lt;filter-chain-map&gt; element should be used within the FilterChainProxy bean declaration.
+ * This in turn should have a list of child &lt;filter-chain&gt; 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.
+ * An example configuration might look like this:
+ *
+ * <pre>
+ &lt;bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
+     &lt;security:filter-chain-map pathType="ant">
+         &lt;security:filter-chain pattern="/do/not/filter" filters="none"/>
+         &lt;security:filter-chain pattern="/**" filters="filter1,filter2,filter3"/>
+     &lt;/security:filter-chain-map>
+ &lt;/bean>
+ * </pre>
+ *
+ * The names "filter1", "filter2", "filter3" should be the bean names of <tt>Filter</tt> instances defined in the
+ * application context. The order of the names defines the order in which the filters will be applied. As shown above,
+ * use of the value "none" for the "filters" can be used to exclude
+ * Please consult the security namespace schema file for a full list of available configuration options.
+ * </p>
+ *
+ *<p>
+ * Each possible URI pattern that <code>FilterChainProxy</code> should service must be entered.
+ * The first matching URI pattern for a given request will be used to define all of the
  * <code>Filter</code>s that apply to that request. NB: This means you must put most specific URI patterns at the top
  * of the list, and ensure all <code>Filter</code>s that should apply for a given URI pattern are entered against the
  * respective entry. The <code>FilterChainProxy</code> will not iterate the remainder of the URI patterns to locate
- * additional <code>Filter</code>s.  The <code>FilterInvocationDefinitionSource</code> described the applicable URI
- * pattern to fire the filter chain, followed by a list of configuration attributes. Each configuration attribute's
- * {@link org.springframework.security.ConfigAttribute#getAttribute()} corresponds to a bean name that is available from the
- * application context.</p>
+ * additional <code>Filter</code>s.</p>
  *  <p><code>FilterChainProxy</code> respects normal handling of <code>Filter</code>s that elect not to call {@link
  * javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
  * javax.servlet.FilterChain)}, in that the remainder of the origial or <code>FilterChainProxy</code>-declared filter
@@ -73,14 +92,14 @@ 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 <code>FilterToBeanProxy</code> will
  * never call this class' {@link #init(FilterConfig)} and {@link #destroy()} methods, meaning each of the filters
- * defined against <code>FilterInvocationDefinitionSource</code> will not be called. If you do need your filters to be
+ * defined in the FilterChainMap will not be called. If you do need your filters to be
  * initialized and destroyed, please set the <code>lifecycle</code> initialization parameter against the
  * <code>FilterToBeanProxy</code> to specify servlet container lifecycle management.</p>
- *  <p>If a filter name of {@link #TOKEN_NONE} is used, this allows specification of a filter pattern which should
- * never cause any filters to fire.</p>
  *
  * @author Carlos Sanchez
  * @author Ben Alex
+ * @author Luke Taylor
+ *
  * @version $Id$
  */
 public class FilterChainProxy implements Filter, InitializingBean, ApplicationContextAware {
@@ -92,19 +111,22 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo
     //~ Instance fields ================================================================================================
 
     private ApplicationContext applicationContext;
-    private FilterInvocationDefinitionSource filterInvocationDefinitionSource;
-
+    private FilterChainMap filterChainMap;
+    private FilterInvocationDefinitionSource fids;
     //~ Methods ========================================================================================================
 
     public void afterPropertiesSet() throws Exception {
-        Assert.notNull(filterInvocationDefinitionSource, "filterInvocationDefinitionSource must be specified");
-        Assert.notNull(this.filterInvocationDefinitionSource.getConfigAttributeDefinitions(),
-            "FilterChainProxy requires the FilterInvocationDefinitionSource to return a non-null response to "
-                    + "getConfigAttributeDefinitions()");
+        // Convert the FilterDefinitionSource to a filterChainMap if set
+        if (fids != null) {
+            Assert.isNull(filterChainMap, "Set the FilterChainMap or FilterInvocationDefinitionSource but not both");
+            setFilterChainMap(new FIDSToFilterChainMapConverter(fids, applicationContext).getFilterChainMap());
+        }
+
+        Assert.notNull(filterChainMap, "A FilterChainMap must be supplied");
     }
 
     public void destroy() {
-        Filter[] filters = obtainAllDefinedFilters();
+        Filter[] filters = filterChainMap.getAllDefinedFilters();
 
         for (int i = 0; i < filters.length; i++) {
             if (filters[i] != null) {
@@ -118,26 +140,16 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo
     }
 
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
-        throws IOException, ServletException {
-        FilterInvocation fi = new FilterInvocation(request, response, chain);
-
-        ConfigAttributeDefinition cad = this.filterInvocationDefinitionSource.getAttributes(fi);
-
-        if (cad == null) {
-            if (logger.isDebugEnabled()) {
-                logger.debug(fi.getRequestUrl() + " has no matching filters");
-            }
-
-            chain.doFilter(request, response);
+            throws IOException, ServletException {
 
-            return;
-        }
+        FilterInvocation fi = new FilterInvocation(request, response, chain);
 
-        Filter[] filters = obtainAllDefinedFilters(cad);
+        Filter[] filters = filterChainMap.getFilters(fi.getRequestUrl());
 
-        if (filters.length == 0) {
+        if (filters == null || filters.length == 0) {
             if (logger.isDebugEnabled()) {
-                logger.debug(fi.getRequestUrl() + " has an empty filter list");
+                logger.debug(fi.getRequestUrl() +
+                        filters == null ? " has no matching filters" : " has an empty filter list");
             }
 
             chain.doFilter(request, response);
@@ -149,12 +161,8 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo
         virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse());
     }
 
-    public FilterInvocationDefinitionSource getFilterInvocationDefinitionSource() {
-        return filterInvocationDefinitionSource;
-    }
-
     public void init(FilterConfig filterConfig) throws ServletException {
-        Filter[] filters = obtainAllDefinedFilters();
+        Filter[] filters = filterChainMap.getAllDefinedFilters();
 
         for (int i = 0; i < filters.length; i++) {
             if (filters[i] != null) {
@@ -168,70 +176,39 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo
     }
 
     /**
-     * Obtains all of the <b>unique</b><code>Filter</code> instances registered against the
-     * <code>FilterInvocationDefinitionSource</code>.<p>This is useful in ensuring a <code>Filter</code> is not
+     * Obtains all of the <b>unique</b><code>Filter</code> instances registered in the
+     * <code>FilterChainMap</code>.
+     * <p>This is useful in ensuring a <code>Filter</code> is not
      * initialized or destroyed twice.</p>
      *
-     * @return all of the <code>Filter</code> instances in the application context for which there has been an entry
-     *         against the <code>FilterInvocationDefinitionSource</code> (only one entry is included in the array for
+     * @deprecated
+     * @return all of the <code>Filter</code> instances in the application context which have an entry
+     *         in the <code>FilterChainMap</code> (only one entry is included in the array for
      *         each <code>Filter</code> that actually exists in application context, even if a given
-     *         <code>Filter</code> is defined multiples times by the <code>FilterInvocationDefinitionSource</code>)
+     *         <code>Filter</code> is defined multiples times by the <code>FilterChainMap</code>)
      */
     protected Filter[] obtainAllDefinedFilters() {
-        Iterator cads = this.filterInvocationDefinitionSource.getConfigAttributeDefinitions();
-        Set list = new LinkedHashSet();
-
-        while (cads.hasNext()) {
-            ConfigAttributeDefinition attribDef = (ConfigAttributeDefinition) cads.next();
-            Filter[] filters = obtainAllDefinedFilters(attribDef);
-
-            for (int i = 0; i < filters.length; i++) {
-                list.add(filters[i]);
-            }
-        }
+        return filterChainMap.getAllDefinedFilters();
+    }
 
-        return (Filter[]) list.toArray(new Filter[0]);
+    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
+        this.applicationContext = applicationContext;
     }
 
     /**
-     * Obtains all of the <code>Filter</code> instances registered against the specified
-     * <code>ConfigAttributeDefinition</code>.
-     *
-     * @param configAttributeDefinition for which we want to obtain associated <code>Filter</code>s
-     *
-     * @return the <code>Filter</code>s against the specified <code>ConfigAttributeDefinition</code> (never
-     *         <code>null</code>)
      *
-     * @throws IllegalArgumentException DOCUMENT ME!
+     * @deprecated Use namespace configuration or call setFilterChainMap instead.
      */
-    private Filter[] obtainAllDefinedFilters(ConfigAttributeDefinition configAttributeDefinition) {
-        List list = new Vector();
-        Iterator attributes = configAttributeDefinition.getConfigAttributes();
-
-        while (attributes.hasNext()) {
-            ConfigAttribute attr = (ConfigAttribute) attributes.next();
-            String filterName = attr.getAttribute();
-
-            if (filterName == null) {
-                throw new IllegalArgumentException("Configuration attribute: '" + attr
-                    + "' returned null to the getAttribute() method, which is invalid when used with FilterChainProxy");
-            }
-
-            if (!filterName.equals(TOKEN_NONE)) {
-                list.add(this.applicationContext.getBean(filterName, Filter.class));
-            }
-        }
-
-        return (Filter[]) list.toArray(new Filter[list.size()]);
+    public void setFilterInvocationDefinitionSource(FilterInvocationDefinitionSource fids) {
+        this.fids = fids;
     }
 
-    public void setApplicationContext(ApplicationContext applicationContext)
-        throws BeansException {
-        this.applicationContext = applicationContext;
+    public void setFilterChainMap(FilterChainMap filterChainMap) {
+        this.filterChainMap = filterChainMap;
     }
 
-    public void setFilterInvocationDefinitionSource(FilterInvocationDefinitionSource filterInvocationDefinitionSource) {
-        this.filterInvocationDefinitionSource = filterInvocationDefinitionSource;
+    public FilterChainMap getFilterChainMap() {
+        return filterChainMap;
     }
 
     //~ Inner Classes ==================================================================================================
@@ -242,7 +219,7 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo
      * <code>FilterChain</code> is used by <code>FilterChainProxy</code> to determine if the next <code>Filter</code>
      * should be called or not.</p>
      */
-    private class VirtualFilterChain implements FilterChain {
+    private static class VirtualFilterChain implements FilterChain {
         private FilterInvocation fi;
         private Filter[] additionalFilters;
         private int currentPosition = 0;
@@ -252,8 +229,6 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo
             this.additionalFilters = additionalFilters;
         }
 
-        private VirtualFilterChain() {}
-
         public void doFilter(ServletRequest request, ServletResponse response)
             throws IOException, ServletException {
             if (currentPosition == additionalFilters.length) {
@@ -276,4 +251,5 @@ public class FilterChainProxy implements Filter, InitializingBean, ApplicationCo
             }
         }
     }
+
 }

+ 41 - 0
core/src/main/java/org/springframework/security/util/RegexUrlPathMatcher.java

@@ -0,0 +1,41 @@
+package org.springframework.security.util;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import java.util.regex.Pattern;
+
+/**
+ * @author luke
+ * @version $Id$
+ */
+public class RegexUrlPathMatcher implements UrlMatcher {
+    private static final Log logger = LogFactory.getLog(RegexUrlPathMatcher.class);
+
+    private boolean convertUrlToLowercaseBeforeComparison = true;
+
+    public Object compile(String path) {
+        return Pattern.compile(path);
+    }
+
+    public void setConvertUrlToLowercaseBeforeComparison(boolean convertUrlToLowercaseBeforeComparison) {
+        this.convertUrlToLowercaseBeforeComparison = convertUrlToLowercaseBeforeComparison;
+    }
+
+    public boolean pathMatchesUrl(Object compiledPath, String url) {
+        Pattern pattern = (Pattern)compiledPath;
+
+        if (convertUrlToLowercaseBeforeComparison) {
+            url = url.toLowerCase();
+            if (logger.isDebugEnabled()) {
+                logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
+            }
+        }
+
+        return pattern.matcher(url).matches();
+    }
+
+    public String getUniversalMatchPattern() {
+        return "/.*";
+    }
+}

+ 18 - 0
core/src/main/java/org/springframework/security/util/UrlMatcher.java

@@ -0,0 +1,18 @@
+package org.springframework.security.util;
+
+/**
+ * Strategy for deciding whether configured path matches a submitted candidate URL.
+ *
+ * @author luke
+ * @version $Id$
+ * @since 2.0
+ */
+public interface UrlMatcher {
+
+    Object compile(String urlPattern);
+
+    boolean pathMatchesUrl(Object compiledUrlPattern, String url);
+
+    /** Returns the path which matches every URL */
+    String getUniversalMatchPattern();
+}

+ 42 - 41
core/src/test/java/org/springframework/security/util/FilterChainProxyTests.java

@@ -15,23 +15,20 @@
 
 package org.springframework.security.util;
 
-import junit.framework.TestCase;
-
+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;
 import org.springframework.security.ConfigAttribute;
 import org.springframework.security.ConfigAttributeDefinition;
 import org.springframework.security.MockApplicationContext;
 import org.springframework.security.MockFilterConfig;
-
-import org.springframework.security.intercept.web.FilterInvocationDefinitionSource;
 import org.springframework.security.intercept.web.MockFilterInvocationDefinitionSource;
 import org.springframework.security.intercept.web.PathBasedFilterInvocationDefinitionMap;
 
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.support.ClassPathXmlApplicationContext;
-
-import org.springframework.mock.web.MockHttpServletRequest;
-import org.springframework.mock.web.MockHttpServletResponse;
-
 
 /**
  * Tests {@link FilterChainProxy}.
@@ -40,32 +37,30 @@ import org.springframework.mock.web.MockHttpServletResponse;
  * @author Ben Alex
  * @version $Id$
  */
-public class FilterChainProxyTests extends TestCase {
-    //~ Constructors ===================================================================================================
+public class FilterChainProxyTests {
+    private ClassPathXmlApplicationContext appCtx;
 
-    // ===========================================================
-    public FilterChainProxyTests() {
-        super();
-    }
+    //~ Methods ========================================================================================================
 
-    public FilterChainProxyTests(String arg0) {
-        super(arg0);
+    @Before
+    public void loadContext() {
+        appCtx = new ClassPathXmlApplicationContext("org/springframework/security/util/filtertest-valid.xml");
     }
 
-    //~ Methods ========================================================================================================
-
-    // ================================================================
-    public static void main(String[] args) {
-        junit.textui.TestRunner.run(FilterChainProxyTests.class);
+    @After
+    public void closeContext() {
+        if (appCtx != null) {
+            appCtx.close();
+        }
     }
 
-    public void testDetectsFilterInvocationDefinitionSourceThatDoesNotReturnAllConfigAttributes()
-        throws Exception {
+    @Test
+    public void testDetectsFilterInvocationDefinitionSourceThatDoesNotReturnAllConfigAttributes() throws Exception {
         FilterChainProxy filterChainProxy = new FilterChainProxy();
         filterChainProxy.setApplicationContext(MockApplicationContext.getContext());
-        filterChainProxy.setFilterInvocationDefinitionSource(new MockFilterInvocationDefinitionSource(false, false));
 
         try {
+            filterChainProxy.setFilterInvocationDefinitionSource(new MockFilterInvocationDefinitionSource(false, false));
             filterChainProxy.afterPropertiesSet();
             fail("Should have thrown IllegalArgumentException");
         } catch (IllegalArgumentException expected) {
@@ -74,8 +69,8 @@ public class FilterChainProxyTests extends TestCase {
         }
     }
 
-    public void testDetectsIfConfigAttributeDoesNotReturnValueForGetAttributeMethod()
-        throws Exception {
+    @Test
+    public void testDetectsIfConfigAttributeDoesNotReturnValueForGetAttributeMethod() throws Exception {
         FilterChainProxy filterChainProxy = new FilterChainProxy();
         filterChainProxy.setApplicationContext(MockApplicationContext.getContext());
 
@@ -86,9 +81,9 @@ public class FilterChainProxyTests extends TestCase {
         fids.addSecureUrl("/**", cad);
 
         filterChainProxy.setFilterInvocationDefinitionSource(fids);
-        filterChainProxy.afterPropertiesSet();
 
         try {
+            filterChainProxy.afterPropertiesSet();
             filterChainProxy.init(new MockFilterConfig());
             fail("Should have thrown IllegalArgumentException");
         } catch (IllegalArgumentException expected) {
@@ -97,8 +92,8 @@ public class FilterChainProxyTests extends TestCase {
         }
     }
 
-    public void testDetectsMissingFilterInvocationDefinitionSource()
-        throws Exception {
+    @Test
+    public void testDetectsMissingFilterInvocationDefinitionSource() throws Exception {
         FilterChainProxy filterChainProxy = new FilterChainProxy();
         filterChainProxy.setApplicationContext(MockApplicationContext.getContext());
 
@@ -106,12 +101,11 @@ public class FilterChainProxyTests extends TestCase {
             filterChainProxy.afterPropertiesSet();
             fail("Should have thrown IllegalArgumentException");
         } catch (IllegalArgumentException expected) {
-            assertEquals("filterInvocationDefinitionSource must be specified", expected.getMessage());
         }
     }
 
+    @Test
     public void testDoNotFilter() throws Exception {
-        ApplicationContext appCtx = new ClassPathXmlApplicationContext("org/springframework/security/util/filtertest-valid.xml");
         FilterChainProxy filterChainProxy = (FilterChainProxy) appCtx.getBean("filterChain", FilterChainProxy.class);
         MockFilter filter = (MockFilter) appCtx.getBean("mockFilter", MockFilter.class);
 
@@ -127,16 +121,22 @@ public class FilterChainProxyTests extends TestCase {
         assertFalse(filter.isWasDestroyed());
     }
 
-    public void testGettersSetters() {
-        FilterChainProxy filterChainProxy = new FilterChainProxy();
-        FilterInvocationDefinitionSource fids = new MockFilterInvocationDefinitionSource(false, false);
-        filterChainProxy.setFilterInvocationDefinitionSource(fids);
-        assertEquals(fids, filterChainProxy.getFilterInvocationDefinitionSource());
+    @Test    
+    public void normalOperation() throws Exception {
+        doNormalOperation((FilterChainProxy) appCtx.getBean("filterChain", FilterChainProxy.class));
     }
 
-    public void testNormalOperation() throws Exception {
-        ApplicationContext appCtx = new ClassPathXmlApplicationContext("org/springframework/security/util/filtertest-valid.xml");
-        FilterChainProxy filterChainProxy = (FilterChainProxy) appCtx.getBean("filterChain", FilterChainProxy.class);
+    @Test
+    public void normalOperationWithNewConfig() throws Exception {
+        doNormalOperation((FilterChainProxy) appCtx.getBean("newFilterChainProxy", FilterChainProxy.class));
+    }
+
+    @Test
+    public void normalOperationWithNewConfigRegex() throws Exception {
+        doNormalOperation((FilterChainProxy) appCtx.getBean("newFilterChainProxyRegex", FilterChainProxy.class));
+    }
+
+    private void doNormalOperation(FilterChainProxy filterChainProxy) throws Exception {
         MockFilter filter = (MockFilter) appCtx.getBean("mockFilter", MockFilter.class);
         assertFalse(filter.isWasInitialized());
         assertFalse(filter.isWasDoFiltered());
@@ -165,6 +165,7 @@ public class FilterChainProxyTests extends TestCase {
         assertTrue(filter.isWasInitialized());
         assertTrue(filter.isWasDoFiltered());
         assertTrue(filter.isWasDestroyed());
+
     }
 
     //~ Inner Classes ==================================================================================================

+ 24 - 3
core/src/test/resources/org/springframework/security/util/filtertest-valid.xml

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
+
 <!--
  * Copyright 2004 Acegi Technology Pty Limited
  *
@@ -18,8 +18,11 @@
  *
  * $Id$
 -->
-
-<beans>
+<beans xmlns="http://www.springframework.org/schema/beans"
+    xmlns:sec="http://www.springframework.org/schema/security"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
+http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
 
 	<bean id="mockFilter" class="org.springframework.security.util.MockFilter"/>
 
@@ -39,4 +42,22 @@
       </property>
     </bean>
 
+    <bean id="newFilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
+        <sec:filter-chain-map pathType="ant">
+            <sec:filter-chain pattern="/foo/**" filters="mockFilter"/>
+            <sec:filter-chain pattern="/some/other/path/**" filters="sif,mockFilter,mockFilter2"/>
+            <sec:filter-chain pattern="/do/not/filter" filters="none"/>
+        </sec:filter-chain-map>
+    </bean>
+
+    <bean id="newFilterChainProxyRegex" class="org.springframework.security.util.FilterChainProxy">
+        <sec:filter-chain-map pathType="regex">
+            <sec:filter-chain pattern="\A/foo/.*\Z" filters="mockFilter"/>
+            <sec:filter-chain pattern="\A/some/other/path/.*\Z" filters="sif,mockFilter,mockFilter2"/>
+            <sec:filter-chain pattern="\A/do/not/filter\Z" filters="none"/>
+        </sec:filter-chain-map>
+    </bean>
+
+    <bean id="sif" class="org.springframework.security.context.HttpSessionContextIntegrationFilter"/>
+
 </beans>