blob: f289a79b872e0bacea10b7c16e0f647ebd1b1e93 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.shiro.web.filter.mgt;
import org.apache.shiro.config.ConfigurationException;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.util.Nameable;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.PathConfigProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Default {@link FilterChainManager} implementation maintaining a map of {@link Filter Filter} instances
* (key: filter name, value: Filter) as well as a map of {@link NamedFilterList NamedFilterList}s created from these
* {@code Filter}s (key: filter chain name, value: NamedFilterList). The {@code NamedFilterList} is essentially a
* {@link FilterChain} that also has a name property by which it can be looked up.
*
* @see NamedFilterList
* @since 1.0
*/
public class DefaultFilterChainManager implements FilterChainManager {
private static transient final Logger log = LoggerFactory.getLogger(DefaultFilterChainManager.class);
private FilterConfig filterConfig;
private Map<String, Filter> filters; //pool of filters available for creating chains
private List<String> globalFilterNames; // list of filters to prepend to every chain
private Map<String, NamedFilterList> filterChains; //key: chain name, value: chain
public DefaultFilterChainManager() {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChains = new LinkedHashMap<String, NamedFilterList>();
this.globalFilterNames = new ArrayList<>();
addDefaultFilters(false);
}
public DefaultFilterChainManager(FilterConfig filterConfig) {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChains = new LinkedHashMap<String, NamedFilterList>();
this.globalFilterNames = new ArrayList<>();
setFilterConfig(filterConfig);
addDefaultFilters(true);
}
/**
* Returns the {@code FilterConfig} provided by the Servlet container at webapp startup.
*
* @return the {@code FilterConfig} provided by the Servlet container at webapp startup.
*/
public FilterConfig getFilterConfig() {
return filterConfig;
}
/**
* Sets the {@code FilterConfig} provided by the Servlet container at webapp startup.
*
* @param filterConfig the {@code FilterConfig} provided by the Servlet container at webapp startup.
*/
public void setFilterConfig(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
}
public Map<String, Filter> getFilters() {
return filters;
}
@SuppressWarnings({"UnusedDeclaration"})
public void setFilters(Map<String, Filter> filters) {
this.filters = filters;
}
public Map<String, NamedFilterList> getFilterChains() {
return filterChains;
}
@SuppressWarnings({"UnusedDeclaration"})
public void setFilterChains(Map<String, NamedFilterList> filterChains) {
this.filterChains = filterChains;
}
public Filter getFilter(String name) {
return this.filters.get(name);
}
public void addFilter(String name, Filter filter) {
addFilter(name, filter, false);
}
public void addFilter(String name, Filter filter, boolean init) {
addFilter(name, filter, init, true);
}
public void createDefaultChain(String chainName) {
// only create the defaultChain if we don't have a chain with this name already
// (the global filters will already be in that chain)
if (!getChainNames().contains(chainName) && !CollectionUtils.isEmpty(globalFilterNames)) {
// add each of global filters
globalFilterNames.stream().forEach(filterName -> addToChain(chainName, filterName));
}
}
public void createChain(String chainName, String chainDefinition) {
if (!StringUtils.hasText(chainName)) {
throw new NullPointerException("chainName cannot be null or empty.");
}
if (!StringUtils.hasText(chainDefinition)) {
throw new NullPointerException("chainDefinition cannot be null or empty.");
}
if (log.isDebugEnabled()) {
log.debug("Creating chain [" + chainName + "] with global filters " + globalFilterNames + " and from String definition [" + chainDefinition + "]");
}
// first add each of global filters
if (!CollectionUtils.isEmpty(globalFilterNames)) {
globalFilterNames.stream().forEach(filterName -> addToChain(chainName, filterName));
}
//parse the value by tokenizing it to get the resulting filter-specific config entries
//
//e.g. for a value of
//
// "authc, roles[admin,user], perms[file:edit]"
//
// the resulting token array would equal
//
// { "authc", "roles[admin,user]", "perms[file:edit]" }
//
String[] filterTokens = splitChainDefinition(chainDefinition);
//each token is specific to each filter.
//strip the name and extract any filter-specific config between brackets [ ]
for (String token : filterTokens) {
String[] nameConfigPair = toNameConfigPair(token);
//now we have the filter name, path and (possibly null) path-specific config. Let's apply them:
addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
}
}
/**
* Splits the comma-delimited filter chain definition line into individual filter definition tokens.
* <p/>
* Example Input:
* <pre>
* foo, bar[baz], blah[x, y]
* </pre>
* Resulting Output:
* <pre>
* output[0] == foo
* output[1] == bar[baz]
* output[2] == blah[x, y]
* </pre>
* @param chainDefinition the comma-delimited filter chain definition.
* @return an array of filter definition tokens
* @since 1.2
* @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
*/
protected String[] splitChainDefinition(String chainDefinition) {
return StringUtils.split(chainDefinition, StringUtils.DEFAULT_DELIMITER_CHAR, '[', ']', true, true);
}
/**
* Based on the given filter chain definition token (e.g. 'foo' or 'foo[bar, baz]'), this will return the token
* as a name/value pair, removing any brackets as necessary. Examples:
* <table>
* <tr>
* <th>Input</th>
* <th>Result</th>
* </tr>
* <tr>
* <td>{@code foo}</td>
* <td>returned[0] == {@code foo}<br/>returned[1] == {@code null}</td>
* </tr>
* <tr>
* <td>{@code foo[bar, baz]}</td>
* <td>returned[0] == {@code foo}<br/>returned[1] == {@code bar, baz}</td>
* </tr>
* </table>
* @param token the filter chain definition token
* @return A name/value pair representing the filter name and a (possibly null) config value.
* @throws ConfigurationException if the token cannot be parsed
* @since 1.2
* @see <a href="https://issues.apache.org/jira/browse/SHIRO-205">SHIRO-205</a>
*/
protected String[] toNameConfigPair(String token) throws ConfigurationException {
try {
String[] pair = token.split("\\[", 2);
String name = StringUtils.clean(pair[0]);
if (name == null) {
throw new IllegalArgumentException("Filter name not found for filter chain definition token: " + token);
}
String config = null;
if (pair.length == 2) {
config = StringUtils.clean(pair[1]);
//if there was an open bracket, it assumed there is a closing bracket, so strip it too:
config = config.substring(0, config.length() - 1);
config = StringUtils.clean(config);
//backwards compatibility prior to implementing SHIRO-205:
//prior to SHIRO-205 being implemented, it was common for end-users to quote the config inside brackets
//if that config required commas. We need to strip those quotes to get to the interior quoted definition
//to ensure any existing quoted definitions still function for end users:
if (config != null && config.startsWith("\"") && config.endsWith("\"")) {
String stripped = config.substring(1, config.length() - 1);
stripped = StringUtils.clean(stripped);
//if the stripped value does not have any internal quotes, we can assume that the entire config was
//quoted and we can use the stripped value.
if (stripped != null && stripped.indexOf('"') == -1) {
config = stripped;
}
//else:
//the remaining config does have internal quotes, so we need to assume that each comma delimited
//pair might be quoted, in which case we need the leading and trailing quotes that we stripped
//So we ignore the stripped value.
}
}
return new String[]{name, config};
} catch (Exception e) {
String msg = "Unable to parse filter chain definition token: " + token;
throw new ConfigurationException(msg, e);
}
}
protected void addFilter(String name, Filter filter, boolean init, boolean overwrite) {
Filter existing = getFilter(name);
if (existing == null || overwrite) {
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
if (init) {
initFilter(filter);
}
this.filters.put(name, filter);
}
}
public void addToChain(String chainName, String filterName) {
addToChain(chainName, filterName, null);
}
public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
if (!StringUtils.hasText(chainName)) {
throw new IllegalArgumentException("chainName cannot be null or empty.");
}
Filter filter = getFilter(filterName);
if (filter == null) {
throw new IllegalArgumentException("There is no filter with name '" + filterName +
"' to apply to chain [" + chainName + "] in the pool of available Filters. Ensure a " +
"filter with that name/path has first been registered with the addFilter method(s).");
}
applyChainConfig(chainName, filter, chainSpecificFilterConfig);
NamedFilterList chain = ensureChain(chainName);
chain.add(filter);
}
public void setGlobalFilters(List<String> globalFilterNames) throws ConfigurationException {
// validate each filter name
if (!CollectionUtils.isEmpty(globalFilterNames)) {
for (String filterName : globalFilterNames) {
Filter filter = filters.get(filterName);
if (filter == null) {
throw new ConfigurationException("There is no filter with name '" + filterName +
"' to apply to the global filters in the pool of available Filters. Ensure a " +
"filter with that name/path has first been registered with the addFilter method(s).");
}
this.globalFilterNames.add(filterName);
}
}
}
protected void applyChainConfig(String chainName, Filter filter, String chainSpecificFilterConfig) {
if (log.isDebugEnabled()) {
log.debug("Attempting to apply path [" + chainName + "] to filter [" + filter + "] " +
"with config [" + chainSpecificFilterConfig + "]");
}
if (filter instanceof PathConfigProcessor) {
((PathConfigProcessor) filter).processPathConfig(chainName, chainSpecificFilterConfig);
} else {
if (StringUtils.hasText(chainSpecificFilterConfig)) {
//they specified a filter configuration, but the Filter doesn't implement PathConfigProcessor
//this is an erroneous config:
String msg = "chainSpecificFilterConfig was specified, but the underlying " +
"Filter instance is not an 'instanceof' " +
PathConfigProcessor.class.getName() + ". This is required if the filter is to accept " +
"chain-specific configuration.";
throw new ConfigurationException(msg);
}
}
}
protected NamedFilterList ensureChain(String chainName) {
NamedFilterList chain = getChain(chainName);
if (chain == null) {
chain = new SimpleNamedFilterList(chainName);
this.filterChains.put(chainName, chain);
}
return chain;
}
public NamedFilterList getChain(String chainName) {
return this.filterChains.get(chainName);
}
public boolean hasChains() {
return !CollectionUtils.isEmpty(this.filterChains);
}
public Set<String> getChainNames() {
//noinspection unchecked
return this.filterChains != null ? this.filterChains.keySet() : Collections.EMPTY_SET;
}
public FilterChain proxy(FilterChain original, String chainName) {
NamedFilterList configured = getChain(chainName);
if (configured == null) {
String msg = "There is no configured chain under the name/key [" + chainName + "].";
throw new IllegalArgumentException(msg);
}
return configured.proxy(original);
}
/**
* Initializes the filter by calling <code>filter.init( {@link #getFilterConfig() getFilterConfig()} );</code>.
*
* @param filter the filter to initialize with the {@code FilterConfig}.
*/
protected void initFilter(Filter filter) {
FilterConfig filterConfig = getFilterConfig();
if (filterConfig == null) {
throw new IllegalStateException("FilterConfig attribute has not been set. This must occur before filter " +
"initialization can occur.");
}
try {
filter.init(filterConfig);
} catch (ServletException e) {
throw new ConfigurationException(e);
}
}
protected void addDefaultFilters(boolean init) {
for (DefaultFilter defaultFilter : DefaultFilter.values()) {
addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
}
}
}