View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.web.servlet;
20  
21  import org.apache.shiro.SecurityUtils;
22  import org.apache.shiro.session.Session;
23  import org.apache.shiro.subject.ExecutionException;
24  import org.apache.shiro.subject.Subject;
25  import org.apache.shiro.web.config.ShiroFilterConfiguration;
26  import org.apache.shiro.web.filter.mgt.FilterChainResolver;
27  import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
28  import org.apache.shiro.web.mgt.WebSecurityManager;
29  import org.apache.shiro.web.subject.WebSubject;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import javax.servlet.FilterChain;
34  import javax.servlet.ServletException;
35  import javax.servlet.ServletRequest;
36  import javax.servlet.ServletResponse;
37  import javax.servlet.http.HttpServletRequest;
38  import javax.servlet.http.HttpServletResponse;
39  import java.io.IOException;
40  import java.util.concurrent.Callable;
41  
42  /**
43   * Abstract base class that provides all standard Shiro request filtering behavior and expects
44   * subclasses to implement configuration-specific logic (INI, XML, .properties, etc.).
45   * <p/>
46   * Subclasses should perform configuration and construction logic in an overridden
47   * {@link #init()} method implementation.  That implementation should make available any constructed
48   * {@code SecurityManager} and {@code FilterChainResolver} by calling
49   * {@link #setSecurityManager(org.apache.shiro.web.mgt.WebSecurityManager)} and
50   * {@link #setFilterChainResolver(org.apache.shiro.web.filter.mgt.FilterChainResolver)} methods respectively.
51   * <h3>Static SecurityManager</h3>
52   * By default, the {@code SecurityManager} instance enabled by this filter <em>will not</em> be enabled in static
53   * memory via the {@code SecurityUtils.}
54   * {@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}
55   * method.  Instead, it is expected that Subject instances will always be constructed on a request-processing thread
56   * via instances of this Filter class.
57   * <p/>
58   * However, if you need to construct {@code Subject} instances on separate (non request-processing) threads, it might
59   * be easiest to enable the SecurityManager to be available in static memory via the
60   * {@link SecurityUtils#getSecurityManager()} method.  You can do this by additionally specifying an {@code init-param}:
61   * <pre>
62   * &lt;filter&gt;
63   *     ... other config here ...
64   *     &lt;init-param&gt;
65   *         &lt;param-name&gt;staticSecurityManagerEnabled&lt;/param-name&gt;
66   *         &lt;param-value&gt;true&lt;/param-value&gt;
67   *     &lt;/init-param&gt;
68   * &lt;/filter&gt;
69   * </pre>
70   * See the Shiro <a href="http://shiro.apache.org/subject.html">Subject documentation</a> for more information as to
71   * if you would do this, particularly the sections on the {@code Subject.Builder} and Thread Association.
72   *
73   * @see <a href="http://shiro.apache.org/subject.html">Subject documentation</a>
74   * @since 1.0
75   */
76  public abstract class AbstractShiroFilter extends OncePerRequestFilter {
77  
78      private static final Logger LOGGER = LoggerFactory.getLogger(AbstractShiroFilter.class);
79  
80      private static final String STATIC_INIT_PARAM_NAME = "staticSecurityManagerEnabled";
81  
82      // Reference to the security manager used by this filter
83      private WebSecurityManager securityManager;
84  
85      // Used to determine which chain should handle an incoming request/response
86      private FilterChainResolver filterChainResolver;
87  
88      /**
89       * Whether to bind the constructed SecurityManager instance to static memory (via
90       * SecurityUtils.setSecurityManager).
91       * This was added to support <a href="https://issues.apache.org/jira/browse/SHIRO-287"/>
92       *
93       * @since 1.2
94       */
95      private boolean staticSecurityManagerEnabled;
96  
97      protected AbstractShiroFilter() {
98          this.staticSecurityManagerEnabled = false;
99      }
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 }