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.servlet;
020
021import org.apache.shiro.SecurityUtils;
022import org.apache.shiro.session.Session;
023import org.apache.shiro.subject.ExecutionException;
024import org.apache.shiro.subject.Subject;
025import org.apache.shiro.web.config.ShiroFilterConfiguration;
026import org.apache.shiro.web.filter.mgt.FilterChainResolver;
027import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
028import org.apache.shiro.web.mgt.WebSecurityManager;
029import org.apache.shiro.web.subject.WebSubject;
030import org.slf4j.Logger;
031import org.slf4j.LoggerFactory;
032
033import javax.servlet.FilterChain;
034import javax.servlet.ServletException;
035import javax.servlet.ServletRequest;
036import javax.servlet.ServletResponse;
037import javax.servlet.http.HttpServletRequest;
038import javax.servlet.http.HttpServletResponse;
039import java.io.IOException;
040import java.util.concurrent.Callable;
041
042/**
043 * Abstract base class that provides all standard Shiro request filtering behavior and expects
044 * subclasses to implement configuration-specific logic (INI, XML, .properties, etc.).
045 * <p/>
046 * Subclasses should perform configuration and construction logic in an overridden
047 * {@link #init()} method implementation.  That implementation should make available any constructed
048 * {@code SecurityManager} and {@code FilterChainResolver} by calling
049 * {@link #setSecurityManager(org.apache.shiro.web.mgt.WebSecurityManager)} and
050 * {@link #setFilterChainResolver(org.apache.shiro.web.filter.mgt.FilterChainResolver)} methods respectively.
051 * <h3>Static SecurityManager</h3>
052 * By default, the {@code SecurityManager} instance enabled by this filter <em>will not</em> be enabled in static
053 * memory via the {@code SecurityUtils.}
054 * {@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}
055 * method.  Instead, it is expected that Subject instances will always be constructed on a request-processing thread
056 * via instances of this Filter class.
057 * <p/>
058 * However, if you need to construct {@code Subject} instances on separate (non request-processing) threads, it might
059 * be easiest to enable the SecurityManager to be available in static memory via the
060 * {@link SecurityUtils#getSecurityManager()} method.  You can do this by additionally specifying an {@code init-param}:
061 * <pre>
062 * &lt;filter&gt;
063 *     ... other config here ...
064 *     &lt;init-param&gt;
065 *         &lt;param-name&gt;staticSecurityManagerEnabled&lt;/param-name&gt;
066 *         &lt;param-value&gt;true&lt;/param-value&gt;
067 *     &lt;/init-param&gt;
068 * &lt;/filter&gt;
069 * </pre>
070 * See the Shiro <a href="http://shiro.apache.org/subject.html">Subject documentation</a> for more information as to
071 * if you would do this, particularly the sections on the {@code Subject.Builder} and Thread Association.
072 *
073 * @see <a href="http://shiro.apache.org/subject.html">Subject documentation</a>
074 * @since 1.0
075 */
076public abstract class AbstractShiroFilter extends OncePerRequestFilter {
077
078    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractShiroFilter.class);
079
080    private static final String STATIC_INIT_PARAM_NAME = "staticSecurityManagerEnabled";
081
082    // Reference to the security manager used by this filter
083    private WebSecurityManager securityManager;
084
085    // Used to determine which chain should handle an incoming request/response
086    private FilterChainResolver filterChainResolver;
087
088    /**
089     * Whether to bind the constructed SecurityManager instance to static memory (via
090     * SecurityUtils.setSecurityManager).
091     * This was added to support <a href="https://issues.apache.org/jira/browse/SHIRO-287"/>
092     *
093     * @since 1.2
094     */
095    private boolean staticSecurityManagerEnabled;
096
097    protected AbstractShiroFilter() {
098        this.staticSecurityManagerEnabled = false;
099    }
100
101    public WebSecurityManager getSecurityManager() {
102        return securityManager;
103    }
104
105    public void setSecurityManager(WebSecurityManager sm) {
106        this.securityManager = sm;
107    }
108
109    public FilterChainResolver getFilterChainResolver() {
110        return filterChainResolver;
111    }
112
113    public void setFilterChainResolver(FilterChainResolver filterChainResolver) {
114        this.filterChainResolver = filterChainResolver;
115    }
116
117    public void setShiroFilterConfiguration(ShiroFilterConfiguration config) {
118        this.setFilterOncePerRequest(config.isFilterOncePerRequest());
119
120        // this property could have already been set with a servlet config param
121        this.setStaticSecurityManagerEnabled(config.isStaticSecurityManagerEnabled() || isStaticSecurityManagerEnabled());
122    }
123
124    /**
125     * Returns {@code true} if the constructed {@link #getSecurityManager() securityManager} reference should be bound
126     * to static memory (via
127     * {@code SecurityUtils.}{@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}),
128     * {@code false} otherwise.
129     * <p/>
130     * The default value is {@code false}.
131     * <p/>
132     *
133     * @return {@code true} if the constructed {@link #getSecurityManager() securityManager} reference should be bound
134     * to static memory (via {@code SecurityUtils.}{@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager)
135     * setSecurityManager}),
136     * {@code false} otherwise.
137     * @see <a href="https://issues.apache.org/jira/browse/SHIRO-287">SHIRO-287</a>
138     * @since 1.2
139     */
140    public boolean isStaticSecurityManagerEnabled() {
141        return staticSecurityManagerEnabled;
142    }
143
144    /**
145     * Sets if the constructed {@link #getSecurityManager() securityManager} reference should be bound
146     * to static memory (via {@code SecurityUtils.}
147     * {@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}).
148     * <p/>
149     * The default value is {@code false}.
150     *
151     * @param staticSecurityManagerEnabled if the constructed {@link #getSecurityManager() securityManager} reference
152     *                                     should be bound to static memory (via
153     *                                     {@code SecurityUtils.}
154     *                                     {@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager)
155     *                                          setSecurityManager}).
156     * @see <a href="https://issues.apache.org/jira/browse/SHIRO-287">SHIRO-287</a>
157     * @since 1.2
158     */
159    public void setStaticSecurityManagerEnabled(boolean staticSecurityManagerEnabled) {
160        this.staticSecurityManagerEnabled = staticSecurityManagerEnabled;
161    }
162
163    protected final void onFilterConfigSet() throws Exception {
164        //added in 1.2 for SHIRO-287:
165        applyStaticSecurityManagerEnabledConfig();
166        init();
167        ensureSecurityManager();
168        //added in 1.2 for SHIRO-287:
169        if (isStaticSecurityManagerEnabled()) {
170            SecurityUtils.setSecurityManager(getSecurityManager());
171        }
172    }
173
174    /**
175     * Checks if the init-param that configures the filter to use static memory has been configured, and if so,
176     * sets the {@link #setStaticSecurityManagerEnabled(boolean)} attribute with the configured value.
177     *
178     * @see <a href="https://issues.apache.org/jira/browse/SHIRO-287">SHIRO-287</a>
179     * @since 1.2
180     */
181    private void applyStaticSecurityManagerEnabledConfig() {
182        String value = getInitParam(STATIC_INIT_PARAM_NAME);
183        if (value != null) {
184            boolean b = Boolean.parseBoolean(value);
185            if (b) {
186                setStaticSecurityManagerEnabled(b);
187            }
188        }
189    }
190
191    public void init() throws Exception {
192    }
193
194    /**
195     * A fallback mechanism called in {@link #onFilterConfigSet()} to ensure that the
196     * {@link #getSecurityManager() securityManager} property has been set by configuration, and if not,
197     * creates one automatically.
198     */
199    private void ensureSecurityManager() {
200        WebSecurityManager securityManager = getSecurityManager();
201        if (securityManager == null) {
202            LOGGER.info("No SecurityManager configured.  Creating default.");
203            securityManager = createDefaultSecurityManager();
204            setSecurityManager(securityManager);
205        }
206    }
207
208    protected WebSecurityManager createDefaultSecurityManager() {
209        return new DefaultWebSecurityManager();
210    }
211
212    protected boolean isHttpSessions() {
213        return getSecurityManager().isHttpSessionMode();
214    }
215
216    /**
217     * Wraps the original HttpServletRequest in a {@link ShiroHttpServletRequest}, which is required for supporting
218     * Servlet Specification behavior backed by a {@link org.apache.shiro.subject.Subject Subject} instance.
219     *
220     * @param orig the original Servlet Container-provided incoming {@code HttpServletRequest} instance.
221     * @return {@link ShiroHttpServletRequest ShiroHttpServletRequest} instance wrapping the original.
222     * @since 1.0
223     */
224    protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
225        return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
226    }
227
228    /**
229     * Prepares the {@code ServletRequest} instance that will be passed to the {@code FilterChain} for request
230     * processing.
231     * <p/>
232     * If the {@code ServletRequest} is an instance of {@link HttpServletRequest}, the value returned from this method
233     * is obtained by calling {@link #wrapServletRequest(javax.servlet.http.HttpServletRequest)} to allow Shiro-specific
234     * HTTP behavior, otherwise the original {@code ServletRequest} argument is returned.
235     *
236     * @param request  the incoming ServletRequest
237     * @param response the outgoing ServletResponse
238     * @param chain    the Servlet Container provided {@code FilterChain} that will receive the returned request.
239     * @return the {@code ServletRequest} instance that will be passed to the {@code FilterChain} for request processing.
240     * @since 1.0
241     */
242    @SuppressWarnings({"UnusedDeclaration"})
243    protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
244        ServletRequest toUse = request;
245        if (request instanceof HttpServletRequest) {
246            HttpServletRequest http = (HttpServletRequest) request;
247            toUse = wrapServletRequest(http);
248        }
249        return toUse;
250    }
251
252    /**
253     * Returns a new {@link ShiroHttpServletResponse} instance, wrapping the {@code orig} argument, in order to provide
254     * correct URL rewriting behavior required by the Servlet Specification when using Shiro-based sessions (and not
255     * Servlet Container HTTP-based sessions).
256     *
257     * @param orig    the original {@code HttpServletResponse} instance provided by the Servlet Container.
258     * @param request the {@code ShiroHttpServletRequest} instance wrapping the original request.
259     * @return the wrapped ServletResponse instance to use during {@link FilterChain} execution.
260     * @since 1.0
261     */
262    protected ServletResponse wrapServletResponse(HttpServletResponse orig, ShiroHttpServletRequest request) {
263        return new ShiroHttpServletResponse(orig, getServletContext(), request);
264    }
265
266    /**
267     * Prepares the {@code ServletResponse} instance that will be passed to the {@code FilterChain} for request
268     * processing.
269     * <p/>
270     * This implementation delegates to {@link #wrapServletRequest(javax.servlet.http.HttpServletRequest)}
271     * only if Shiro-based sessions are enabled (that is, !{@link #isHttpSessions()}) and the request instance is a
272     * {@link ShiroHttpServletRequest}.  This ensures that any URL rewriting that occurs is handled correctly using the
273     * Shiro-managed Session's sessionId and not a servlet container session ID.
274     * <p/>
275     * If HTTP-based sessions are enabled (the default), then this method does nothing and just returns the
276     * {@code ServletResponse} argument as-is, relying on the default Servlet Container URL rewriting logic.
277     *
278     * @param request  the incoming ServletRequest
279     * @param response the outgoing ServletResponse
280     * @param chain    the Servlet Container provided {@code FilterChain} that will receive the returned request.
281     * @return the {@code ServletResponse} instance that will be passed to the {@code FilterChain} during request processing.
282     * @since 1.0
283     */
284    @SuppressWarnings({"UnusedDeclaration"})
285    protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) {
286        ServletResponse toUse = response;
287        if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest)
288                && (response instanceof HttpServletResponse)) {
289            //the ShiroHttpServletResponse exists to support URL rewriting for session ids.  This is only needed if
290            //using Shiro sessions (i.e. not simple HttpSession based sessions):
291            toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request);
292        }
293        return toUse;
294    }
295
296    /**
297     * Creates a {@link WebSubject} instance to associate with the incoming request/response pair which will be used
298     * throughout the request/response execution.
299     *
300     * @param request  the incoming {@code ServletRequest}
301     * @param response the outgoing {@code ServletResponse}
302     * @return the {@code WebSubject} instance to associate with the request/response execution
303     * @since 1.0
304     */
305    protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
306        return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();
307    }
308
309    /**
310     * Updates any 'native'  Session's last access time that might exist to the timestamp when this method is called.
311     * If native sessions are not enabled (that is, standard Servlet container sessions are being used) or there is no
312     * session ({@code subject.getSession(false) == null}), this method does nothing.
313     * <p/>This method implementation merely calls
314     * <code>Session.{@link org.apache.shiro.session.Session#touch() touch}()</code> on the session.
315     *
316     * @param request  incoming request - ignored, but available to subclasses that might wish to override this method
317     * @param response outgoing response - ignored, but available to subclasses that might wish to override this method
318     * @since 1.0
319     */
320    @SuppressWarnings({"UnusedDeclaration"})
321    protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) {
322        //'native' sessions
323        if (!isHttpSessions()) {
324            Subject subject = SecurityUtils.getSubject();
325            //Subject should never _ever_ be null, but just in case:
326            if (subject != null) {
327                Session session = subject.getSession(false);
328                if (session != null) {
329                    try {
330                        session.touch();
331                    } catch (Throwable t) {
332                        LOGGER.error("session.touch() method invocation has failed.  Unable to update "
333                                + "the corresponding session's last access time based on the incoming request.", t);
334                    }
335                }
336            }
337        }
338    }
339
340    /**
341     * {@code doFilterInternal} implementation that sets-up, executes, and cleans-up a Shiro-filtered request.  It
342     * performs the following ordered operations:
343     * <ol>
344     * <li>{@link #prepareServletRequest(ServletRequest, ServletResponse, FilterChain) Prepares}
345     * the incoming {@code ServletRequest} for use during Shiro's processing</li>
346     * <li>{@link #prepareServletResponse(ServletRequest, ServletResponse, FilterChain) Prepares}
347     * the outgoing {@code ServletResponse} for use during Shiro's processing</li>
348     * <li> {@link #createSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) Creates} a
349     * {@link Subject} instance based on the specified request/response pair.</li>
350     * <li>Finally {@link Subject#execute(Runnable) executes} the
351     * {@link #updateSessionLastAccessTime(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} and
352     * {@link #executeChain(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)}
353     * methods</li>
354     * </ol>
355     * <p/>
356     * The {@code Subject.}{@link Subject#execute(Runnable) execute(Runnable)} call in step #4 is used as an
357     * implementation technique to guarantee proper thread binding and restoration is completed successfully.
358     *
359     * @param servletRequest  the incoming {@code ServletRequest}
360     * @param servletResponse the outgoing {@code ServletResponse}
361     * @param chain           the container-provided {@code FilterChain} to execute
362     * @throws IOException                    if an IO error occurs
363     * @throws javax.servlet.ServletException if a Throwable other than an IOException
364     */
365    protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
366            throws ServletException, IOException {
367
368        Throwable t = null;
369
370        try {
371            final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
372            final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
373
374            final Subject subject = createSubject(request, response);
375
376            subject.execute((Callable<Void>) () -> {
377                updateSessionLastAccessTime(request, response);
378                executeChain(request, response, chain);
379                return null;
380            });
381        } catch (ExecutionException ex) {
382            t = ex.getCause();
383        } catch (Throwable throwable) {
384            t = throwable;
385        }
386
387        if (t != null) {
388            if (t instanceof ServletException) {
389                throw (ServletException) t;
390            }
391            if (t instanceof IOException) {
392                throw (IOException) t;
393            }
394            //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
395            String msg = "Filtered request failed.";
396            throw new ServletException(msg, t);
397        }
398    }
399
400    /**
401     * Returns the {@code FilterChain} to execute for the given request.
402     * <p/>
403     * The {@code origChain} argument is the
404     * original {@code FilterChain} supplied by the Servlet Container, but it may be modified to provide
405     * more behavior by pre-pending further chains according to the Shiro configuration.
406     * <p/>
407     * This implementation returns the chain that will actually be executed by acquiring the chain from a
408     * {@link #getFilterChainResolver() filterChainResolver}.  The resolver determines exactly which chain to
409     * execute, typically based on URL configuration.  If no chain is returned from the resolver call
410     * (returns {@code null}), then the {@code origChain} will be returned by default.
411     *
412     * @param request   the incoming ServletRequest
413     * @param response  the outgoing ServletResponse
414     * @param origChain the original {@code FilterChain} provided by the Servlet Container
415     * @return the {@link FilterChain} to execute for the given request
416     * @since 1.0
417     */
418    protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
419
420        FilterChain chain = origChain;
421
422        FilterChainResolver resolver = getFilterChainResolver();
423        if (resolver == null) {
424            LOGGER.debug("No FilterChainResolver configured.  Returning original FilterChain.");
425            return origChain;
426        }
427
428        FilterChain resolved = resolver.getChain(request, response, origChain);
429        if (resolved != null) {
430            LOGGER.trace("Resolved a configured FilterChain for the current request.");
431            chain = resolved;
432        } else {
433            LOGGER.trace("No FilterChain configured for the current request.  Using the default.");
434        }
435
436        return chain;
437    }
438
439    /**
440     * Executes a {@link FilterChain} for the given request.
441     * <p/>
442     * This implementation first delegates to
443     * <code>{@link #getExecutionChain(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
444     * getExecutionChain}</code>
445     * to allow the application's Shiro configuration to determine exactly how the chain should execute.  The resulting
446     * value from that call is then executed directly by calling the returned {@code FilterChain}'s
447     * {@link FilterChain#doFilter doFilter} method.  That is:
448     * <pre>
449     * FilterChain chain = {@link #getExecutionChain}(request, response, origChain);
450     * chain.{@link FilterChain#doFilter doFilter}(request,response);</pre>
451     *
452     * @param request   the incoming ServletRequest
453     * @param response  the outgoing ServletResponse
454     * @param origChain the Servlet Container-provided chain that may be wrapped further by an application-configured
455     *                  chain of Filters.
456     * @throws IOException      if the underlying {@code chain.doFilter} call results in an IOException
457     * @throws ServletException if the underlying {@code chain.doFilter} call results in a ServletException
458     * @since 1.0
459     */
460    protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
461            throws IOException, ServletException {
462        FilterChain chain = getExecutionChain(request, response, origChain);
463        chain.doFilter(request, response);
464    }
465}