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