001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.shiro.web.filter; 020 021import org.apache.shiro.util.AntPathMatcher; 022import org.apache.shiro.util.PatternMatcher; 023import org.apache.shiro.web.servlet.AdviceFilter; 024import org.apache.shiro.web.util.WebUtils; 025import org.owasp.encoder.Encode; 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028 029import javax.servlet.Filter; 030import javax.servlet.ServletRequest; 031import javax.servlet.ServletResponse; 032import java.util.LinkedHashMap; 033import java.util.Map; 034 035import static org.apache.shiro.lang.util.StringUtils.split; 036 037/** 038 * <p>Base class for Filters that will process only specified paths and allow all others to pass through.</p> 039 * 040 * @since 0.9 041 */ 042public abstract class PathMatchingFilter extends AdviceFilter implements PathConfigProcessor { 043 044 /** 045 * Log available to this class only 046 */ 047 private static final Logger LOGGER = LoggerFactory.getLogger(PathMatchingFilter.class); 048 049 private static final String DEFAULT_PATH_SEPARATOR = "/"; 050 051 /** 052 * PatternMatcher used in determining which paths to react to for a given request. 053 */ 054 protected PatternMatcher pathMatcher = new AntPathMatcher(); 055 056 /** 057 * A collection of path-to-config entries where the key is a path which this filter should process and 058 * the value is the (possibly null) configuration element specific to this Filter for that specific path. 059 * <p/> 060 * <p>To put it another way, the keys are the paths (urls) that this Filter will process. 061 * <p>The values are filter-specific data that this Filter should use when processing the corresponding 062 * key (path). The values can be null if no Filter-specific config was specified for that url. 063 */ 064 protected Map<String, Object> appliedPaths = new LinkedHashMap<String, Object>(); 065 066 /** 067 * Splits any comma-delimited values that might be found in the <code>config</code> argument and sets the resulting 068 * <code>String[]</code> array on the <code>appliedPaths</code> internal Map. 069 * <p/> 070 * That is: 071 * <pre><code> 072 * String[] values = null; 073 * if (config != null) { 074 * values = split(config); 075 * } 076 * <p/> 077 * this.{@link #appliedPaths appliedPaths}.put(path, values); 078 * </code></pre> 079 * 080 * @param path the application context path to match for executing this filter. 081 * @param config the specified for <em>this particular filter only</em> for the given <code>path</code> 082 * @return this configured filter. 083 */ 084 public Filter processPathConfig(String path, String config) { 085 String[] values = null; 086 if (config != null) { 087 values = split(config); 088 } 089 090 this.appliedPaths.put(path, values); 091 return this; 092 } 093 094 @Override 095 public void setCaseInsensitive(boolean caseInsensitive) { 096 if (pathMatcher != null) { 097 pathMatcher.setCaseInsensitive(caseInsensitive); 098 } 099 } 100 101 /** 102 * Returns the context path within the application based on the specified <code>request</code>. 103 * <p/> 104 * This implementation merely delegates to 105 * {@link WebUtils#getPathWithinApplication(javax.servlet.http.HttpServletRequest) 106 * WebUtils.getPathWithinApplication(request)}, 107 * but can be overridden by subclasses for custom logic. 108 * 109 * @param request the incoming <code>ServletRequest</code> 110 * @return the context path within the application. 111 */ 112 protected String getPathWithinApplication(ServletRequest request) { 113 return WebUtils.getPathWithinApplication(WebUtils.toHttp(request)); 114 } 115 116 /** 117 * Returns <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern, 118 * <code>false</code> otherwise. 119 * <p/> 120 * The default implementation acquires the <code>request</code>'s path within the application and determines 121 * if that matches: 122 * <p/> 123 * <code>String requestURI = {@link #getPathWithinApplication(ServletRequest) getPathWithinApplication(request)};<br/> 124 * return {@link #pathsMatch(String, String) pathsMatch(path,requestURI)}</code> 125 * 126 * @param path the configured url pattern to check the incoming request against. 127 * @param request the incoming ServletRequest 128 * @return <code>true</code> if the incoming <code>request</code> matches the specified <code>path</code> pattern, 129 * <code>false</code> otherwise. 130 */ 131 protected boolean pathsMatch(String path, ServletRequest request) { 132 String requestURI = getPathWithinApplication(request); 133 134 LOGGER.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, Encode.forHtml(requestURI)); 135 boolean match = pathsMatch(path, requestURI); 136 137 if (!match) { 138 if (requestURI != null && !DEFAULT_PATH_SEPARATOR.equals(requestURI) 139 && requestURI.endsWith(DEFAULT_PATH_SEPARATOR)) { 140 requestURI = requestURI.substring(0, requestURI.length() - 1); 141 } 142 if (path != null && !DEFAULT_PATH_SEPARATOR.equals(path) 143 && path.endsWith(DEFAULT_PATH_SEPARATOR)) { 144 path = path.substring(0, path.length() - 1); 145 } 146 LOGGER.trace("Attempting to match pattern '{}' with current requestURI '{}'...", path, Encode.forHtml(requestURI)); 147 match = pathsMatch(path, requestURI); 148 } 149 150 return match; 151 } 152 153 /** 154 * Returns <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string, 155 * <code>false</code> otherwise. 156 * <p/> 157 * Simply delegates to 158 * <b><code>this.pathMatcher.{@link PatternMatcher#matches(String, String) matches(pattern,path)}</code></b>, 159 * but can be overridden by subclasses for custom matching behavior. 160 * 161 * @param pattern the pattern to match against 162 * @param path the value to match with the specified <code>pattern</code> 163 * @return <code>true</code> if the <code>path</code> matches the specified <code>pattern</code> string, 164 * <code>false</code> otherwise. 165 */ 166 protected boolean pathsMatch(String pattern, String path) { 167 boolean matches = pathMatcher.matches(pattern, path); 168 LOGGER.trace("Pattern [{}] matches path [{}] => [{}]", pattern, path, matches); 169 return matches; 170 } 171 172 /** 173 * Implementation that handles path-matching behavior before a request is evaluated. If the path matches and 174 * the filter 175 * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object) isEnabled} for 176 * that path/config, the request will be allowed through via the result from 177 * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle}. If the 178 * path does not match or the filter is not enabled for that path, this filter will allow passthrough immediately 179 * to allow the {@code FilterChain} to continue executing. 180 * <p/> 181 * In order to retain path-matching functionality, subclasses should not override this method if at all 182 * possible, and instead override 183 * {@link #onPreHandle(javax.servlet.ServletRequest, javax.servlet.ServletResponse, Object) onPreHandle} instead. 184 * 185 * @param request the incoming ServletRequest 186 * @param response the outgoing ServletResponse 187 * @return {@code true} if the filter chain is allowed to continue to execute, {@code false} if a subclass has 188 * handled the request explicitly. 189 * @throws Exception if an error occurs 190 */ 191 protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { 192 193 if (this.appliedPaths == null || this.appliedPaths.isEmpty()) { 194 if (LOGGER.isTraceEnabled()) { 195 LOGGER.trace("appliedPaths property is null or empty. This Filter will passthrough immediately."); 196 } 197 return true; 198 } 199 200 for (String path : this.appliedPaths.keySet()) { 201 // If the path does match, then pass on to the subclass implementation for specific checks 202 //(first match 'wins'): 203 if (pathsMatch(path, request)) { 204 LOGGER.trace("Current requestURI matches pattern '{}'. Determining filter chain execution...", path); 205 Object config = this.appliedPaths.get(path); 206 return isFilterChainContinued(request, response, path, config); 207 } 208 } 209 210 //no path matched, allow the request to go through: 211 return true; 212 } 213 214 /** 215 * Simple method to abstract out logic from the preHandle implementation - it was getting a bit unruly. 216 * 217 * @since 1.2 218 */ 219 @SuppressWarnings({"JavaDoc"}) 220 private boolean isFilterChainContinued(ServletRequest request, ServletResponse response, 221 String path, Object pathConfig) throws Exception { 222 223 //isEnabled check added in 1.2 224 if (isEnabled(request, response, path, pathConfig)) { 225 if (LOGGER.isTraceEnabled()) { 226 LOGGER.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}]. " 227 + "Delegating to subclass implementation for 'onPreHandle' check.", 228 getName(), path, pathConfig); 229 } 230 //The filter is enabled for this specific request, so delegate to subclass implementations 231 //so they can decide if the request should continue through the chain or not: 232 return onPreHandle(request, response, pathConfig); 233 } 234 235 if (LOGGER.isTraceEnabled()) { 236 LOGGER.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}]. " 237 + "The next element in the FilterChain will be called immediately.", 238 getName(), path, pathConfig); 239 } 240 //This filter is disabled for this specific request, 241 //return 'true' immediately to indicate that the filter will not process the request 242 //and let the request/response to continue through the filter chain: 243 return true; 244 } 245 246 /** 247 * This default implementation always returns {@code true} and should be overridden by subclasses for custom 248 * logic if necessary. 249 * 250 * @param request the incoming ServletRequest 251 * @param response the outgoing ServletResponse 252 * @param mappedValue the filter-specific config value mapped to this filter in the URL rules mappings. 253 * @return {@code true} if the request should be able to continue, {@code false} if the filter will 254 * handle the response directly. 255 * @throws Exception if an error occurs 256 * @see #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse, String, Object) 257 */ 258 protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { 259 return true; 260 } 261 262 @SuppressWarnings("UnusedParameters") 263 /** 264 * Path-matching version of the parent class's 265 * {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} method, but additionally allows 266 * for inspection of any path-specific configuration values corresponding to the specified request. Subclasses 267 * may wish to inspect this additional mapped configuration to determine if the filter is enabled or not. 268 * <p/> 269 * This method's default implementation ignores the {@code path} and {@code mappedValue} arguments and merely 270 * returns the value from a call to {@link #isEnabled(javax.servlet.ServletRequest, javax.servlet.ServletResponse)}. 271 * It is expected that subclasses override this method if they need to perform enable/disable logic for a specific 272 * request based on any path-specific config for the filter instance. 273 * 274 * @param request the incoming servlet request 275 * @param response the outbound servlet response 276 * @param path the path matched for the incoming servlet request 277 * that has been configured with the given {@code mappedValue}. 278 * @param mappedValue the filter-specific config value mapped to 279 * this filter in the URL rules mappings for the given {@code path}. 280 * @return {@code true} if this filter should filter the specified request, {@code false} if it should let the 281 * request/response pass through immediately to the next element in the {@code FilterChain}. 282 * @throws Exception in the case of any error 283 * @since 1.2 284 */ 285 protected boolean isEnabled(ServletRequest request, ServletResponse response, String path, Object mappedValue) 286 throws Exception { 287 return isEnabled(request, response); 288 } 289}