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.util.Nameable; 024import org.apache.shiro.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.Collections; 034import java.util.LinkedHashMap; 035import java.util.Map; 036import java.util.Set; 037 038/** 039 * Default {@link FilterChainManager} implementation maintaining a map of {@link Filter Filter} instances 040 * (key: filter name, value: Filter) as well as a map of {@link NamedFilterList NamedFilterList}s created from these 041 * {@code Filter}s (key: filter chain name, value: NamedFilterList). The {@code NamedFilterList} is essentially a 042 * {@link FilterChain} that also has a name property by which it can be looked up. 043 * 044 * @see NamedFilterList 045 * @since 1.0 046 */ 047public class DefaultFilterChainManager implements FilterChainManager { 048 049 private static transient final Logger log = LoggerFactory.getLogger(DefaultFilterChainManager.class); 050 051 private FilterConfig filterConfig; 052 053 private Map<String, Filter> filters; //pool of filters available for creating chains 054 055 private Map<String, NamedFilterList> filterChains; //key: chain name, value: chain 056 057 public DefaultFilterChainManager() { 058 this.filters = new LinkedHashMap<String, Filter>(); 059 this.filterChains = new LinkedHashMap<String, NamedFilterList>(); 060 addDefaultFilters(false); 061 } 062 063 public DefaultFilterChainManager(FilterConfig filterConfig) { 064 this.filters = new LinkedHashMap<String, Filter>(); 065 this.filterChains = new LinkedHashMap<String, NamedFilterList>(); 066 setFilterConfig(filterConfig); 067 addDefaultFilters(true); 068 } 069 070 /** 071 * Returns the {@code FilterConfig} provided by the Servlet container at webapp startup. 072 * 073 * @return the {@code FilterConfig} provided by the Servlet container at webapp startup. 074 */ 075 public FilterConfig getFilterConfig() { 076 return filterConfig; 077 } 078 079 /** 080 * Sets the {@code FilterConfig} provided by the Servlet container at webapp startup. 081 * 082 * @param filterConfig the {@code FilterConfig} provided by the Servlet container at webapp startup. 083 */ 084 public void setFilterConfig(FilterConfig filterConfig) { 085 this.filterConfig = filterConfig; 086 } 087 088 public Map<String, Filter> getFilters() { 089 return filters; 090 } 091 092 @SuppressWarnings({"UnusedDeclaration"}) 093 public void setFilters(Map<String, Filter> filters) { 094 this.filters = filters; 095 } 096 097 public Map<String, NamedFilterList> getFilterChains() { 098 return filterChains; 099 } 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}