blob: d4ae303de597559d9e243310faacf9e8e85c75bb [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.shiro.web.servlet;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.Flushable;
import org.apache.shiro.subject.ExecutionException;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ThreadContext;
import org.apache.shiro.web.filter.mgt.FilterChainResolver;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.mgt.WebSecurityManager;
import org.apache.shiro.web.subject.WebSubject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.Callable;
/**
* Abstract base class that provides all standard Shiro request filtering behavior and expects
* subclasses to implement configuration-specific logic (INI, XML, .properties, etc).
* <p/>
* Subclasses should perform configuration and construction logic in an overridden
* {@link #init()} method implementation. That implementation should make available any constructed
* {@code SecurityManager} and {@code FilterChainResolver} by calling
* {@link #setSecurityManager(org.apache.shiro.web.mgt.WebSecurityManager)} and
* {@link #setFilterChainResolver(org.apache.shiro.web.filter.mgt.FilterChainResolver)} methods respectively.
* <h3>Static SecurityManager</h3>
* By default the {@code SecurityManager} instance enabled by this filter <em>will not</em> be enabled in static
* memory via the {@code SecurityUtils.}{@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}
* method. Instead, it is expected that Subject instances will always be constructed on a request-processing thread
* via instances of this Filter class.
* <p/>
* However, if you need to construct {@code Subject} instances on separate (non request-processing) threads, it might
* be easiest to enable the SecurityManager to be available in static memory via the
* {@link SecurityUtils#getSecurityManager()} method. You can do this by additionally specifying an {@code init-param}:
* <pre>
* &lt;filter&gt;
* ... other config here ...
* &lt;init-param&gt;
* &lt;param-name&gt;staticSecurityManagerEnabled&lt;/param-name&gt;
* &lt;param-value&gt;true&lt;/param-value&gt;
* &lt;/init-param&gt;
* &lt;/filter&gt;
* </pre>
* See the Shiro <a href="http://shiro.apache.org/subject.html">Subject documentation</a> for more information as to
* if you would do this, particularly the sections on the {@code Subject.Builder} and Thread Association.
*
* @since 1.0
* @see <a href="http://shiro.apache.org/subject.html">Subject documentation</a>
*/
public abstract class AbstractShiroFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(AbstractShiroFilter.class);
private static final String STATIC_INIT_PARAM_NAME = "staticSecurityManagerEnabled";
// Reference to the security manager used by this filter
private WebSecurityManager securityManager;
// Used to determine which chain should handle an incoming request/response
private FilterChainResolver filterChainResolver;
/**
* Whether or not to bind the constructed SecurityManager instance to static memory (via
* SecurityUtils.setSecurityManager). This was added to support https://issues.apache.org/jira/browse/SHIRO-287
* @since 1.2
*/
private boolean staticSecurityManagerEnabled;
protected AbstractShiroFilter() {
this.staticSecurityManagerEnabled = false;
}
public WebSecurityManager getSecurityManager() {
return securityManager;
}
public void setSecurityManager(WebSecurityManager sm) {
this.securityManager = sm;
}
public FilterChainResolver getFilterChainResolver() {
return filterChainResolver;
}
public void setFilterChainResolver(FilterChainResolver filterChainResolver) {
this.filterChainResolver = filterChainResolver;
}
/**
* Returns {@code true} if the constructed {@link #getSecurityManager() securityManager} reference should be bound
* to static memory (via
* {@code SecurityUtils.}{@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}),
* {@code false} otherwise.
* <p/>
* The default value is {@code false}.
* <p/>
*
*
* @return {@code true} if the constructed {@link #getSecurityManager() securityManager} reference should be bound
* to static memory (via {@code SecurityUtils.}{@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}),
* {@code false} otherwise.
* @since 1.2
* @see <a href="https://issues.apache.org/jira/browse/SHIRO-287">SHIRO-287</a>
*/
public boolean isStaticSecurityManagerEnabled() {
return staticSecurityManagerEnabled;
}
/**
* Sets if the constructed {@link #getSecurityManager() securityManager} reference should be bound
* to static memory (via {@code SecurityUtils.}{@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}).
* <p/>
* The default value is {@code false}.
*
* @param staticSecurityManagerEnabled if the constructed {@link #getSecurityManager() securityManager} reference
* should be bound to static memory (via
* {@code SecurityUtils.}{@link SecurityUtils#setSecurityManager(org.apache.shiro.mgt.SecurityManager) setSecurityManager}).
* @since 1.2
* @see <a href="https://issues.apache.org/jira/browse/SHIRO-287">SHIRO-287</a>
*/
public void setStaticSecurityManagerEnabled(boolean staticSecurityManagerEnabled) {
this.staticSecurityManagerEnabled = staticSecurityManagerEnabled;
}
protected final void onFilterConfigSet() throws Exception {
//added in 1.2 for SHIRO-287:
applyStaticSecurityManagerEnabledConfig();
init();
ensureSecurityManager();
//added in 1.2 for SHIRO-287:
if (isStaticSecurityManagerEnabled()) {
SecurityUtils.setSecurityManager(getSecurityManager());
}
}
/**
* Checks if the init-param that configures the filter to use static memory has been configured, and if so,
* sets the {@link #setStaticSecurityManagerEnabled(boolean)} attribute with the configured value.
*
* @since 1.2
* @see <a href="https://issues.apache.org/jira/browse/SHIRO-287">SHIRO-287</a>
*/
private void applyStaticSecurityManagerEnabledConfig() {
String value = getInitParam(STATIC_INIT_PARAM_NAME);
if (value != null) {
Boolean b = Boolean.valueOf(value);
if (b != null) {
setStaticSecurityManagerEnabled(b);
}
}
}
public void init() throws Exception {
}
/**
* A fallback mechanism called in {@link #onFilterConfigSet()} to ensure that the
* {@link #getSecurityManager() securityManager} property has been set by configuration, and if not,
* creates one automatically.
*/
private void ensureSecurityManager() {
WebSecurityManager securityManager = getSecurityManager();
if (securityManager == null) {
log.info("No SecurityManager configured. Creating default.");
securityManager = createDefaultSecurityManager();
setSecurityManager(securityManager);
}
}
protected WebSecurityManager createDefaultSecurityManager() {
return new DefaultWebSecurityManager();
}
protected boolean isHttpSessions() {
return getSecurityManager().isHttpSessionMode();
}
/**
* Wraps the original HttpServletRequest in a {@link ShiroHttpServletRequest}, which is required for supporting
* Servlet Specification behavior backed by a {@link org.apache.shiro.subject.Subject Subject} instance.
*
* @param orig the original Servlet Container-provided incoming {@code HttpServletRequest} instance.
* @return {@link ShiroHttpServletRequest ShiroHttpServletRequest} instance wrapping the original.
* @since 1.0
*/
protected ServletRequest wrapServletRequest(HttpServletRequest orig) {
return new ShiroHttpServletRequest(orig, getServletContext(), isHttpSessions());
}
/**
* Prepares the {@code ServletRequest} instance that will be passed to the {@code FilterChain} for request
* processing.
* <p/>
* If the {@code ServletRequest} is an instance of {@link HttpServletRequest}, the value returned from this method
* is obtained by calling {@link #wrapServletRequest(javax.servlet.http.HttpServletRequest)} to allow Shiro-specific
* HTTP behavior, otherwise the original {@code ServletRequest} argument is returned.
*
* @param request the incoming ServletRequest
* @param response the outgoing ServletResponse
* @param chain the Servlet Container provided {@code FilterChain} that will receive the returned request.
* @return the {@code ServletRequest} instance that will be passed to the {@code FilterChain} for request processing.
* @since 1.0
*/
@SuppressWarnings({"UnusedDeclaration"})
protected ServletRequest prepareServletRequest(ServletRequest request, ServletResponse response, FilterChain chain) {
ServletRequest toUse = request;
if (request instanceof HttpServletRequest) {
HttpServletRequest http = (HttpServletRequest) request;
toUse = wrapServletRequest(http);
}
return toUse;
}
/**
* Returns a new {@link ShiroHttpServletResponse} instance, wrapping the {@code orig} argument, in order to provide
* correct URL rewriting behavior required by the Servlet Specification when using Shiro-based sessions (and not
* Servlet Container HTTP-based sessions).
*
* @param orig the original {@code HttpServletResponse} instance provided by the Servlet Container.
* @param request the {@code ShiroHttpServletRequest} instance wrapping the original request.
* @return the wrapped ServletResponse instance to use during {@link FilterChain} execution.
* @since 1.0
*/
protected ServletResponse wrapServletResponse(HttpServletResponse orig, ShiroHttpServletRequest request) {
return new ShiroHttpServletResponse(orig, getServletContext(), request);
}
/**
* Prepares the {@code ServletResponse} instance that will be passed to the {@code FilterChain} for request
* processing.
* <p/>
* This implementation delegates to {@link #wrapServletRequest(javax.servlet.http.HttpServletRequest)}
* only if Shiro-based sessions are enabled (that is, !{@link #isHttpSessions()}) and the request instance is a
* {@link ShiroHttpServletRequest}. This ensures that any URL rewriting that occurs is handled correctly using the
* Shiro-managed Session's sessionId and not a servlet container session ID.
* <p/>
* If HTTP-based sessions are enabled (the default), then this method does nothing and just returns the
* {@code ServletResponse} argument as-is, relying on the default Servlet Container URL rewriting logic.
*
* @param request the incoming ServletRequest
* @param response the outgoing ServletResponse
* @param chain the Servlet Container provided {@code FilterChain} that will receive the returned request.
* @return the {@code ServletResponse} instance that will be passed to the {@code FilterChain} during request processing.
* @since 1.0
*/
@SuppressWarnings({"UnusedDeclaration"})
protected ServletResponse prepareServletResponse(ServletRequest request, ServletResponse response, FilterChain chain) {
ServletResponse toUse = response;
if (!isHttpSessions() && (request instanceof ShiroHttpServletRequest) &&
(response instanceof HttpServletResponse)) {
//the ShiroHttpServletResponse exists to support URL rewriting for session ids. This is only needed if
//using Shiro sessions (i.e. not simple HttpSession based sessions):
toUse = wrapServletResponse((HttpServletResponse) response, (ShiroHttpServletRequest) request);
}
return toUse;
}
/**
* Creates a {@link WebSubject} instance to associate with the incoming request/response pair which will be used
* throughout the request/response execution.
*
* @param request the incoming {@code ServletRequest}
* @param response the outgoing {@code ServletResponse}
* @return the {@code WebSubject} instance to associate with the request/response execution
* @since 1.0
*/
protected WebSubject createSubject(ServletRequest request, ServletResponse response) {
WebSubject.Builder builder = new WebSubject.Builder(getSecurityManager(), request, response);
builder.sessionUpdateDeferred(true); //added in 1.3 - MUST be accompanied by finally 'flush'.
return builder.buildWebSubject();
}
/**
* Updates any 'native' Session's last access time that might exist to the timestamp when this method is called.
* If native sessions are not enabled (that is, standard Servlet container sessions are being used) or there is no
* session ({@code subject.getSession(false) == null}), this method does nothing.
* <p/>This method implementation merely calls
* <code>Session.{@link org.apache.shiro.session.Session#touch() touch}()</code> on the session.
*
* @param request incoming request - ignored, but available to subclasses that might wish to override this method
* @param response outgoing response - ignored, but available to subclasses that might wish to override this method
* @since 1.0
*/
@SuppressWarnings({"UnusedDeclaration"})
protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) {
if (!isHttpSessions()) { //'native' sessions
Subject subject = SecurityUtils.getSubject();
//Subject should never _ever_ be null, but just in case:
if (subject != null) {
Session session = subject.getSession(false);
if (session != null) {
try {
session.touch();
} catch (Throwable t) {
log.error("session.touch() method invocation has failed. Unable to update" +
"the corresponding session's last access time based on the incoming request.", t);
}
}
}
}
}
/**
* {@code doFilterInternal} implementation that sets-up, executes, and cleans-up a Shiro-filtered request. It
* performs the following ordered operations:
* <ol>
* <li>{@link #prepareServletRequest(ServletRequest, ServletResponse, FilterChain) Prepares}
* the incoming {@code ServletRequest} for use during Shiro's processing</li>
* <li>{@link #prepareServletResponse(ServletRequest, ServletResponse, FilterChain) Prepares}
* the outgoing {@code ServletResponse} for use during Shiro's processing</li>
* <li> {@link #createSubject(javax.servlet.ServletRequest, javax.servlet.ServletResponse) Creates} a
* {@link Subject} instance based on the specified request/response pair.</li>
* <li>Finally {@link Subject#execute(Runnable) executes} the
* {@link #updateSessionLastAccessTime(javax.servlet.ServletRequest, javax.servlet.ServletResponse)} and
* {@link #executeChain(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)}
* methods</li>
* </ol>
* <p/>
* The {@code Subject.}{@link Subject#execute(Runnable) execute(Runnable)} call in step #4 is used as an
* implementation technique to guarantee proper thread binding and restoration is completed successfully.
*
* @param servletRequest the incoming {@code ServletRequest}
* @param servletResponse the outgoing {@code ServletResponse}
* @param chain the container-provided {@code FilterChain} to execute
* @throws IOException if an IO error occurs
* @throws javax.servlet.ServletException if an Throwable other than an IOException
*/
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)
throws ServletException, IOException {
Throwable t = null;
try {
final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
final Subject subject = createSubject(request, response);
try {
//noinspection unchecked
subject.execute(new Callable() {
public Object call() throws Exception {
try {
updateSessionLastAccessTime(request, response);
executeChain(request, response, chain);
return null;
} finally {
Session session = subject.getSession(false);
if (session instanceof Flushable) {
((Flushable) session).flush();
}
}
}
});
} finally {
ThreadContext.remove(); //silence innocuous Tomcat ThreadLocal warnings
}
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}
if (t != null) {
if (t instanceof ServletException) {
throw (ServletException) t;
}
if (t instanceof IOException) {
throw (IOException) t;
}
//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:
String msg = "Filtered request failed.";
throw new ServletException(msg, t);
}
}
/**
* Returns the {@code FilterChain} to execute for the given request.
* <p/>
* The {@code origChain} argument is the
* original {@code FilterChain} supplied by the Servlet Container, but it may be modified to provide
* more behavior by pre-pending further chains according to the Shiro configuration.
* <p/>
* This implementation returns the chain that will actually be executed by acquiring the chain from a
* {@link #getFilterChainResolver() filterChainResolver}. The resolver determines exactly which chain to
* execute, typically based on URL configuration. If no chain is returned from the resolver call
* (returns {@code null}), then the {@code origChain} will be returned by default.
*
* @param request the incoming ServletRequest
* @param response the outgoing ServletResponse
* @param origChain the original {@code FilterChain} provided by the Servlet Container
* @return the {@link FilterChain} to execute for the given request
* @since 1.0
*/
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;
FilterChainResolver resolver = getFilterChainResolver();
if (resolver == null) {
log.debug("No FilterChainResolver configured. Returning original FilterChain.");
return origChain;
}
FilterChain resolved = resolver.getChain(request, response, origChain);
if (resolved != null) {
log.trace("Resolved a configured FilterChain for the current request.");
chain = resolved;
} else {
log.trace("No FilterChain configured for the current request. Using the default.");
}
return chain;
}
/**
* Executes a {@link FilterChain} for the given request.
* <p/>
* This implementation first delegates to
* <code>{@link #getExecutionChain(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain) getExecutionChain}</code>
* to allow the application's Shiro configuration to determine exactly how the chain should execute. The resulting
* value from that call is then executed directly by calling the returned {@code FilterChain}'s
* {@link FilterChain#doFilter doFilter} method. That is:
* <pre>
* FilterChain chain = {@link #getExecutionChain}(request, response, origChain);
* chain.{@link FilterChain#doFilter doFilter}(request,response);</pre>
*
* @param request the incoming ServletRequest
* @param response the outgoing ServletResponse
* @param origChain the Servlet Container-provided chain that may be wrapped further by an application-configured
* chain of Filters.
* @throws IOException if the underlying {@code chain.doFilter} call results in an IOException
* @throws ServletException if the underlying {@code chain.doFilter} call results in a ServletException
* @since 1.0
*/
protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
throws IOException, ServletException {
FilterChain chain = getExecutionChain(request, response, origChain);
chain.doFilter(request, response);
}
}