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     */
019    package org.apache.shiro.web.servlet;
020    
021    import org.apache.shiro.SecurityUtils;
022    import org.apache.shiro.session.Session;
023    import org.apache.shiro.subject.ExecutionException;
024    import org.apache.shiro.subject.Subject;
025    import org.apache.shiro.web.filter.mgt.FilterChainResolver;
026    import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
027    import org.apache.shiro.web.mgt.WebSecurityManager;
028    import org.apache.shiro.web.subject.WebSubject;
029    import org.slf4j.Logger;
030    import org.slf4j.LoggerFactory;
031    
032    import javax.servlet.FilterChain;
033    import javax.servlet.ServletException;
034    import javax.servlet.ServletRequest;
035    import javax.servlet.ServletResponse;
036    import javax.servlet.http.HttpServletRequest;
037    import javax.servlet.http.HttpServletResponse;
038    import java.io.IOException;
039    import java.util.concurrent.Callable;
040    
041    /**
042     * Abstract base class that provides all standard Shiro request filtering behavior and expects
043     * subclasses to implement configuration-specific logic (INI, XML, .properties, etc).
044     * <p/>
045     * Subclasses should perform configuration and construction logic in an overridden
046     * {@link #init()} method implementation.  That implementation should make available any constructed
047     * {@code SecurityManager} and {@code FilterChainResolver} by calling
048     * {@link #setSecurityManager(org.apache.shiro.web.mgt.WebSecurityManager)} and
049     * {@link #setFilterChainResolver(org.apache.shiro.web.filter.mgt.FilterChainResolver)} methods respectively.
050     *
051     * @since 1.0
052     */
053    public abstract class AbstractShiroFilter extends OncePerRequestFilter {
054    
055        private static final Logger log = LoggerFactory.getLogger(AbstractShiroFilter.class);
056    
057        // Reference to the security manager used by this filter
058        private WebSecurityManager securityManager;
059    
060        // Used to determine which chain should handle an incoming request/response
061        private FilterChainResolver filterChainResolver;
062    
063        protected AbstractShiroFilter() {
064        }
065    
066        public WebSecurityManager getSecurityManager() {
067            return securityManager;
068        }
069    
070        public void setSecurityManager(WebSecurityManager sm) {
071            this.securityManager = sm;
072        }
073    
074        public FilterChainResolver getFilterChainResolver() {
075            return filterChainResolver;
076        }
077    
078        public void setFilterChainResolver(FilterChainResolver filterChainResolver) {
079            this.filterChainResolver = filterChainResolver;
080        }
081    
082        protected final void onFilterConfigSet() throws Exception {
083            init();
084            ensureSecurityManager();
085        }
086    
087        public void init() throws Exception {
088        }
089    
090        /**
091         * A fallback mechanism called in {@link #onFilterConfigSet()} to ensure that the
092         * {@link #getSecurityManager() securityManager} property has been set by configuration, and if not,
093         * creates one automatically.
094         */
095        private void ensureSecurityManager() {
096            WebSecurityManager securityManager = getSecurityManager();
097            if (securityManager == null) {
098                log.info("No SecurityManager configured.  Creating default.");
099                securityManager = createDefaultSecurityManager();
100                setSecurityManager(securityManager);
101            }
102        }
103    
104        protected WebSecurityManager createDefaultSecurityManager() {
105            return new DefaultWebSecurityManager();
106        }
107    
108        protected boolean isHttpSessions() {
109            return getSecurityManager().isHttpSessionMode();
110        }
111    
112        /**
113         * Wraps the original HttpServletRequest in a {@link ShiroHttpServletRequest}, which is required for supporting
114         * Servlet Specification behavior backed by a {@link org.apache.shiro.subject.Subject Subject} instance.
115         *
116         * @param orig the original Servlet Container-provided incoming {@code HttpServletRequest} instance.
117         * @return {@link ShiroHttpServletRequest ShiroHttpServletRequest} instance wrapping the original.
118         * @since 1.0
119         */
120        protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
121            return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
122        }
123    
124        /**
125         * Prepares the {@code ServletRequest} instance that will be passed to the {@code FilterChain} for request
126         * processing.
127         * <p/>
128         * If the {@code ServletRequest} is an instance of {@link HttpServletRequest}, the value returned from this method
129         * is obtained by calling {@link #wrapServletRequest(javax.servlet.http.HttpServletRequest)} to allow Shiro-specific
130         * HTTP behavior, otherwise the original {@code ServletRequest} argument is returned.
131         *
132         * @param request  the incoming ServletRequest
133         * @param response the outgoing ServletResponse
134         * @param chain    the Servlet Container provided {@code FilterChain} that will receive the returned request.
135         * @return the {@code ServletRequest} instance that will be passed to the {@code FilterChain} for request processing.
136         * @since 1.0
137         */
138        @SuppressWarnings({"UnusedDeclaration"})
139        protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
140            ServletRequest toUse = request;
141            if (request instanceof HttpServletRequest) {
142                HttpServletRequest http = (HttpServletRequest) request;
143                toUse = wrapServletRequest(http);
144            }
145            return toUse;
146        }
147    
148        /**
149         * Returns a new {@link ShiroHttpServletResponse} instance, wrapping the {@code orig} argument, in order to provide
150         * correct URL rewriting behavior required by the Servlet Specification when using Shiro-based sessions (and not
151         * Servlet Container HTTP-based sessions).
152         *
153         * @param orig    the original {@code HttpServletResponse} instance provided by the Servlet Container.
154         * @param request the {@code ShiroHttpServletRequest} instance wrapping the original request.
155         * @return the wrapped ServletResponse instance to use during {@link FilterChain} execution.
156         * @since 1.0
157         */
158        protected ServletResponse wrapServletResponse(HttpServletResponse orig, ShiroHttpServletRequest request) {
159            return new ShiroHttpServletResponse(orig, getServletContext(), request);
160        }
161    
162        /**
163         * Prepares the {@code ServletResponse} instance that will be passed to the {@code FilterChain} for request
164         * processing.
165         * <p/>
166         * This implementation delegates to {@link #wrapServletRequest(javax.servlet.http.HttpServletRequest)}
167         * only if Shiro-based sessions are enabled (that is, !{@link #isHttpSessions()}) and the request instance is a
168         * {@link ShiroHttpServletRequest}.  This ensures that any URL rewriting that occurs is handled correctly using the
169         * Shiro-managed Session's sessionId and not a servlet container session ID.
170         * <p/>
171         * If HTTP-based sessions are enabled (the default), then this method does nothing and just returns the
172         * {@code ServletResponse} argument as-is, relying on the default Servlet Container URL rewriting logic.
173         *
174         * @param request  the incoming ServletRequest
175         * @param response the outgoing ServletResponse
176         * @param chain    the Servlet Container provided {@code FilterChain} that will receive the returned request.
177         * @return the {@code ServletResponse} instance that will be passed to the {@code FilterChain} during request processing.
178         * @since 1.0
179         */
180        @SuppressWarnings({"UnusedDeclaration"})
181        protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) {
182            ServletResponse toUse = response;
183            if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) &&
184                    (response instanceof HttpServletResponse)) {
185                //the ShiroHttpServletResponse exists to support URL rewriting for session ids.  This is only needed if
186                //using Shiro sessions (i.e. not simple HttpSession based sessions):
187                toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request);
188            }
189            return toUse;
190        }
191    
192        /**
193         * Creates a {@link WebSubject} instance to associate with the incoming request/response pair which will be used
194         * throughout the request/response execution.
195         *
196         * @param request  the incoming {@code ServletRequest}
197         * @param response the outgoing {@code ServletResponse}
198         * @return the {@code WebSubject} instance to associate with the request/response execution
199         * @since 1.0
200         */
201        protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
202            return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
203        }
204    
205        /**
206         * Updates any 'native'  Session's last access time that might exist to the timestamp when this method is called.
207         * If native sessions are not enabled (that is, standard Servlet container sessions are being used) or there is no
208         * session ({@code subject.getSession(false) == null}), this method does nothing.
209         * <p/>This method implementation merely calls
210         * <code>Session.{@link org.apache.shiro.session.Session#touch() touch}()</code> on the session.
211         *
212         * @param request  incoming request - ignored, but available to subclasses that might wish to override this method
213         * @param response outgoing response - ignored, but available to subclasses that might wish to override this method
214         * @since 1.0
215         */
216        @SuppressWarnings({"UnusedDeclaration"})
217        protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) {
218            if (!isHttpSessions()) { //'native' sessions
219                Subject subject = SecurityUtils.getSubject();
220                //Subject should never _ever_ be null, but just in case:
221                if (subject != null) {
222                    Session session = subject.getSession(false);
223                    if (session != null) {
224                        try {
225                            session.touch();
226                        } catch (Throwable t) {
227                            log.error("session.touch() method invocation has failed.  Unable to update" +
228                                    "the corresponding session's last access time based on the incoming request.", t);
229                        }
230                    }
231                }
232            }
233        }
234    
235        /**
236         * {@code doFilterInternal} implementation that sets-up, executes, and cleans-up a Shiro-filtered request.  It
237         * performs the following ordered operations:
238         * <ol>
239         * <li>{@link #prepareServletRequest(ServletRequest, ServletResponse, FilterChain) Prepares}
240         * the incoming {@code ServletRequest} for use during Shiro's processing</li>
241         * <li>{@link #prepareServletResponse(ServletRequest, ServletResponse, FilterChain) Prepares}
242         * the outgoing {@code ServletResponse} for use during Shiro's processing</li>
243         * <li> {@link #createSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) Creates} a
244         * {@link Subject} instance based on the specified request/response pair.</li>
245         * <li>Finally {@link Subject#execute(Runnable) executes} the
246         * {@link #updateSessionLastAccessTime(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} and
247         * {@link #executeChain(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)}
248         * methods</li>
249         * </ol>
250         * <p/>
251         * The {@code Subject.}{@link Subject#execute(Runnable) execute(Runnable)} call in step #4 is used as an
252         * implementation technique to guarantee proper thread binding and restoration is completed successfully.
253         *
254         * @param servletRequest  the incoming {@code ServletRequest}
255         * @param servletResponse the outgoing {@code ServletResponse}
256         * @param chain           the container-provided {@code FilterChain} to execute
257         * @throws IOException                    if an IO error occurs
258         * @throws javax.servlet.ServletException if an Throwable other than an IOException
259         */
260        protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
261                throws ServletException, IOException {
262    
263            Throwable t = null;
264    
265            try {
266                final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
267                final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
268    
269                final Subject subject = createSubject(request, response);
270    
271                //noinspection unchecked
272                subject.execute(new Callable() {
273                    public Object call() throws Exception {
274                        updateSessionLastAccessTime(request, response);
275                        executeChain(request, response, chain);
276                        return null;
277                    }
278                });
279            } catch (ExecutionException ex) {
280                t = ex.getCause();
281            } catch (Throwable throwable) {
282                t = throwable;
283            }
284    
285            if (t != null) {
286                if (t instanceof ServletException) {
287                    throw (ServletException) t;
288                }
289                if (t instanceof IOException) {
290                    throw (IOException) t;
291                }
292                //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
293                String msg = "Filtered request failed.";
294                throw new ServletException(msg, t);
295            }
296        }
297    
298        /**
299         * Returns the {@code FilterChain} to execute for the given request.
300         * <p/>
301         * The {@code origChain} argument is the
302         * original {@code FilterChain} supplied by the Servlet Container, but it may be modified to provide
303         * more behavior by pre-pending further chains according to the Shiro configuration.
304         * <p/>
305         * This implementation returns the chain that will actually be executed by acquiring the chain from a
306         * {@link #getFilterChainResolver() filterChainResolver}.  The resolver determines exactly which chain to
307         * execute, typically based on URL configuration.  If no chain is returned from the resolver call
308         * (returns {@code null}), then the {@code origChain} will be returned by default.
309         *
310         * @param request   the incoming ServletRequest
311         * @param response  the outgoing ServletResponse
312         * @param origChain the original {@code FilterChain} provided by the Servlet Container
313         * @return the {@link FilterChain} to execute for the given request
314         * @since 1.0
315         */
316        protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
317            FilterChain chain = origChain;
318    
319            FilterChainResolver resolver = getFilterChainResolver();
320            if (resolver == null) {
321                log.debug("No FilterChainResolver configured.  Returning original FilterChain.");
322                return origChain;
323            }
324    
325            FilterChain resolved = resolver.getChain(request, response, origChain);
326            if (resolved != null) {
327                log.trace("Resolved a configured FilterChain for the current request.");
328                chain = resolved;
329            } else {
330                log.trace("No FilterChain configured for the current request.  Using the default.");
331            }
332    
333            return chain;
334        }
335    
336        /**
337         * Executes a {@link FilterChain} for the given request.
338         * <p/>
339         * This implementation first delegates to
340         * <code>{@link #getExecutionChain(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) getExecutionChain}</code>
341         * to allow the application's Shiro configuration to determine exactly how the chain should execute.  The resulting
342         * value from that call is then executed directly by calling the returned {@code FilterChain}'s
343         * {@link FilterChain#doFilter doFilter} method.  That is:
344         * <pre>
345         * FilterChain chain = {@link #getExecutionChain}(request, response, origChain);
346         * chain.{@link FilterChain#doFilter doFilter}(request,response);</pre>
347         *
348         * @param request   the incoming ServletRequest
349         * @param response  the outgoing ServletResponse
350         * @param origChain the Servlet Container-provided chain that may be wrapped further by an application-configured
351         *                  chain of Filters.
352         * @throws IOException      if the underlying {@code chain.doFilter} call results in an IOException
353         * @throws ServletException if the underlying {@code chain.doFilter} call results in a ServletException
354         * @since 1.0
355         */
356        protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
357                throws IOException, ServletException {
358            FilterChain chain = getExecutionChain(request, response, origChain);
359            chain.doFilter(request, response);
360        }
361    }