View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.springframework.security.util;
17  
18  import org.apache.commons.logging.Log;
19  import org.apache.commons.logging.LogFactory;
20  import org.springframework.beans.BeansException;
21  import org.springframework.beans.factory.InitializingBean;
22  import org.springframework.context.ApplicationContext;
23  import org.springframework.context.ApplicationContextAware;
24  import org.springframework.security.intercept.web.*;
25  import org.springframework.util.Assert;
26  import org.springframework.web.filter.DelegatingFilterProxy;
27  
28  import javax.servlet.*;
29  import java.io.IOException;
30  import java.util.*;
31  
32  
33  /**
34   * Delegates <code>Filter</code> requests to a list of Spring-managed beans.
35   * As of version 2.0, you shouldn't need to explicitly configure a <tt>FilterChainProxy</tt> bean in your application
36   * context unless you need very fine control over the filter chain contents. Most cases should be adequately covered
37   * by the default <tt>&lt;security:http /&gt</tt> namespace configuration options.
38   *
39   * <p>The <code>FilterChainProxy</code> is loaded via a standard Spring {@link DelegatingFilterProxy} declaration in
40   * <code>web.xml</code>. <code>FilterChainProxy</code> will then pass {@link #init(FilterConfig)}, {@link #destroy()}
41   * and {@link #doFilter(ServletRequest, ServletResponse, FilterChain)} invocations through to each <code>Filter</code>
42   * defined against <code>FilterChainProxy</code>.
43   *
44   * <p>As of version 2.0, <tt>FilterChainProxy</tt> is configured using an ordered Map of path patterns to <tt>List</tt>s
45   * of <tt>Filter</tt> objects. In previous
46   * versions, a {@link FilterInvocationDefinitionSource} was used. This is now deprecated in favour of namespace-based
47   * configuration which provides a more robust and simplfied syntax.  The Map instance will normally be
48   * created while parsing the namespace configuration, so doesn't have to be set explicitly.
49   * Instead the &lt;filter-chain-map&gt; element should be used within the FilterChainProxy bean declaration.
50   * This in turn should have a list of child &lt;filter-chain&gt; elements which each define a URI pattern and the list
51   * of filters (as comma-separated bean names) which should be applied to requests which match the pattern.
52   * An example configuration might look like this:
53   *
54   * <pre>
55   &lt;bean id="myfilterChainProxy" class="org.springframework.security.util.FilterChainProxy">
56       &lt;security:filter-chain-map pathType="ant">
57           &lt;security:filter-chain pattern="/do/not/filter" filters="none"/>
58           &lt;security:filter-chain pattern="/**" filters="filter1,filter2,filter3"/>
59       &lt;/security:filter-chain-map>
60   &lt;/bean>
61   * </pre>
62   *
63   * The names "filter1", "filter2", "filter3" should be the bean names of <tt>Filter</tt> instances defined in the
64   * application context. The order of the names defines the order in which the filters will be applied. As shown above,
65   * use of the value "none" for the "filters" can be used to exclude
66   * Please consult the security namespace schema file for a full list of available configuration options.
67   *
68   * <p>
69   * Each possible URI pattern that <code>FilterChainProxy</code> should service must be entered.
70   * The first matching URI pattern for a given request will be used to define all of the
71   * <code>Filter</code>s that apply to that request. NB: This means you must put most specific URI patterns at the top
72   * of the list, and ensure all <code>Filter</code>s that should apply for a given URI pattern are entered against the
73   * respective entry. The <code>FilterChainProxy</code> will not iterate the remainder of the URI patterns to locate
74   * additional <code>Filter</code>s.
75   *
76   * <p><code>FilterChainProxy</code> respects normal handling of <code>Filter</code>s that elect not to call {@link
77   * javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse,
78   * javax.servlet.FilterChain)}, in that the remainder of the original or <code>FilterChainProxy</code>-declared filter
79   * chain will not be called.
80   *
81   * <p>Note the <code>Filter</code> lifecycle mismatch between the servlet container and IoC
82   * container. As described in the {@link DelegatingFilterProxy} JavaDocs, we recommend you allow the IoC
83   * container to manage the lifecycle instead of the servlet container. By default the <code>DelegatingFilterProxy</code>
84   * will never call this class' {@link #init(FilterConfig)} and {@link #destroy()} methods, which in turns means that
85   * the corresponding methods on the filter beans managed by this class will never be called. If you do need your filters to be
86   * initialized and destroyed, please set the <tt>targetFilterLifecycle</tt> initialization parameter against the
87   * <code>DelegatingFilterProxy</code> to specify that servlet container lifecycle management should be used. You don't
88   * need to worry about this in most cases.
89   *
90   * @author Carlos Sanchez
91   * @author Ben Alex
92   * @author Luke Taylor
93   *
94   * @version $Id: FilterChainProxy.java 3245 2008-08-11 19:15:33Z luke_t $
95   */
96  public class FilterChainProxy implements Filter, InitializingBean, ApplicationContextAware {
97      //~ Static fields/initializers =====================================================================================
98  
99      private static final Log logger = LogFactory.getLog(FilterChainProxy.class);
100     public static final String TOKEN_NONE = "#NONE#";
101 
102     //~ Instance fields ================================================================================================
103 
104     private ApplicationContext applicationContext;
105     /** Map of the original pattern Strings to filter chains */
106     private Map uncompiledFilterChainMap;
107     /** Compiled pattern version of the filter chain map */
108     private Map filterChainMap;
109     private UrlMatcher matcher = new AntUrlPathMatcher();
110     private boolean stripQueryStringFromUrls = true;
111     private DefaultFilterInvocationDefinitionSource fids;
112 
113     //~ Methods ========================================================================================================
114 
115     public void afterPropertiesSet() throws Exception {
116         // Convert the FilterDefinitionSource to a filterChainMap if set
117         if (fids != null) {
118             Assert.isNull(uncompiledFilterChainMap, "Set the filterChainMap or FilterInvocationDefinitionSource but not both");
119             FIDSToFilterChainMapConverter converter = new FIDSToFilterChainMapConverter(fids, applicationContext);
120             setMatcher(converter.getMatcher());
121             setFilterChainMap(converter.getFilterChainMap());
122             fids = null;
123         }
124 
125         Assert.notNull(uncompiledFilterChainMap, "filterChainMap must be set");
126 
127     }
128 
129     public void init(FilterConfig filterConfig) throws ServletException {
130         Filter[] filters = obtainAllDefinedFilters();
131 
132         for (int i = 0; i < filters.length; i++) {
133             if (filters[i] != null) {
134                 if (logger.isDebugEnabled()) {
135                     logger.debug("Initializing Filter defined in ApplicationContext: '" + filters[i].toString() + "'");
136                 }
137 
138                 filters[i].init(filterConfig);
139             }
140         }
141     }
142 
143     public void destroy() {
144         Filter[] filters = obtainAllDefinedFilters();
145 
146         for (int i = 0; i < filters.length; i++) {
147             if (filters[i] != null) {
148                 if (logger.isDebugEnabled()) {
149                     logger.debug("Destroying Filter defined in ApplicationContext: '" + filters[i].toString() + "'");
150                 }
151 
152                 filters[i].destroy();
153             }
154         }
155     }
156 
157     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
158             throws IOException, ServletException {
159 
160         FilterInvocation fi = new FilterInvocation(request, response, chain);
161         List filters = getFilters(fi.getRequestUrl());
162 
163         if (filters == null || filters.size() == 0) {
164             if (logger.isDebugEnabled()) {
165                 logger.debug(fi.getRequestUrl() +
166                         filters == null ? " has no matching filters" : " has an empty filter list");
167             }
168 
169             chain.doFilter(request, response);
170 
171             return;
172         }
173 
174         VirtualFilterChain virtualFilterChain = new VirtualFilterChain(fi, filters);
175         virtualFilterChain.doFilter(fi.getRequest(), fi.getResponse());
176     }
177 
178     /**
179      * Returns the first filter chain matching the supplied URL.
180      *
181      * @param url the request URL
182      * @return an ordered array of Filters defining the filter chain
183      */
184     public List getFilters(String url)  {
185         if (stripQueryStringFromUrls) {
186             // String query string - see SEC-953
187             int firstQuestionMarkIndex = url.indexOf("?");
188 
189             if (firstQuestionMarkIndex != -1) {
190                 url = url.substring(0, firstQuestionMarkIndex);
191             }
192         }
193 
194 
195         Iterator filterChains = filterChainMap.entrySet().iterator();
196 
197         while (filterChains.hasNext()) {
198             Map.Entry entry = (Map.Entry) filterChains.next();
199             Object path = entry.getKey();
200 
201             if (matcher.requiresLowerCaseUrl()) {
202                 url = url.toLowerCase();
203 
204                 if (logger.isDebugEnabled()) {
205                     logger.debug("Converted URL to lowercase, from: '" + url + "'; to: '" + url + "'");
206                 }
207             }
208 
209             boolean matched = matcher.pathMatchesUrl(path, url);
210 
211             if (logger.isDebugEnabled()) {
212                 logger.debug("Candidate is: '" + url + "'; pattern is " + path + "; matched=" + matched);
213             }
214 
215             if (matched) {
216                 return (List) entry.getValue();
217             }
218         }
219 
220         return null;
221     }
222 
223     /**
224      * Obtains all of the <b>unique</b><code>Filter</code> instances registered in the map of
225      * filter chains.
226      * <p>This is useful in ensuring a <code>Filter</code> is not initialized or destroyed twice.</p>
227      *
228      * @return all of the <code>Filter</code> instances in the application context which have an entry
229      *         in the map (only one entry is included in the array for
230      *         each <code>Filter</code> that actually exists in application context, even if a given
231      *         <code>Filter</code> is defined multiples times in the filter chain map)
232      */
233     protected Filter[] obtainAllDefinedFilters() {
234         Set allFilters = new LinkedHashSet();
235 
236         Iterator it = filterChainMap.values().iterator();
237 
238         while (it.hasNext()) {
239             allFilters.addAll((List) it.next());
240         }
241 
242         return (Filter[]) new ArrayList(allFilters).toArray(new Filter[0]);
243     }
244 
245     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
246         this.applicationContext = applicationContext;
247     }
248 
249     /**
250      *
251      * @deprecated Use namespace configuration or call setFilterChainMap instead.
252      */
253     public void setFilterInvocationDefinitionSource(FilterInvocationDefinitionSource fids) {
254         Assert.isInstanceOf(DefaultFilterInvocationDefinitionSource.class, fids,
255                 "Must be a DefaultFilterInvocationDefinitionSource");
256         this.fids = (DefaultFilterInvocationDefinitionSource) fids;
257     }
258 
259     /**
260      * Sets the mapping of URL patterns to filter chains.
261      *
262      * The map keys should be the paths and the values should be arrays of <tt>Filter</tt> objects.
263      * It's VERY important that the type of map used preserves ordering - the order in which the iterator
264      * returns the entries must be the same as the order they were added to the map, otherwise you have no way
265      * of guaranteeing that the most specific patterns are returned before the more general ones. So make sure
266      * the Map used is an instance of <tt>LinkedHashMap</tt> or an equivalent, rather than a plain <tt>HashMap</tt>, for
267      * example.
268      *
269      * @param filterChainMap the map of path Strings to <tt>Filter[]</tt>s.
270      */
271     public void setFilterChainMap(Map filterChainMap) {
272         uncompiledFilterChainMap = new LinkedHashMap(filterChainMap);
273         checkPathOrder();
274         createCompiledMap();
275     }
276 
277     private void checkPathOrder() {
278         // Check that the universal pattern is listed at the end, if at all
279         String[] paths = (String[]) uncompiledFilterChainMap.keySet().toArray(new String[0]);
280         String universalMatch = matcher.getUniversalMatchPattern();
281 
282         for (int i=0; i < paths.length-1; i++) {
283             if (paths[i].equals(universalMatch)) {
284                 throw new IllegalArgumentException("A universal match pattern " + universalMatch + " is defined " +
285                         " before other patterns in the filter chain, causing them to be ignored. Please check the " +
286                         "ordering in your <security:http> namespace or FilterChainProxy bean configuration");
287             }
288         }
289     }
290 
291     private void createCompiledMap() {
292         Iterator paths = uncompiledFilterChainMap.keySet().iterator();
293         filterChainMap = new LinkedHashMap(uncompiledFilterChainMap.size());
294 
295         while (paths.hasNext()) {
296             Object path = paths.next();
297             Assert.isInstanceOf(String.class, path, "Path pattern must be a String");
298             Object compiledPath = matcher.compile((String)path);
299             Object filters = uncompiledFilterChainMap.get(path);
300 
301             Assert.isInstanceOf(List.class, filters);
302             // Check the contents
303             Iterator filterIterator = ((List)filters).iterator();
304 
305             while (filterIterator.hasNext()) {
306                 Object filter = filterIterator.next();
307                 Assert.isInstanceOf(Filter.class, filter, "Objects in filter chain must be of type Filter. ");
308             }
309 
310             filterChainMap.put(compiledPath, filters);
311         }
312     }
313 
314 
315     /**
316      * Returns a copy of the underlying filter chain map. Modifications to the map contents
317      * will not affect the FilterChainProxy state - to change the map call <tt>setFilterChainMap</tt>.
318      *
319      * @return the map of path pattern Strings to filter chain arrays (with ordering guaranteed).
320      */
321     public Map getFilterChainMap() {
322         return new LinkedHashMap(uncompiledFilterChainMap);
323     }
324 
325     public void setMatcher(UrlMatcher matcher) {
326         this.matcher = matcher;
327     }
328 
329     public UrlMatcher getMatcher() {
330         return matcher;
331     }
332 
333     /**
334      * If set to 'true', the query string will be stripped from the request URL before
335      * attempting to find a matching filter chain. This is the default value.
336      */
337     public void setStripQueryStringFromUrls(boolean stripQueryStringFromUrls) {
338         this.stripQueryStringFromUrls = stripQueryStringFromUrls;
339     }
340 
341     public String toString() {
342         StringBuffer sb = new StringBuffer();
343         sb.append("FilterChainProxy[");
344         sb.append(" UrlMatcher = ").append(matcher);
345         sb.append("; Filter Chains: ");
346         sb.append(uncompiledFilterChainMap);
347         sb.append("]");
348 
349         return sb.toString();
350     }
351 
352     //~ Inner Classes ==================================================================================================
353 
354     /**
355      * A <code>FilterChain</code> that records whether or not {@link
356      * FilterChain#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} is called.<p>This
357      * <code>FilterChain</code> is used by <code>FilterChainProxy</code> to determine if the next <code>Filter</code>
358      * should be called or not.</p>
359      */
360     private static class VirtualFilterChain implements FilterChain {
361         private FilterInvocation fi;
362         private List additionalFilters;
363         private int currentPosition = 0;
364 
365         private VirtualFilterChain(FilterInvocation filterInvocation, List additionalFilters) {
366             this.fi = filterInvocation;
367             this.additionalFilters = additionalFilters;
368         }
369 
370         public void doFilter(ServletRequest request, ServletResponse response)
371             throws IOException, ServletException {
372             if (currentPosition == additionalFilters.size()) {
373                 if (logger.isDebugEnabled()) {
374                     logger.debug(fi.getRequestUrl()
375                         + " reached end of additional filter chain; proceeding with original chain");
376                 }
377 
378                 fi.getChain().doFilter(request, response);
379             } else {
380                 currentPosition++;
381 
382                 Filter nextFilter = (Filter) additionalFilters.get(currentPosition - 1);
383 
384                 if (logger.isDebugEnabled()) {
385                     logger.debug(fi.getRequestUrl() + " at position " + currentPosition + " of "
386                         + additionalFilters.size() + " in additional filter chain; firing Filter: '"
387                         + nextFilter + "'");
388                 }
389 
390                nextFilter.doFilter(request, response, this);
391             }
392         }
393     }
394 
395 }