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.mgt; 020 021import org.apache.shiro.config.ConfigurationException; 022import org.apache.shiro.util.CollectionUtils; 023import org.apache.shiro.lang.util.Nameable; 024import org.apache.shiro.lang.util.StringUtils; 025import org.apache.shiro.web.filter.PathConfigProcessor; 026import org.slf4j.Logger; 027import org.slf4j.LoggerFactory; 028 029import javax.servlet.Filter; 030import javax.servlet.FilterChain; 031import javax.servlet.FilterConfig; 032import javax.servlet.ServletException; 033import java.util.ArrayList; 034import java.util.LinkedHashMap; 035import java.util.List; 036import java.util.Map; 037import java.util.Set; 038 039/** 040 * Default {@link FilterChainManager} implementation maintaining a map of {@link Filter Filter} instances 041 * (key: filter name, value: Filter) as well as a map of {@link NamedFilterList NamedFilterList}s created from these 042 * {@code Filter}s (key: filter chain name, value: NamedFilterList). The {@code NamedFilterList} is essentially a 043 * {@link FilterChain} that also has a name property by which it can be looked up. 044 * 045 * @see NamedFilterList 046 * @since 1.0 047 */ 048public class DefaultFilterChainManager implements FilterChainManager { 049 050 private static final Logger LOGGER = LoggerFactory.getLogger(DefaultFilterChainManager.class); 051 052 private FilterConfig filterConfig; 053 054 /** 055 * pool of filters available for creating chains 056 */ 057 private Map<String, Filter> filters; 058 059 /** 060 * list of filters to prepend to every chain 061 */ 062 private List<String> globalFilterNames; 063 064 /** 065 * key: chain name, value: chain 066 */ 067 private Map<String, NamedFilterList> filterChains; 068 069 private boolean caseInsensitive; 070 071 public DefaultFilterChainManager() { 072 this.filters = new LinkedHashMap<String, Filter>(); 073 this.filterChains = new LinkedHashMap<String, NamedFilterList>(); 074 this.globalFilterNames = new ArrayList<>(); 075 addDefaultFilters(false); 076 } 077 078 public DefaultFilterChainManager(FilterConfig filterConfig) { 079 this.filters = new LinkedHashMap<String, Filter>(); 080 this.filterChains = new LinkedHashMap<String, NamedFilterList>(); 081 this.globalFilterNames = new ArrayList<>(); 082 setFilterConfig(filterConfig); 083 addDefaultFilters(true); 084 } 085 086 /** 087 * Returns the {@code FilterConfig} provided by the Servlet container at webapp startup. 088 * 089 * @return the {@code FilterConfig} provided by the Servlet container at webapp startup. 090 */ 091 public FilterConfig getFilterConfig() { 092 return filterConfig; 093 } 094 095 /** 096 * Sets the {@code FilterConfig} provided by the Servlet container at webapp startup. 097 * 098 * @param filterConfig the {@code FilterConfig} provided by the Servlet container at webapp startup. 099 */ 100 public void setFilterConfig(FilterConfig filterConfig) { 101 this.filterConfig = filterConfig; 102 } 103 104 public Map<String, Filter> getFilters() { 105 return filters; 106 } 107 108 @SuppressWarnings({"UnusedDeclaration"}) 109 public void setFilters(Map<String, Filter> filters) { 110 this.filters = filters; 111 } 112 113 public Map<String, NamedFilterList> getFilterChains() { 114 return filterChains; 115 } 116 117 @SuppressWarnings({"UnusedDeclaration"}) 118 public void setFilterChains(Map<String, NamedFilterList> filterChains) { 119 this.filterChains = filterChains; 120 } 121 122 public Filter getFilter(String name) { 123 return this.filters.get(name); 124 } 125 126 @Override 127 public void setCaseInsensitive(boolean caseInsensitive) { 128 this.caseInsensitive = caseInsensitive; 129 } 130 131 public void addFilter(String name, Filter filter) { 132 addFilter(name, filter, false); 133 } 134 135 public void addFilter(String name, Filter filter, boolean init) { 136 addFilter(name, filter, init, true); 137 } 138 139 public void createDefaultChain(String chainName) { 140 // only create the defaultChain if we don't have a chain with this name already 141 // (the global filters will already be in that chain) 142 if (!getChainNames().contains(chainName) && !CollectionUtils.isEmpty(globalFilterNames)) { 143 // add each of global filters 144 globalFilterNames.stream().forEach(filterName -> addToChain(chainName, filterName)); 145 } 146 } 147 148 public void createChain(String chainName, String chainDefinition) { 149 if (!StringUtils.hasText(chainName)) { 150 throw new NullPointerException("chainName cannot be null or empty."); 151 } 152 if (!StringUtils.hasText(chainDefinition)) { 153 throw new NullPointerException("chainDefinition cannot be null or empty."); 154 } 155 156 if (LOGGER.isDebugEnabled()) { 157 LOGGER.debug("Creating chain [" + chainName + "] with global filters " 158 + globalFilterNames + " and from String definition [" 159 + chainDefinition + "]"); 160 } 161 162 // first add each of global filters 163 if (!CollectionUtils.isEmpty(globalFilterNames)) { 164 globalFilterNames.stream().forEach(filterName -> addToChain(chainName, filterName)); 165 } 166 167 //parse the value by tokenizing it to get the resulting filter-specific config entries 168 // 169 //e.g. for a value of 170 // 171 // "authc, roles[admin,user], perms[file:edit]" 172 // 173 // the resulting token array would equal 174 // 175 // { "authc", "roles[admin,user]", "perms[file:edit]" } 176 // 177 String[] filterTokens = splitChainDefinition(chainDefinition); 178 179 //each token is specific to each filter. 180 //strip the name and extract any filter-specific config between brackets [ ] 181 for (String token : filterTokens) { 182 String[] nameConfigPair = toNameConfigPair(token); 183 184 //now we have the filter name, path and (possibly null) path-specific config. Let's apply them: 185 addToChain(chainName, nameConfigPair[0], nameConfigPair[1]); 186 } 187 } 188 189 /** 190 * Splits the comma-delimited filter chain definition line into individual filter definition tokens. 191 * <p/> 192 * Example Input: 193 * <pre> 194 * foo, bar[baz], blah[x, y] 195 * </pre> 196 * Resulting Output: 197 * <pre> 198 * output[0] == foo 199 * output[1] == bar[baz] 200 * output[2] == blah[x, y] 201 * </pre> 202 * 203 * @param chainDefinition the comma-delimited filter chain definition. 204 * @return an array of filter definition tokens 205 * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a> 206 * @since 1.2 207 */ 208 protected String[] splitChainDefinition(String chainDefinition) { 209 return StringUtils.split(chainDefinition, StringUtils.DEFAULT_DELIMITER_CHAR, '[', ']', true, true); 210 } 211 212 /** 213 * Based on the given filter chain definition token (e.g. 'foo' or 'foo[bar, baz]'), this will return the token 214 * as a name/value pair, removing any brackets as necessary. Examples: 215 * <table> 216 * <tr> 217 * <th>Input</th> 218 * <th>Result</th> 219 * </tr> 220 * <tr> 221 * <td>{@code foo}</td> 222 * <td>returned[0] == {@code foo}<br/>returned[1] == {@code null}</td> 223 * </tr> 224 * <tr> 225 * <td>{@code foo[bar, baz]}</td> 226 * <td>returned[0] == {@code foo}<br/>returned[1] == {@code bar, baz}</td> 227 * </tr> 228 * </table> 229 * 230 * @param token the filter chain definition token 231 * @return A name/value pair representing the filter name and a (possibly null) config value. 232 * @throws ConfigurationException if the token cannot be parsed 233 * @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a> 234 * @since 1.2 235 */ 236 protected String[] toNameConfigPair(String token) throws ConfigurationException { 237 238 try { 239 String[] pair = token.split("\\[", 2); 240 String name = StringUtils.clean(pair[0]); 241 242 if (name == null) { 243 throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token); 244 } 245 String config = null; 246 247 if (pair.length == 2) { 248 config = StringUtils.clean(pair[1]); 249 //if there was an open bracket, it assumed there is a closing bracket, so strip it too: 250 config = config.substring(0, config.length() - 1); 251 config = StringUtils.clean(config); 252 253 //backwards compatibility prior to implementing SHIRO-205: 254 //prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets 255 //if that config required commas. We need to strip those quotes to get to the interior quoted definition 256 //to ensure any existing quoted definitions still function for end users: 257 if (config != null && config.startsWith("\"") && config.endsWith("\"")) { 258 String stripped = config.substring(1, config.length() - 1); 259 stripped = StringUtils.clean(stripped); 260 261 //if the stripped value does not have any internal quotes, we can assume that the entire config was 262 //quoted and we can use the stripped value. 263 if (stripped != null && stripped.indexOf('"') == -1) { 264 config = stripped; 265 } 266 //else: 267 //the remaining config does have internal quotes, so we need to assume that each comma delimited 268 //pair might be quoted, in which case we need the leading and trailing quotes that we stripped 269 //So we ignore the stripped value. 270 } 271 } 272 273 return new String[] {name, config}; 274 275 } catch (Exception e) { 276 String msg = "Unable to parse filter chain definition token: " + token; 277 throw new ConfigurationException(msg, e); 278 } 279 } 280 281 protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) { 282 Filter existing = getFilter(name); 283 if (existing == null || overwrite) { 284 if (filter instanceof Nameable) { 285 ((Nameable) filter).setName(name); 286 } 287 if (init) { 288 initFilter(filter); 289 } 290 this.filters.put(name, filter); 291 } 292 } 293 294 public void addToChain(String chainName, String filterName) { 295 addToChain(chainName, filterName, null); 296 } 297 298 public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) { 299 if (!StringUtils.hasText(chainName)) { 300 throw new IllegalArgumentException("chainName cannot be null or empty."); 301 } 302 Filter filter = getFilter(filterName); 303 if (filter == null) { 304 throw new IllegalArgumentException("There is no filter with name '" + filterName 305 + "' to apply to chain [" + chainName + "] in the pool of available Filters. Ensure a " 306 + "filter with that name/path has first been registered with the addFilter method(s)."); 307 } 308 309 applyChainConfig(chainName, filter, chainSpecificFilterConfig); 310 311 NamedFilterList chain = ensureChain(chainName); 312 chain.add(filter); 313 } 314 315 public void setGlobalFilters(List<String> globalFilterNames) throws ConfigurationException { 316 // validate each filter name 317 if (!CollectionUtils.isEmpty(globalFilterNames)) { 318 for (String filterName : globalFilterNames) { 319 Filter filter = filters.get(filterName); 320 if (filter == null) { 321 throw new ConfigurationException("There is no filter with name '" + filterName 322 + "' to apply to the global filters in the pool of available Filters. Ensure a " 323 + "filter with that name/path has first been registered with the addFilter method(s)."); 324 } 325 this.globalFilterNames.add(filterName); 326 } 327 } 328 } 329 330 protected void applyChainConfig(String chainName, Filter filter, String chainSpecificFilterConfig) { 331 if (LOGGER.isDebugEnabled()) { 332 LOGGER.debug("Attempting to apply path [" + chainName + "] to filter [" + filter + "] " 333 + "with config [" + chainSpecificFilterConfig + "]"); 334 } 335 if (filter instanceof PathConfigProcessor) { 336 ((PathConfigProcessor) filter).processPathConfig(chainName, chainSpecificFilterConfig); 337 ((PathConfigProcessor) filter).setCaseInsensitive(caseInsensitive); 338 } else { 339 if (StringUtils.hasText(chainSpecificFilterConfig)) { 340 //they specified a filter configuration, but the Filter doesn't implement PathConfigProcessor 341 //this is an erroneous config: 342 String msg = "chainSpecificFilterConfig was specified, but the underlying " 343 + "Filter instance is not an 'instanceof' " 344 + PathConfigProcessor.class.getName() + ". This is required if the filter is to accept " 345 + "chain-specific configuration."; 346 throw new ConfigurationException(msg); 347 } 348 } 349 } 350 351 protected NamedFilterList ensureChain(String chainName) { 352 NamedFilterList chain = getChain(chainName); 353 if (chain == null) { 354 chain = new SimpleNamedFilterList(chainName); 355 this.filterChains.put(chainName, chain); 356 } 357 return chain; 358 } 359 360 public NamedFilterList getChain(String chainName) { 361 return this.filterChains.get(chainName); 362 } 363 364 public boolean hasChains() { 365 return !CollectionUtils.isEmpty(this.filterChains); 366 } 367 368 public Set<String> getChainNames() { 369 return this.filterChains != null ? this.filterChains.keySet() : Set.of(); 370 } 371 372 public FilterChain proxy(FilterChain original, String chainName) { 373 NamedFilterList configured = getChain(chainName); 374 if (configured == null) { 375 String msg = "There is no configured chain under the name/key [" + chainName + "]."; 376 throw new IllegalArgumentException(msg); 377 } 378 return configured.proxy(original); 379 } 380 381 /** 382 * Initializes the filter by calling <code>filter.init( {@link #getFilterConfig() getFilterConfig()} );</code>. 383 * 384 * @param filter the filter to initialize with the {@code FilterConfig}. 385 */ 386 protected void initFilter(Filter filter) { 387 FilterConfig filterConfig = getFilterConfig(); 388 if (filterConfig == null) { 389 throw new IllegalStateException("FilterConfig attribute has not been set. This must occur before filter " 390 + "initialization can occur."); 391 } 392 try { 393 filter.init(filterConfig); 394 } catch (ServletException e) { 395 throw new ConfigurationException(e); 396 } 397 } 398 399 protected void addDefaultFilters(boolean init) { 400 for (DefaultFilter defaultFilter : DefaultFilter.values()) { 401 addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false); 402 } 403 } 404}