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    public PathMatchingFilterChainResolver caseInsensitive(boolean caseInsensitive) {
065        setCaseInsensitive(caseInsensitive);
066        return this;
067    }
068
069    /**
070     * Returns the {@code PatternMatcher} used when determining if an incoming request's path
071     * matches a configured filter chain.  Unless overridden, the
072     * default implementation is an {@link AntPathMatcher AntPathMatcher}.
073     *
074     * @return the {@code PatternMatcher} used when determining if an incoming request's path
075     * matches a configured filter chain.
076     */
077    public PatternMatcher getPathMatcher() {
078        return pathMatcher;
079    }
080
081    /**
082     * Sets the {@code PatternMatcher} used when determining if an incoming request's path
083     * matches a configured filter chain.  Unless overridden, the
084     * default implementation is an {@link AntPathMatcher AntPathMatcher}.
085     *
086     * @param pathMatcher the {@code PatternMatcher} used when determining if an incoming request's path
087     *                    matches a configured filter chain.
088     */
089    public void setPathMatcher(PatternMatcher pathMatcher) {
090        this.pathMatcher = pathMatcher;
091    }
092
093    public boolean isCaseInsensitive() {
094        return pathMatcher != null && pathMatcher.isCaseInsensitive();
095    }
096
097    public void setCaseInsensitive(boolean caseInsensitive) {
098        if (pathMatcher != null) {
099            pathMatcher.setCaseInsensitive(caseInsensitive);
100        }
101        if (filterChainManager != null) {
102            filterChainManager.setCaseInsensitive(caseInsensitive);
103        }
104    }
105
106    public FilterChainManager getFilterChainManager() {
107        return filterChainManager;
108    }
109
110    @SuppressWarnings({"UnusedDeclaration"})
111    public void setFilterChainManager(FilterChainManager filterChainManager) {
112        this.filterChainManager = filterChainManager;
113    }
114
115    public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
116        FilterChainManager filterChainManager = getFilterChainManager();
117        if (!filterChainManager.hasChains()) {
118            return null;
119        }
120
121        final String requestURI = getPathWithinApplication(request);
122        final String requestURINoTrailingSlash = removeTrailingSlash(requestURI);
123
124        //the 'chain names' in this implementation are actually path patterns defined by the user.  We just use them
125        //as the chain name for the FilterChainManager's requirements
126        for (String pathPattern : filterChainManager.getChainNames()) {
127            // If the path does match, then pass on to the subclass implementation for specific checks:
128            if (pathMatches(pathPattern, requestURI)) {
129                if (LOGGER.isTraceEnabled()) {
130                    LOGGER.trace("Matched path pattern [{}] for requestURI [{}].  "
131                            + "Utilizing corresponding filter chain...", pathPattern, Encode.forHtml(requestURI));
132                }
133                return filterChainManager.proxy(originalChain, pathPattern);
134            } else {
135
136                // in spring web, the requestURI "/resource/menus" ---- "resource/menus/" both can access the resource
137                // but the pathPattern match "/resource/menus" can not match "resource/menus/"
138                // user can use requestURI + "/" to simply bypassed chain filter, to bypassed shiro protect
139
140                pathPattern = removeTrailingSlash(pathPattern);
141
142                if (pathMatches(pathPattern, requestURINoTrailingSlash)) {
143                    if (LOGGER.isTraceEnabled()) {
144                        LOGGER.trace("Matched path pattern [{}] for requestURI [{}].  "
145                                + "Utilizing corresponding filter chain...",
146                                    pathPattern, Encode.forHtml(requestURINoTrailingSlash));
147                    }
148                    return filterChainManager.proxy(originalChain, pathPattern);
149                }
150            }
151        }
152
153        return null;
154    }
155
156    /**
157     * Returns {@code true} if an incoming request path (the {@code path} argument)
158     * matches a configured filter chain path (the {@code pattern} argument), {@code false} otherwise.
159     * <p/>
160     * Simply delegates to
161     * <b><code>{@link #getPathMatcher() getPathMatcher()}.
162     * {@link PatternMatcher#matches(String, String) matches(pattern,path)}</code></b>.
163     * Subclass implementers should think carefully before overriding this method, as typically a custom
164     * {@code PathMatcher} should be configured for custom path matching behavior instead.  Favor OO composition
165     * rather than inheritance to limit your exposure to Shiro implementation details which may change over time.
166     *
167     * @param pattern the pattern to match against
168     * @param path    the value to match with the specified {@code pattern}
169     * @return {@code true} if the request {@code path} matches the specified filter chain url {@code pattern},
170     * {@code false} otherwise.
171     */
172    protected boolean pathMatches(String pattern, String path) {
173        PatternMatcher pathMatcher = getPathMatcher();
174        return pathMatcher.matches(pattern, path);
175    }
176
177    /**
178     * Merely returns
179     * <code>WebUtils.{@link org.apache.shiro.web.util.WebUtils#getPathWithinApplication(javax.servlet.http.HttpServletRequest)
180     * getPathWithinApplication(request)}</code>
181     * and can be overridden by subclasses for custom request-to-application-path resolution behavior.
182     *
183     * @param request the incoming {@code ServletRequest}
184     * @return the request's path within the application.
185     */
186    protected String getPathWithinApplication(ServletRequest request) {
187        return WebUtils.getPathWithinApplication(WebUtils.toHttp(request));
188    }
189
190    private static String removeTrailingSlash(String path) {
191        if (path != null && !DEFAULT_PATH_SEPARATOR.equals(path)
192                && path.endsWith(DEFAULT_PATH_SEPARATOR)) {
193            return path.substring(0, path.length() - 1);
194        }
195        return path;
196    }
197}