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.util.AntPathMatcher;
022import org.apache.shiro.util.PatternMatcher;
023import org.apache.shiro.web.util.WebUtils;
024import org.owasp.encoder.Encode;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028import javax.servlet.FilterChain;
029import javax.servlet.FilterConfig;
030import javax.servlet.ServletRequest;
031import javax.servlet.ServletResponse;
032
033/**
034 * A {@code FilterChainResolver} that resolves {@link FilterChain}s based on url path
035 * matching, as determined by a configurable {@link #setPathMatcher(PatternMatcher) PathMatcher}.
036 * <p/>
037 * This implementation functions by consulting a {@link org.apache.shiro.web.filter.mgt.FilterChainManager}
038 * for all configured filter chains (keyed
039 * by configured path pattern).  If an incoming Request path matches one of the configured path patterns (via
040 * the {@code PathMatcher}, the corresponding configured {@code FilterChain} is returned.
041 *
042 * @since 1.0
043 */
044public class PathMatchingFilterChainResolver implements FilterChainResolver {
045
046    private static final Logger LOGGER = LoggerFactory.getLogger(PathMatchingFilterChainResolver.class);
047
048    private static final String DEFAULT_PATH_SEPARATOR = "/";
049
050    private FilterChainManager filterChainManager;
051
052    private PatternMatcher pathMatcher;
053
054    public PathMatchingFilterChainResolver() {
055        this.pathMatcher = new AntPathMatcher();
056        this.filterChainManager = new DefaultFilterChainManager();
057    }
058
059    public PathMatchingFilterChainResolver(FilterConfig filterConfig) {
060        this.pathMatcher = new AntPathMatcher();
061        this.filterChainManager = new DefaultFilterChainManager(filterConfig);
062    }
063
064    /**
065     * Returns the {@code PatternMatcher} used when determining if an incoming request's path
066     * matches a configured filter chain.  Unless overridden, the
067     * default implementation is an {@link AntPathMatcher AntPathMatcher}.
068     *
069     * @return the {@code PatternMatcher} used when determining if an incoming request's path
070     * matches a configured filter chain.
071     */
072    public PatternMatcher getPathMatcher() {
073        return pathMatcher;
074    }
075
076    /**
077     * Sets the {@code PatternMatcher} used when determining if an incoming request's path
078     * matches a configured filter chain.  Unless overridden, the
079     * default implementation is an {@link AntPathMatcher AntPathMatcher}.
080     *
081     * @param pathMatcher the {@code PatternMatcher} used when determining if an incoming request's path
082     *                    matches a configured filter chain.
083     */
084    public void setPathMatcher(PatternMatcher pathMatcher) {
085        this.pathMatcher = pathMatcher;
086    }
087
088    public FilterChainManager getFilterChainManager() {
089        return filterChainManager;
090    }
091
092    @SuppressWarnings({"UnusedDeclaration"})
093    public void setFilterChainManager(FilterChainManager filterChainManager) {
094        this.filterChainManager = filterChainManager;
095    }
096
097    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
098        FilterChainManager filterChainManager = getFilterChainManager();
099        if (!filterChainManager.hasChains()) {
100            return null;
101        }
102
103        final String requestURI = getPathWithinApplication(request);
104        final String requestURINoTrailingSlash = removeTrailingSlash(requestURI);
105
106        //the 'chain names' in this implementation are actually path patterns defined by the user.  We just use them
107        //as the chain name for the FilterChainManager's requirements
108        for (String pathPattern : filterChainManager.getChainNames()) {
109            // If the path does match, then pass on to the subclass implementation for specific checks:
110            if (pathMatches(pathPattern, requestURI)) {
111                if (LOGGER.isTraceEnabled()) {
112                    LOGGER.trace("Matched path pattern [{}] for requestURI [{}].  "
113                            + "Utilizing corresponding filter chain...", pathPattern, Encode.forHtml(requestURI));
114                }
115                return filterChainManager.proxy(originalChain, pathPattern);
116            } else {
117
118                // in spring web, the requestURI "/resource/menus" ---- "resource/menus/" both can access the resource
119                // but the pathPattern match "/resource/menus" can not match "resource/menus/"
120                // user can use requestURI + "/" to simply bypassed chain filter, to bypassed shiro protect
121
122                pathPattern = removeTrailingSlash(pathPattern);
123
124                if (pathMatches(pathPattern, requestURINoTrailingSlash)) {
125                    if (LOGGER.isTraceEnabled()) {
126                        LOGGER.trace("Matched path pattern [{}] for requestURI [{}].  "
127                                + "Utilizing corresponding filter chain...",
128                                    pathPattern, Encode.forHtml(requestURINoTrailingSlash));
129                    }
130                    return filterChainManager.proxy(originalChain, pathPattern);
131                }
132            }
133        }
134
135        return null;
136    }
137
138    /**
139     * Returns {@code true} if an incoming request path (the {@code path} argument)
140     * matches a configured filter chain path (the {@code pattern} argument), {@code false} otherwise.
141     * <p/>
142     * Simply delegates to
143     * <b><code>{@link #getPathMatcher() getPathMatcher()}.
144     * {@link PatternMatcher#matches(String, String) matches(pattern,path)}</code></b>.
145     * Subclass implementers should think carefully before overriding this method, as typically a custom
146     * {@code PathMatcher} should be configured for custom path matching behavior instead.  Favor OO composition
147     * rather than inheritance to limit your exposure to Shiro implementation details which may change over time.
148     *
149     * @param pattern the pattern to match against
150     * @param path    the value to match with the specified {@code pattern}
151     * @return {@code true} if the request {@code path} matches the specified filter chain url {@code pattern},
152     * {@code false} otherwise.
153     */
154    protected boolean pathMatches(String pattern, String path) {
155        PatternMatcher pathMatcher = getPathMatcher();
156        return pathMatcher.matches(pattern, path);
157    }
158
159    /**
160     * Merely returns
161     * <code>WebUtils.{@link org.apache.shiro.web.util.WebUtils#getPathWithinApplication(javax.servlet.http.HttpServletRequest)
162     * getPathWithinApplication(request)}</code>
163     * and can be overridden by subclasses for custom request-to-application-path resolution behavior.
164     *
165     * @param request the incoming {@code ServletRequest}
166     * @return the request's path within the application.
167     */
168    protected String getPathWithinApplication(ServletRequest request) {
169        return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
170    }
171
172    private static String removeTrailingSlash(String path) {
173        if (path != null && !DEFAULT_PATH_SEPARATOR.equals(path)
174                && path.endsWith(DEFAULT_PATH_SEPARATOR)) {
175            return path.substring(0, path.length() - 1);
176        }
177        return path;
178    }
179}