diff --git a/core/src/main/java/org/springframework/security/config/FilterChainMapBeanDefinitionDecorator.java b/core/src/main/java/org/springframework/security/config/FilterChainMapBeanDefinitionDecorator.java new file mode 100644 index 0000000000..3cc7113709 --- /dev/null +++ b/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 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); + } + } + } + } +} 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 new file mode 100644 index 0000000000..3a6eccb5df --- /dev/null +++ b/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; + } +} diff --git a/core/src/main/java/org/springframework/security/intercept/web/FilterChainMap.java b/core/src/main/java/org/springframework/security/intercept/web/FilterChainMap.java new file mode 100644 index 0000000000..d41e6f7e2f --- /dev/null +++ b/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 uniqueFilter instances registered in the + * FilterChainMap. + *

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

+ * @return all of the Filter instances which have an entry + * in the FilterChainMap (only one entry is included in the array for + * each Filter instance, even if a given + * Filter is used multiples times by the FilterChainMap) + */ + 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]); + } +} diff --git a/core/src/main/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMap.java b/core/src/main/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMap.java index 88c2e063fd..ea7da72e21 100644 --- a/core/src/main/java/org/springframework/security/intercept/web/PathBasedFilterInvocationDefinitionMap.java +++ b/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 { diff --git a/core/src/main/java/org/springframework/security/intercept/web/RegExpBasedFilterInvocationDefinitionMap.java b/core/src/main/java/org/springframework/security/intercept/web/RegExpBasedFilterInvocationDefinitionMap.java index c5aacd0af7..ec08b3a005 100644 --- a/core/src/main/java/org/springframework/security/intercept/web/RegExpBasedFilterInvocationDefinitionMap.java +++ b/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 { diff --git a/core/src/main/java/org/springframework/security/util/AntUrlPathMatcher.java b/core/src/main/java/org/springframework/security/util/AntUrlPathMatcher.java new file mode 100644 index 0000000000..6ba621c28d --- /dev/null +++ b/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 "/**"; + } +} 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 f5faf1c80c..158cdd11d1 100644 --- a/core/src/main/java/org/springframework/security/util/FilterChainProxy.java +++ b/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 Filter requests to a list of Spring-managed beans.

The FilterChainProxy is - * loaded via a standard {@link org.springframework.security.util.FilterToBeanProxy} declaration in web.xml. + * Delegates Filter requests to a list of Spring-managed beans. + * As of version 2.0, you shouldn't need to explicitly configure a FilterChainProxy 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 <security:http /> namespace configuration options. + * + *

The FilterChainProxy is loaded via a standard + * {@link org.springframework.security.util.FilterToBeanProxy} declaration in web.xml. * FilterChainProxy will then pass {@link #init(FilterConfig)}, {@link #destroy()} and {@link * #doFilter(ServletRequest, ServletResponse, FilterChain)} invocations through to each Filter defined * against FilterChainProxy.

- *

FilterChainProxy is configured using a standard {@link - * org.springframework.security.intercept.web.FilterInvocationDefinitionSource}. Each possible URI pattern that - * FilterChainProxy should service must be entered. The first matching URI pattern located by - * FilterInvocationDefinitionSource for a given request will be used to define all of the + * + *

As of version 2.0, FilterChainProxy 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 FilterChainMap instance will be + * created while parsing the namespace configuration, so it doesn't require an explicit bean declaration. + * 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. + * An example configuration might look like this: + * + *

+ <bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
+     <security:filter-chain-map pathType="ant">
+         <security:filter-chain pattern="/do/not/filter" filters="none"/>
+         <security:filter-chain pattern="/**" filters="filter1,filter2,filter3"/>
+     </security:filter-chain-map>
+ </bean>
+ * 
+ * + * The names "filter1", "filter2", "filter3" should be the bean names of Filter 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. + *

+ * + *

+ * Each possible URI pattern that FilterChainProxy should service must be entered. + * The first matching URI pattern for a given request will be used to define all of the * Filters that apply to that request. NB: This means you must put most specific URI patterns at the top * of the list, and ensure all Filters that should apply for a given URI pattern are entered against the * respective entry. The FilterChainProxy will not iterate the remainder of the URI patterns to locate - * additional Filters. The FilterInvocationDefinitionSource 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.

+ * additional Filters.

*

FilterChainProxy respects normal handling of Filters 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 FilterChainProxy-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 FilterToBeanProxy will * never call this class' {@link #init(FilterConfig)} and {@link #destroy()} methods, meaning each of the filters - * defined against FilterInvocationDefinitionSource 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 lifecycle initialization parameter against the * FilterToBeanProxy to specify servlet container lifecycle management.

- *

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.

* * @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 uniqueFilter instances registered against the - * FilterInvocationDefinitionSource.

This is useful in ensuring a Filter is not + * Obtains all of the uniqueFilter instances registered in the + * FilterChainMap. + *

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

* - * @return all of the Filter instances in the application context for which there has been an entry - * against the FilterInvocationDefinitionSource (only one entry is included in the array for + * @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 * each Filter that actually exists in application context, even if a given - * Filter is defined multiples times by the FilterInvocationDefinitionSource) + * Filter is defined multiples times by the FilterChainMap) */ 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 Filter instances registered against the specified - * ConfigAttributeDefinition. - * - * @param configAttributeDefinition for which we want to obtain associated Filters - * - * @return the Filters against the specified ConfigAttributeDefinition (never - * null) * - * @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 * FilterChain is used by FilterChainProxy to determine if the next Filter * should be called or not.

*/ - 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 } } } + } diff --git a/core/src/main/java/org/springframework/security/util/RegexUrlPathMatcher.java b/core/src/main/java/org/springframework/security/util/RegexUrlPathMatcher.java new file mode 100644 index 0000000000..8776a95ec0 --- /dev/null +++ b/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 "/.*"; + } +} diff --git a/core/src/main/java/org/springframework/security/util/UrlMatcher.java b/core/src/main/java/org/springframework/security/util/UrlMatcher.java new file mode 100644 index 0000000000..1345353401 --- /dev/null +++ b/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(); +} 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 7d758b75dd..744874707d 100644 --- a/core/src/test/java/org/springframework/security/util/FilterChainProxyTests.java +++ b/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 ================================================================================================== 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 f000cf5ef0..082bb95f52 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 @@ -1,5 +1,5 @@ - + - - + @@ -39,4 +42,22 @@ + + + + + + + + + + + + + + + + + +