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;
20  
21  import org.apache.shiro.util.AntPathMatcher;
22  import org.apache.shiro.util.PatternMatcher;
23  import org.apache.shiro.util.StringUtils;
24  import org.apache.shiro.web.servlet.AdviceFilter;
25  import org.apache.shiro.web.util.WebUtils;
26  import org.owasp.encoder.Encode;
27  import org.slf4j.Logger;
28  import org.slf4j.LoggerFactory;
29  
30  import javax.servlet.Filter;
31  import javax.servlet.ServletRequest;
32  import javax.servlet.ServletResponse;
33  import java.util.LinkedHashMap;
34  import java.util.Map;
35  
36  import static org.apache.shiro.util.StringUtils.split;
37  
38  /**
39   * <p>Base class for Filters that will process only specified paths and allow all others to pass through.</p>
40   *
41   * @since 0.9
42   */
43  public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor {
44  
45      /**
46       * Log available to this class only
47       */
48      private static final Logger log = LoggerFactory.getLogger(PathMatchingFilter.class);
49  
50      private static final String DEFAULT_PATH_SEPARATOR = "/";
51  
52      /**
53       * PatternMatcher used in determining which paths to react to for a given request.
54       */
55      protected PatternMatcher pathMatcher = new AntPathMatcher();
56  
57      /**
58       * A collection of path-to-config entries where the key is a path which this filter should process and
59       * the value is the (possibly null) configuration element specific to this Filter for that specific path.
60       * <p/>
61       * <p>To put it another way, the keys are the paths (urls) that this Filter will process.
62       * <p>The values are filter-specific data that this Filter should use when processing the corresponding
63       * key (path).  The values can be null if no Filter-specific config was specified for that url.
64       */
65      protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>();
66  
67      /**
68       * Splits any comma-delmited values that might be found in the <code>config</code> argument and sets the resulting
69       * <code>String[]</code> array on the <code>appliedPaths</code> internal Map.
70       * <p/>
71       * That is:
72       * <pre><code>
73       * String[] values = null;
74       * if (config != null) {
75       *     values = split(config);
76       * }
77       * <p/>
78       * this.{@link #appliedPaths appliedPaths}.put(path, values);
79       * </code></pre>
80       *
81       * @param path   the application context path to match for executing this filter.
82       * @param config the specified for <em>this particular filter only</em> for the given <code>path</code>
83       * @return this configured filter.
84       */
85      public Filter processPathConfig(String path, String config) {
86          String[] values = null;
87          if (config != null) {
88              values = split(config);
89          }
90  
91          this.appliedPaths.put(path, values);
92          return this;
93      }
94  
95      /**
96       * Returns the context path within the application based on the specified <code>request</code>.
97       * <p/>
98       * This implementation merely delegates to
99       * {@link WebUtils#getPathWithinApplication(javax.servlet.http.HttpServletRequest) WebUtils.getPathWithinApplication(request)},
100      * but can be overridden by subclasses for custom logic.
101      *
102      * @param request the incoming <code>ServletRequest</code>
103      * @return the context path within the application.
104      */
105     protected String getPathWithinApplication(ServletRequest request) {
106         return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
107     }
108 
109     /**
110      * Returns <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
111      * <code>false</code> otherwise.
112      * <p/>
113      * The default implementation acquires the <code>request</code>'s path within the application and determines
114      * if that matches:
115      * <p/>
116      * <code>String requestURI = {@link #getPathWithinApplication(javax.servlet.ServletRequest) getPathWithinApplication(request)};<br/>
117      * return {@link #pathsMatch(String, String) pathsMatch(path,requestURI)}</code>
118      *
119      * @param path    the configured url pattern to check the incoming request against.
120      * @param request the incoming ServletRequest
121      * @return <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern,
122      *         <code>false</code> otherwise.
123      */
124     protected boolean pathsMatch(String path, ServletRequest request) {
125         String requestURI = getPathWithinApplication(request);
126         if (requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI)
127                 && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) {
128             requestURI = requestURI.substring(0, requestURI.length() - 1);
129         }
130         if (path != null && !DEFAULT_PATH_SEPARATOR.equals(path)
131                 && path.endsWith(DEFAULT_PATH_SEPARATOR)) {
132             path = path.substring(0, path.length() - 1);
133         }
134         log.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, Encode.forHtml(requestURI));
135         return pathsMatch(path, requestURI);
136     }
137 
138     /**
139      * Returns <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
140      * <code>false</code> otherwise.
141      * <p/>
142      * Simply delegates to
143      * <b><code>this.pathMatcher.{@link PatternMatcher#matches(String, String) matches(pattern,path)}</code></b>,
144      * but can be overridden by subclasses for custom matching behavior.
145      *
146      * @param pattern the pattern to match against
147      * @param path    the value to match with the specified <code>pattern</code>
148      * @return <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string,
149      *         <code>false</code> otherwise.
150      */
151     protected boolean pathsMatch(String pattern, String path) {
152         return pathMatcher.matches(pattern, path);
153     }
154 
155     /**
156      * Implementation that handles path-matching behavior before a request is evaluated.  If the path matches and
157      * the filter
158      * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object) isEnabled} for
159      * that path/config, the request will be allowed through via the result from
160      * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle}.  If the
161      * path does not match or the filter is not enabled for that path, this filter will allow passthrough immediately
162      * to allow the {@code FilterChain} to continue executing.
163      * <p/>
164      * In order to retain path-matching functionality, subclasses should not override this method if at all
165      * possible, and instead override
166      * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle} instead.
167      *
168      * @param request  the incoming ServletRequest
169      * @param response the outgoing ServletResponse
170      * @return {@code true} if the filter chain is allowed to continue to execute, {@code false} if a subclass has
171      *         handled the request explicitly.
172      * @throws Exception if an error occurs
173      */
174     protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
175 
176         if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {
177             if (log.isTraceEnabled()) {
178                 log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");
179             }
180             return true;
181         }
182 
183         for (String path : this.appliedPaths.keySet()) {
184             // If the path does match, then pass on to the subclass implementation for specific checks
185             //(first match 'wins'):
186             if (pathsMatch(path, request)) {
187                 log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
188                 Object config = this.appliedPaths.get(path);
189                 return isFilterChainContinued(request, response, path, config);
190             }
191         }
192 
193         //no path matched, allow the request to go through:
194         return true;
195     }
196 
197     /**
198      * Simple method to abstract out logic from the preHandle implementation - it was getting a bit unruly.
199      *
200      * @since 1.2
201      */
202     @SuppressWarnings({"JavaDoc"})
203     private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,
204                                            String path, Object pathConfig) throws Exception {
205 
206         if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2
207             if (log.isTraceEnabled()) {
208                 log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}].  " +
209                         "Delegating to subclass implementation for 'onPreHandle' check.",
210                         new Object[]{getName(), path, pathConfig});
211             }
212             //The filter is enabled for this specific request, so delegate to subclass implementations
213             //so they can decide if the request should continue through the chain or not:
214             return onPreHandle(request, response, pathConfig);
215         }
216 
217         if (log.isTraceEnabled()) {
218             log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}].  " +
219                     "The next element in the FilterChain will be called immediately.",
220                     new Object[]{getName(), path, pathConfig});
221         }
222         //This filter is disabled for this specific request,
223         //return 'true' immediately to indicate that the filter will not process the request
224         //and let the request/response to continue through the filter chain:
225         return true;
226     }
227 
228     /**
229      * This default implementation always returns {@code true} and should be overridden by subclasses for custom
230      * logic if necessary.
231      *
232      * @param request     the incoming ServletRequest
233      * @param response    the outgoing ServletResponse
234      * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings.
235      * @return {@code true} if the request should be able to continue, {@code false} if the filter will
236      *         handle the response directly.
237      * @throws Exception if an error occurs
238      * @see #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object)
239      */
240     protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
241         return true;
242     }
243 
244     /**
245      * Path-matching version of the parent class's
246      * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method, but additionally allows
247      * for inspection of any path-specific configuration values corresponding to the specified request.  Subclasses
248      * may wish to inspect this additional mapped configuration to determine if the filter is enabled or not.
249      * <p/>
250      * This method's default implementation ignores the {@code path} and {@code mappedValue} arguments and merely
251      * returns the value from a call to {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}.
252      * It is expected that subclasses override this method if they need to perform enable/disable logic for a specific
253      * request based on any path-specific config for the filter instance.
254      *
255      * @param request     the incoming servlet request
256      * @param response    the outbound servlet response
257      * @param path        the path matched for the incoming servlet request that has been configured with the given {@code mappedValue}.
258      * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings for the given {@code path}.
259      * @return {@code true} if this filter should filter the specified request, {@code false} if it should let the
260      *         request/response pass through immediately to the next element in the {@code FilterChain}.
261      * @throws Exception in the case of any error
262      * @since 1.2
263      */
264     @SuppressWarnings({"UnusedParameters"})
265     protected boolean isEnabled(ServletRequest request, ServletResponse response, String path, Object mappedValue)
266             throws Exception {
267         return isEnabled(request, response);
268     }
269 }