View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.filter.mgt;
20  
21  import org.apache.shiro.config.ConfigurationException;
22  import org.apache.shiro.util.CollectionUtils;
23  import org.apache.shiro.util.Nameable;
24  import org.apache.shiro.util.StringUtils;
25  import org.apache.shiro.web.filter.PathConfigProcessor;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import javax.servlet.Filter;
30  import javax.servlet.FilterChain;
31  import javax.servlet.FilterConfig;
32  import javax.servlet.ServletException;
33  import java.util.Collections;
34  import java.util.LinkedHashMap;
35  import java.util.Map;
36  import java.util.Set;
37  
38  /**
39   * Default {@link FilterChainManager} implementation maintaining a map of {@link Filter Filter} instances
40   * (key: filter name, value: Filter) as well as a map of {@link NamedFilterList NamedFilterList}s created from these
41   * {@code Filter}s (key: filter chain name, value: NamedFilterList).  The {@code NamedFilterList} is essentially a
42   * {@link FilterChain} that also has a name property by which it can be looked up.
43   *
44   * @see NamedFilterList
45   * @since 1.0
46   */
47  public class DefaultFilterChainManager implements FilterChainManager {
48  
49      private static transient final Logger log = LoggerFactory.getLogger(DefaultFilterChainManager.class);
50  
51      private FilterConfig filterConfig;
52  
53      private Map<String, Filter> filters; //pool of filters available for creating chains
54  
55      private Map<String, NamedFilterList> filterChains; //key: chain name, value: chain
56  
57      public DefaultFilterChainManager() {
58          this.filters = new LinkedHashMap<String, Filter>();
59          this.filterChains = new LinkedHashMap<String, NamedFilterList>();
60          addDefaultFilters(false);
61      }
62  
63      public DefaultFilterChainManager(FilterConfig filterConfig) {
64          this.filters = new LinkedHashMap<String, Filter>();
65          this.filterChains = new LinkedHashMap<String, NamedFilterList>();
66          setFilterConfig(filterConfig);
67          addDefaultFilters(true);
68      }
69  
70      /**
71       * Returns the {@code FilterConfig} provided by the Servlet container at webapp startup.
72       *
73       * @return the {@code FilterConfig} provided by the Servlet container at webapp startup.
74       */
75      public FilterConfig getFilterConfig() {
76          return filterConfig;
77      }
78  
79      /**
80       * Sets the {@code FilterConfig} provided by the Servlet container at webapp startup.
81       *
82       * @param filterConfig the {@code FilterConfig} provided by the Servlet container at webapp startup.
83       */
84      public void setFilterConfig(FilterConfig filterConfig) {
85          this.filterConfig = filterConfig;
86      }
87  
88      public Map<String, Filter> getFilters() {
89          return filters;
90      }
91  
92      @SuppressWarnings({"UnusedDeclaration"})
93      public void setFilters(Map<String, Filter> filters) {
94          this.filters = filters;
95      }
96  
97      public Map<String, NamedFilterList> getFilterChains() {
98          return filterChains;
99      }
100 
101     @SuppressWarnings({"UnusedDeclaration"})
102     public void setFilterChains(Map<String, NamedFilterList> filterChains) {
103         this.filterChains = filterChains;
104     }
105 
106     public Filter getFilter(String name) {
107         return this.filters.get(name);
108     }
109 
110     public void addFilter(String name, Filter filter) {
111         addFilter(name, filter, false);
112     }
113 
114     public void addFilter(String name, Filter filter, boolean init) {
115         addFilter(name, filter, init, true);
116     }
117 
118     public void createChain(String chainName, String chainDefinition) {
119         if (!StringUtils.hasText(chainName)) {
120             throw new NullPointerException("chainName cannot be null or empty.");
121         }
122         if (!StringUtils.hasText(chainDefinition)) {
123             throw new NullPointerException("chainDefinition cannot be null or empty.");
124         }
125 
126         if (log.isDebugEnabled()) {
127             log.debug("Creating chain [" + chainName + "] from String definition [" + chainDefinition + "]");
128         }
129 
130         //parse the value by tokenizing it to get the resulting filter-specific config entries
131         //
132         //e.g. for a value of
133         //
134         //     "authc, roles[admin,user], perms[file:edit]"
135         //
136         // the resulting token array would equal
137         //
138         //     { "authc", "roles[admin,user]", "perms[file:edit]" }
139         //
140         String[] filterTokens = splitChainDefinition(chainDefinition);
141 
142         //each token is specific to each filter.
143         //strip the name and extract any filter-specific config between brackets [ ]
144         for (String token : filterTokens) {
145             String[] nameConfigPair = toNameConfigPair(token);
146 
147             //now we have the filter name, path and (possibly null) path-specific config.  Let's apply them:
148             addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
149         }
150     }
151 
152     /**
153      * Splits the comma-delimited filter chain definition line into individual filter definition tokens.
154      * <p/>
155      * Example Input:
156      * <pre>
157      *     foo, bar[baz], blah[x, y]
158      * </pre>
159      * Resulting Output:
160      * <pre>
161      *     output[0] == foo
162      *     output[1] == bar[baz]
163      *     output[2] == blah[x, y]
164      * </pre>
165      * @param chainDefinition the comma-delimited filter chain definition.
166      * @return an array of filter definition tokens
167      * @since 1.2
168      * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
169      */
170     protected String[] splitChainDefinition(String chainDefinition) {
171         return StringUtils.split(chainDefinition, StringUtils.DEFAULT_DELIMITER_CHAR, '[', ']', true, true);
172     }
173 
174     /**
175      * Based on the given filter chain definition token (e.g. 'foo' or 'foo[bar, baz]'), this will return the token
176      * as a name/value pair, removing any brackets as necessary.  Examples:
177      * <table>
178      *     <tr>
179      *         <th>Input</th>
180      *         <th>Result</th>
181      *     </tr>
182      *     <tr>
183      *         <td>{@code foo}</td>
184      *         <td>returned[0] == {@code foo}<br/>returned[1] == {@code null}</td>
185      *     </tr>
186      *     <tr>
187      *         <td>{@code foo[bar, baz]}</td>
188      *         <td>returned[0] == {@code foo}<br/>returned[1] == {@code bar, baz}</td>
189      *     </tr>
190      * </table>
191      * @param token the filter chain definition token
192      * @return A name/value pair representing the filter name and a (possibly null) config value.
193      * @throws ConfigurationException if the token cannot be parsed
194      * @since 1.2
195      * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
196      */
197     protected String[] toNameConfigPair(String token) throws ConfigurationException {
198 
199         try {
200             String[] pair = token.split("\\[", 2);
201             String name = StringUtils.clean(pair[0]);
202 
203             if (name == null) {
204                 throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token);
205             }
206             String config = null;
207 
208             if (pair.length == 2) {
209                 config = StringUtils.clean(pair[1]);
210                 //if there was an open bracket, it assumed there is a closing bracket, so strip it too:
211                 config = config.substring(0, config.length() - 1);
212                 config = StringUtils.clean(config);
213 
214                 //backwards compatibility prior to implementing SHIRO-205:
215                 //prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets
216                 //if that config required commas.  We need to strip those quotes to get to the interior quoted definition
217                 //to ensure any existing quoted definitions still function for end users:
218                 if (config != null && config.startsWith("\"") && config.endsWith("\"")) {
219                     String stripped = config.substring(1, config.length() - 1);
220                     stripped = StringUtils.clean(stripped);
221 
222                     //if the stripped value does not have any internal quotes, we can assume that the entire config was
223                     //quoted and we can use the stripped value.
224                     if (stripped != null && stripped.indexOf('"') == -1) {
225                         config = stripped;
226                     }
227                     //else:
228                     //the remaining config does have internal quotes, so we need to assume that each comma delimited
229                     //pair might be quoted, in which case we need the leading and trailing quotes that we stripped
230                     //So we ignore the stripped value.
231                 }
232             }
233             
234             return new String[]{name, config};
235 
236         } catch (Exception e) {
237             String msg = "Unable to parse filter chain definition token: " + token;
238             throw new ConfigurationException(msg, e);
239         }
240     }
241 
242     protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
243         Filter existing = getFilter(name);
244         if (existing == null || overwrite) {
245             if (filter instanceof Nameable) {
246                 ((Nameable) filter).setName(name);
247             }
248             if (init) {
249                 initFilter(filter);
250             }
251             this.filters.put(name, filter);
252         }
253     }
254 
255     public void addToChain(String chainName, String filterName) {
256         addToChain(chainName, filterName, null);
257     }
258 
259     public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
260         if (!StringUtils.hasText(chainName)) {
261             throw new IllegalArgumentException("chainName cannot be null or empty.");
262         }
263         Filter filter = getFilter(filterName);
264         if (filter == null) {
265             throw new IllegalArgumentException("There is no filter with name '" + filterName +
266                     "' to apply to chain [" + chainName + "] in the pool of available Filters.  Ensure a " +
267                     "filter with that name/path has first been registered with the addFilter method(s).");
268         }
269 
270         applyChainConfig(chainName, filter, chainSpecificFilterConfig);
271 
272         NamedFilterList chain = ensureChain(chainName);
273         chain.add(filter);
274     }
275 
276     protected void applyChainConfig(String chainName, Filter filter, String chainSpecificFilterConfig) {
277         if (log.isDebugEnabled()) {
278             log.debug("Attempting to apply path [" + chainName + "] to filter [" + filter + "] " +
279                     "with config [" + chainSpecificFilterConfig + "]");
280         }
281         if (filter instanceof PathConfigProcessor) {
282             ((PathConfigProcessor) filter).processPathConfig(chainName, chainSpecificFilterConfig);
283         } else {
284             if (StringUtils.hasText(chainSpecificFilterConfig)) {
285                 //they specified a filter configuration, but the Filter doesn't implement PathConfigProcessor
286                 //this is an erroneous config:
287                 String msg = "chainSpecificFilterConfig was specified, but the underlying " +
288                         "Filter instance is not an 'instanceof' " +
289                         PathConfigProcessor.class.getName() + ".  This is required if the filter is to accept " +
290                         "chain-specific configuration.";
291                 throw new ConfigurationException(msg);
292             }
293         }
294     }
295 
296     protected NamedFilterList ensureChain(String chainName) {
297         NamedFilterList chain = getChain(chainName);
298         if (chain == null) {
299             chain = new SimpleNamedFilterList(chainName);
300             this.filterChains.put(chainName, chain);
301         }
302         return chain;
303     }
304 
305     public NamedFilterList getChain(String chainName) {
306         return this.filterChains.get(chainName);
307     }
308 
309     public boolean hasChains() {
310         return !CollectionUtils.isEmpty(this.filterChains);
311     }
312 
313     public Set<String> getChainNames() {
314         //noinspection unchecked
315         return this.filterChains != null ? this.filterChains.keySet() : Collections.EMPTY_SET;
316     }
317 
318     public FilterChain proxy(FilterChain original, String chainName) {
319         NamedFilterList configured = getChain(chainName);
320         if (configured == null) {
321             String msg = "There is no configured chain under the name/key [" + chainName + "].";
322             throw new IllegalArgumentException(msg);
323         }
324         return configured.proxy(original);
325     }
326 
327     /**
328      * Initializes the filter by calling <code>filter.init( {@link #getFilterConfig() getFilterConfig()} );</code>.
329      *
330      * @param filter the filter to initialize with the {@code FilterConfig}.
331      */
332     protected void initFilter(Filter filter) {
333         FilterConfig filterConfig = getFilterConfig();
334         if (filterConfig == null) {
335             throw new IllegalStateException("FilterConfig attribute has not been set.  This must occur before filter " +
336                     "initialization can occur.");
337         }
338         try {
339             filter.init(filterConfig);
340         } catch (ServletException e) {
341             throw new ConfigurationException(e);
342         }
343     }
344 
345     protected void addDefaultFilters(boolean init) {
346         for (DefaultFilter defaultFilter : DefaultFilter.values()) {
347             addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
348         }
349     }
350 }