Browse Source

SEC-1039: Created new filter SecurityContextPersistenceFilter and SecurityContextRepository strategy to replace HttpSessionContextIntegrationFilter functionality.

Luke Taylor 17 năm trước cách đây
mục cha
commit
4d81d750cd

+ 39 - 0
core/src/main/java/org/springframework/security/context/HttpRequestResponseHolder.java

@@ -0,0 +1,39 @@
+package org.springframework.security.context;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Used to pass the incoming request to {@link SecurityContextRepository#loadContext(HttpRequestResponseHolder)},
+ * allowing the method to swap the request for a wrapped version, as well as returning the <tt>SecurityContext</tt>
+ * value.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ * @since 2.5
+ */
+public class HttpRequestResponseHolder {
+    HttpServletRequest request;
+    HttpServletResponse response;
+
+    public HttpRequestResponseHolder(HttpServletRequest request, HttpServletResponse response) {
+        this.request = request;
+        this.response = response;
+    }
+
+    HttpServletRequest getRequest() {
+        return request;
+    }
+
+    void setRequest(HttpServletRequest request) {
+        this.request = request;
+    }
+
+    HttpServletResponse getResponse() {
+        return response;
+    }
+
+    void setResponse(HttpServletResponse response) {
+        this.response = response;
+    }
+}

+ 329 - 0
core/src/main/java/org/springframework/security/context/HttpSessionSecurityContextRepository.java

@@ -0,0 +1,329 @@
+package org.springframework.security.context;
+
+import java.lang.reflect.Method;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.security.AuthenticationTrustResolver;
+import org.springframework.security.AuthenticationTrustResolverImpl;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * A <tt>SecurityContextRepository</tt> implementation which stores the security context in the HttpSession between
+ * requests.
+ * <p>
+ * The <code>HttpSession</code> will be queried to retrieve the <code>SecurityContext</code> in the <tt>loadContext</tt>
+ * method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY}). If a valid <code>SecurityContext</code> cannot be
+ * obtained from the <code>HttpSession</code> for whatever reason, a fresh <code>SecurityContext</code> will be created
+ * and returned instead. The created object will be an instance of the class set using the
+ * {@link #setContextClass(Class)} method. If this hasn't been set, a {@link SecurityContextImpl} will be returned.
+ * <p>
+ * When <tt>saveContext</tt> is called, the context will be stored under the same key, provided
+ * <ol>
+ * <li>The value has changed</li>
+ * <li>The configured <tt>AuthenticationTrustResolver</tt> does not report that the contents represent an anonymous
+ * user</li>
+ * </ol>
+ * <p>
+ * With the standard configuration, no <code>HttpSession</code> will be created during <tt>loadContext</tt> if one does
+ * not already exist. When <tt>saveContext</tt> is called at the end of the web request, and no session exists, a new
+ * <code>HttpSession</code> will <b>only</b> be created if the supplied <tt>SecurityContext</tt> is not equal
+ * to a <code>new</code> instance of the {@link #setContextClass(Class) contextClass} (or an empty
+ * <tt>SecurityContextImpl</tt> if the class has not been set. This avoids needless <code>HttpSession</code> creation,
+ * but automates the storage of changes made to the context during the request. Note that if
+ * {@link SecurityContextPersistenceFilter} is configured to eagerly create sessions, then the session-minimisation
+ * logic applied here will not make any difference. If you are using eager session creation, then you should
+ * ensure that the <tt>allowSessionCreation</tt> property of this class is set to <tt>true</tt> (the default).
+ * <p>
+ * If for whatever reason no <code>HttpSession</code> should <b>ever</b> be created (e.g. Basic authentication is being
+ * used or similar clients that will never present the same <code>jsessionid</code> etc), then
+ * {@link #setAllowSessionCreation(boolean) allowSessionCreation} should be set to <code>false</code>.
+ * Only do this if you really need to conserve server memory and ensure all classes using the
+ * <code>SecurityContextHolder</code> are designed to have no persistence of the <code>SecurityContext</code>
+ * between web requests.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ * @since 2.5
+ */
+public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
+    public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
+
+    protected final Log logger = LogFactory.getLog(this.getClass());
+
+    private Class<? extends SecurityContext> securityContextClass = null;
+    /** SecurityContext instance used to check for equality with default (unauthenticated) content */
+    private Object contextObject = new SecurityContextImpl();
+    private boolean cloneFromHttpSession = false;
+    private boolean allowSessionCreation = true;
+
+    private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
+
+    public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
+        HttpServletRequest request = requestResponseHolder.getRequest();
+        HttpServletResponse response = requestResponseHolder.getResponse();
+        HttpSession httpSession = request.getSession(false);
+
+        SecurityContext context = readSecurityContextFromSession(httpSession);
+
+        if (context == null) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("No SecurityContext was available from the HttpSession: " + httpSession +". " +
+                        "A new one will be created.");
+            }
+            context = generateNewContext();
+
+        }
+
+        requestResponseHolder.setResponse(new SaveToSessionResponseWrapper(response, request,
+                httpSession != null, context.hashCode()));
+
+        return context;
+    }
+
+    public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {
+        SaveToSessionResponseWrapper responseWrapper = (SaveToSessionResponseWrapper)response;
+        // saveContext() might already be called by the response wrapper
+        // if something in the chain called sendError() or sendRedirect(). This ensures we only call it
+        // once per request.
+        if (!responseWrapper.isContextSaved() ) {
+            responseWrapper.saveContext(context);
+        }
+    }
+
+
+
+    /**
+     * Gets the security context from the session (if available) and returns it.
+     * <p>
+     * If the session is null, the context object is null or the context object stored in the session
+     * is not an instance of SecurityContext it will return null.
+     * <p>
+     * If <tt>cloneFromHttpSession</tt> is set to true, it will attempt to clone the context object
+     * and return the cloned instance.
+     *
+     * @param httpSession the session obtained from the request.
+     */
+    private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
+        if (httpSession == null) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("No HttpSession currently exists");
+            }
+
+            return null;
+        }
+
+        // Session exists, so try to obtain a context from it.
+
+        Object contextFromSession = httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
+
+        if (contextFromSession == null) {
+            if (logger.isDebugEnabled()) {
+                logger.debug("HttpSession returned null object for SPRING_SECURITY_CONTEXT");
+            }
+
+            return null;
+        }
+
+        // We now have the security context object from the session.
+        if (!(contextFromSession instanceof SecurityContext)) {
+            if (logger.isWarnEnabled()) {
+                logger.warn("SPRING_SECURITY_CONTEXT did not contain a SecurityContext but contained: '"
+                        + contextFromSession + "'; are you improperly modifying the HttpSession directly "
+                        + "(you should always use SecurityContextHolder) or using the HttpSession attribute "
+                        + "reserved for this class?");
+            }
+
+            return null;
+        }
+
+        // Clone if required (see SEC-356)
+        if (cloneFromHttpSession) {
+            contextFromSession = cloneContext(contextFromSession);
+        }
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: '" + contextFromSession + "'");
+        }
+
+        // Everything OK. The only non-null return from this method.
+
+        return (SecurityContext) contextFromSession;
+    }
+
+    /**
+     *
+     * @param context the object which was stored under the security context key in the HttpSession.
+     * @return the cloned SecurityContext object. Never null.
+     */
+    private Object cloneContext(Object context) {
+        Object clonedContext = null;
+        Assert.isInstanceOf(Cloneable.class, context,
+                "Context must implement Cloneable and provide a Object.clone() method");
+        try {
+            Method m = context.getClass().getMethod("clone", new Class[]{});
+            if (!m.isAccessible()) {
+                m.setAccessible(true);
+            }
+            clonedContext = m.invoke(context, new Object[]{});
+        } catch (Exception ex) {
+            ReflectionUtils.handleReflectionException(ex);
+        }
+
+        return clonedContext;
+    }
+
+    /**
+     * By default, returns an instance of {@link SecurityContextImpl}.
+     * If a custom <tt>SecurityContext</tt> implementation is in use (i.e. the <tt>securityContextClass</tt> property
+     * is set), it will attempt to invoke the no-args constructor on the supplied class instead and return the created
+     * instance.
+     *
+     * @return a new SecurityContext instance. Never null.
+     */
+    SecurityContext generateNewContext() {
+        if (securityContextClass == null) {
+            return new SecurityContextImpl();
+        }
+
+        SecurityContext context = null;
+        try {
+            context = securityContextClass.newInstance();
+        } catch (Exception e) {
+            ReflectionUtils.handleReflectionException(e);
+        }
+        return context;
+    }
+
+    @SuppressWarnings("unchecked")
+    void setSecurityContextClass(Class contextClass) {
+        if (contextClass == null || (!SecurityContext.class.isAssignableFrom(contextClass))) {
+            throw new IllegalArgumentException("securityContextClass must implement SecurityContext "
+                    + "(typically use org.springframework.security.context.SecurityContextImpl; existing class is "
+                    + contextClass + ")");
+        }
+
+        this.securityContextClass = contextClass;
+        contextObject = generateNewContext();
+    }
+
+    void setCloneFromHttpSession(boolean cloneFromHttpSession) {
+        this.cloneFromHttpSession = cloneFromHttpSession;
+    }
+
+    void setAllowSessionCreation(boolean allowSessionCreation) {
+        this.allowSessionCreation = allowSessionCreation;
+    }
+
+    //~ Inner Classes ==================================================================================================
+
+    /**
+     * Wrapper that is applied to every request/response to update the <code>HttpSession<code> with
+     * the <code>SecurityContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
+     * happens. See SEC-398.
+     * <p>
+     * Stores the necessary state from the start of the request in order to make a decision about whether
+     * the security context has changed before saving it.
+     */
+    class SaveToSessionResponseWrapper extends SaveContextOnUpdateOrErrorResponseWrapper {
+
+        private HttpServletRequest request;
+        private boolean httpSessionExistedAtStartOfRequest;
+        private int contextHashBeforeChainExecution;
+
+        /**
+         * Takes the parameters required to call <code>saveContext()</code> successfully in
+         * addition to the request and the response object we are wrapping.
+         *
+         * @param request the request object (used to obtain the session, if one exists).
+         * @param httpSessionExistedAtStartOfRequest indicates whether there was a session in place before the
+         *        filter chain executed. If this is true, and the session is found to be null, this indicates that it was
+         *        invalidated during the request and a new session will now be created.
+         * @param contextHashBeforeChainExecution the hashcode of the context before the filter chain executed.
+         *        The context will only be stored if it has a different hashcode, indicating that the context changed
+         *        during the request.
+         */
+        SaveToSessionResponseWrapper(HttpServletResponse response, HttpServletRequest request,
+                                                      boolean httpSessionExistedAtStartOfRequest,
+                                                      int contextHashBeforeChainExecution) {
+            super(response);
+            this.request = request;
+            this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
+            this.contextHashBeforeChainExecution = contextHashBeforeChainExecution;
+        }
+
+        /**
+         * Stores the supplied security context in the session (if available) and if it has changed since it was
+         * set at the start of the request. If the AuthenticationTrustResolver identifies the current user as
+         * anonymous, then the context will not be stored.
+         *
+         * @param context the context object obtained from the SecurityContextHolder after the request has
+         *        been processed by the filter chain. SecurityContextHolder.getContext() cannot be used to obtain
+         *        the context as it has already been cleared by the time this method is called.
+         *
+         */
+        @Override
+        void saveContext(SecurityContext context) {
+            HttpSession httpSession = request.getSession(false);
+
+            if (httpSession == null) {
+                if (httpSessionExistedAtStartOfRequest) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("HttpSession is now null, but was not null at start of request; "
+                                + "session was invalidated, so do not create a new session");
+                    }
+                } else {
+                    // Generate a HttpSession only if we need to
+
+                    if (!allowSessionCreation) {
+                        if (logger.isDebugEnabled()) {
+                            logger.debug("The HttpSession is currently null, and the "
+                                            + "HttpSessionContextIntegrationFilter is prohibited from creating an HttpSession "
+                                            + "(because the allowSessionCreation property is false) - SecurityContext thus not "
+                                            + "stored for next request");
+                        }
+                    } else if (!contextObject.equals(context)) {
+                        if (logger.isDebugEnabled()) {
+                            logger.debug("HttpSession being created as SecurityContextHolder contents are non-default");
+                        }
+
+                        try {
+                            httpSession = request.getSession(true);
+                        } catch (IllegalStateException e) {
+                            // Response must already be committed, therefore can't create a new session
+                        }
+
+                    } else {
+                        if (logger.isDebugEnabled()) {
+                            logger.debug("HttpSession is null, but SecurityContextHolder has not changed from default: ' "
+                                    + context
+                                    + "'; not creating HttpSession or storing SecurityContextHolder contents");
+                        }
+                    }
+                }
+            }
+
+            // If HttpSession exists, store current SecurityContextHolder contents but only if
+            // the SecurityContext has actually changed (see JIRA SEC-37)
+            if (httpSession != null && context.hashCode() != contextHashBeforeChainExecution) {
+                // See SEC-766
+                if (authenticationTrustResolver.isAnonymous(context.getAuthentication())) {
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("SecurityContext contents are anonymous - context will not be stored in HttpSession. ");
+                    }
+                } else {
+                    httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
+
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("SecurityContext stored to HttpSession: '" + context + "'");
+                    }
+                }
+            }
+        }
+    }
+}

+ 77 - 0
core/src/main/java/org/springframework/security/context/SaveContextOnUpdateOrErrorResponseWrapper.java

@@ -0,0 +1,77 @@
+package org.springframework.security.context;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * Base class for response wrappers which encapsulate the logic for storing a security context and which
+ * store the with the <code>SecurityContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
+ * happens. See SEC-398.
+ * <p>
+ * Sub-classes should implement the {@link #saveContext(SecurityContext context)} method.
+ *
+ * @author Luke Taylor
+ * @author Marten Algesten
+ * @version $Id$
+ * @since 2.5
+ */
+abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServletResponseWrapper {
+
+    boolean contextSaved = false;
+
+    SaveContextOnUpdateOrErrorResponseWrapper(HttpServletResponse response) {
+        super(response);
+    }
+
+    /**
+     * Implements the logic for storing the security context.
+     *
+     * @param context the <tt>SecurityContext</tt> instance to store
+     */
+    abstract void saveContext(SecurityContext context);
+
+    /**
+     * Makes sure the session is updated before calling the
+     * superclass <code>sendError()</code>
+     */
+    public void sendError(int sc) throws IOException {
+        doSaveContext();
+        super.sendError(sc);
+    }
+
+    /**
+     * Makes sure the session is updated before calling the
+     * superclass <code>sendError()</code>
+     */
+    public void sendError(int sc, String msg) throws IOException {
+        doSaveContext();
+        super.sendError(sc, msg);
+    }
+
+    /**
+     * Makes sure the context is stored before calling the
+     * superclass <code>sendRedirect()</code>
+     */
+    public void sendRedirect(String location) throws IOException {
+        doSaveContext();
+        super.sendRedirect(location);
+    }
+
+    /**
+     * Calls <code>saveContext()</code> with the current contents of the <tt>SecurityContextHolder</tt>.
+     */
+    private void doSaveContext() {
+        saveContext(SecurityContextHolder.getContext());
+        contextSaved = true;
+    }
+
+    /**
+     * Tells if the response wrapper has called <code>saveContext()</code> because of an error or redirect.
+     */
+    public boolean isContextSaved() {
+        return contextSaved;
+    }
+
+}

+ 16 - 12
core/src/main/java/org/springframework/security/context/SecurityContextHolder.java

@@ -21,20 +21,24 @@ import java.lang.reflect.Constructor;
 
 
 /**
- * Associates a given {@link SecurityContext} with the current execution thread.<p>This class provides a series of
- * static methods that delegate to an instance of {@link org.springframework.security.context.SecurityContextHolderStrategy}. The
- * purpose of the class is to provide a convenient way to specify the strategy that should be used for a given JVM.
+ * Associates a given {@link SecurityContext} with the current execution thread.
+ * <p>
+ * This class provides a series of static methods that delegate to an instance of
+ * {@link org.springframework.security.context.SecurityContextHolderStrategy}. The purpose of the class is to provide a
+ * convenient way to specify the strategy that should be used for a given JVM.
  * This is a JVM-wide setting, since everything in this class is <code>static</code> to facilitate ease of use in
- * calling code.</p>
- *  <p>To specify which strategy should be used, you must provide a mode setting. A mode setting is one of the
+ * calling code.
+ * <p>
+ * To specify which strategy should be used, you must provide a mode setting. A mode setting is one of the
  * three valid <code>MODE_</code> settings defined as <code>static final</code> fields, or a fully qualified classname
- * to a concrete implementation of {@link org.springframework.security.context.SecurityContextHolderStrategy} that provides a
- * public no-argument constructor.</p>
- *  <p>There are two ways to specify the desired strategy mode <code>String</code>. The first is to specify it via
+ * to a concrete implementation of {@link org.springframework.security.context.SecurityContextHolderStrategy} that
+ * provides a public no-argument constructor.
+ * <p>
+ * There are two ways to specify the desired strategy mode <code>String</code>. The first is to specify it via
  * the system property keyed on {@link #SYSTEM_PROPERTY}. The second is to call {@link #setStrategyName(String)}
  * before using the class. If neither approach is used, the class will default to using {@link #MODE_THREADLOCAL},
  * which is backwards compatible, has fewer JVM incompatibilities and is appropriate on servers (whereas {@link
- * #MODE_GLOBAL} is definitely inappropriate for server use).</p>
+ * #MODE_GLOBAL} is definitely inappropriate for server use).
  *
  * @author Ben Alex
  * @version $Id$
@@ -75,7 +79,7 @@ public class SecurityContextHolder {
     }
 
     /**
-     * Primarily for troubleshooting purposes, this method shows how many times the class has reinitialized its
+     * Primarily for troubleshooting purposes, this method shows how many times the class has re-initialized its
      * <code>SecurityContextHolderStrategy</code>.
      *
      * @return the count (should be one unless you've called {@link #setStrategyName(String)} to switch to an alternate
@@ -122,9 +126,9 @@ public class SecurityContextHolder {
 
     /**
      * Changes the preferred strategy. Do <em>NOT</em> call this method more than once for a given JVM, as it
-     * will reinitialize the strategy and adversely affect any existing threads using the old strategy.
+     * will re-initialize the strategy and adversely affect any existing threads using the old strategy.
      *
-     * @param strategyName the fully qualified classname of the strategy that should be used.
+     * @param strategyName the fully qualified class name of the strategy that should be used.
      */
     public static void setStrategyName(String strategyName) {
         SecurityContextHolder.strategyName = strategyName;

+ 94 - 0
core/src/main/java/org/springframework/security/context/SecurityContextPersistenceFilter.java

@@ -0,0 +1,94 @@
+package org.springframework.security.context;
+
+import java.io.IOException;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.springframework.security.ui.FilterChainOrder;
+import org.springframework.security.ui.SpringSecurityFilter;
+
+/**
+ * Populates the {@link SecurityContextHolder} with information obtained from
+ * the configured {@link SecurityContextRepository} prior to the request and stores it back in the repository
+ * once the request has completed. By default it uses an {@link HttpSessionSecurityContextRepository}. See this
+ * class for information <tt>HttpSession</tt> related configuration options.
+ * <p>
+ * This filter will only execute once per request, to resolve servlet container (specifically Weblogic)
+ * incompatibilities.
+ * <p>
+ * This filter MUST be executed BEFORE any authentication processing mechanisms. Authentication processing mechanisms
+ * (e.g. BASIC, CAS processing filters etc) expect the <code>SecurityContextHolder</code> to contain a valid
+ * <code>SecurityContext</code> by the time they execute.
+ * <p>
+ * This is essentially a refactoring of the old <tt>HttpSessionContextIntegrationFilter</tt> to delegate
+ * the storage issues to a separate strategy, allowing for more customization in the way the security context is
+ * maintained between requests.
+ * <p>
+ * The <tt>forceEagerSessionCreation</tt> property can be used to ensure that a session is always available before
+ * the filter chain executes (the default is <code>false</code>, as this is resource intensive and not recommended).
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ * @since 2.5
+ */
+public class SecurityContextPersistenceFilter extends SpringSecurityFilter {
+
+    static final String FILTER_APPLIED = "__spring_security_scpf_applied";
+
+    private SecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+
+    private boolean forceEagerSessionCreation = false;
+
+    @Override
+    protected void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        if (request.getAttribute(FILTER_APPLIED) != null) {
+            // ensure that filter is only applied once per request
+            chain.doFilter(request, response);
+            return;
+        }
+
+        request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
+
+        if (forceEagerSessionCreation) {
+            HttpSession session = request.getSession();
+            logger.debug("Eagerly created session: " + session.getId());
+        }
+
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+
+        try {
+            SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
+            SecurityContextHolder.setContext(contextBeforeChainExecution);
+
+            chain.doFilter(holder.getRequest(), holder.getResponse());
+
+        } finally {
+            SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
+            // Crucial removal of SecurityContextHolder contents - do this before anything else.
+            SecurityContextHolder.clearContext();
+            repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("SecurityContextHolder now cleared, as request processing completed");
+            }
+        }
+    }
+
+    public void setSecurityContextRepository(SecurityContextRepository repo) {
+        this.repo = repo;
+    }
+
+    void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
+        this.forceEagerSessionCreation = forceEagerSessionCreation;
+    }
+
+    public int getOrder() {
+        return FilterChainOrder.SECURITY_CONTEXT_FILTER;
+    }
+}

+ 51 - 0
core/src/main/java/org/springframework/security/context/SecurityContextRepository.java

@@ -0,0 +1,51 @@
+package org.springframework.security.context;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Strategy used for persisting a {@link SecurityContext} between requests.
+ * <p>
+ * Used by {@link SecurityContextPersistenceFilter} to obtain the context which should be used for the current thread
+ * of execution and to store the context once it has been removed from thread-local storage and the request has
+ * completed.
+ * <p>
+ * The persistence mechanism used will depend on the implementation, but most commonly the <tt>HttpSession</tt> will
+ * be used to store the context.
+ *
+ * @author Luke Taylor
+ * @version $Id$
+ * @since 2.5
+ *
+ * @see SecurityContextPersistenceFilter
+ * @see HttpSessionSecurityContextRepository
+ * @see SaveContextOnUpdateOrErrorResponseWrapper
+ */
+public interface SecurityContextRepository {
+
+    /**
+     * Obtains the security context for the supplied request. For an unauthenticated user, an empty context
+     * implementation should be returned. This method should not return null.
+     * <p>
+     * The use of the <tt>HttpRequestResponseHolder</tt> parameter allows implementations to return wrapped versions of
+     * the request or response (or both), allowing them to access implementation-specific state for the request.
+     * The values obtained from the holder will be passed on to the filter chain and also to the <tt>saveContext</tt>
+     * method when it is finally called. Implementations may wish to return a subclass of
+     * {@link SaveContextOnUpdateOrErrorResponseWrapper} as the response object, which guarantees that the context is
+     * persisted when an error or redirect occurs.
+     *
+     * @param requestResponseHolder holder for the current request and response for which the context should be loaded.
+     *
+     * @return The security context which should be used for the current request, never null.
+     */
+    SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder);
+
+    /**
+     * Stores the security context on completion of a request.
+     *
+     * @param context the non-null context which was obtained f
+     * @param request
+     * @param response
+     */
+    void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response);
+}

+ 2 - 1
core/src/main/java/org/springframework/security/ui/FilterChainOrder.java

@@ -22,7 +22,8 @@ public abstract class FilterChainOrder {
 
     public static final int CHANNEL_FILTER              = FILTER_CHAIN_FIRST;
     public static final int CONCURRENT_SESSION_FILTER   = FILTER_CHAIN_FIRST + INTERVAL * i++;
-    public static final int HTTP_SESSION_CONTEXT_FILTER = FILTER_CHAIN_FIRST + INTERVAL * i++;
+    public static final int SECURITY_CONTEXT_FILTER     = FILTER_CHAIN_FIRST + INTERVAL * i++;
+    public static final int HTTP_SESSION_CONTEXT_FILTER = SECURITY_CONTEXT_FILTER;
     public static final int LOGOUT_FILTER               = FILTER_CHAIN_FIRST + INTERVAL * i++;
     public static final int X509_FILTER                 = FILTER_CHAIN_FIRST + INTERVAL * i++;
     public static final int PRE_AUTH_FILTER             = FILTER_CHAIN_FIRST + INTERVAL * i++;

+ 182 - 0
core/src/test/java/org/springframework/security/context/HttpSessionSecurityContextRepositoryTests.java

@@ -0,0 +1,182 @@
+package org.springframework.security.context;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.Authentication;
+import org.springframework.security.providers.TestingAuthenticationToken;
+
+public class HttpSessionSecurityContextRepositoryTests {
+    private final TestingAuthenticationToken testToken = new TestingAuthenticationToken("someone", "passwd", "ROLE_A");
+
+    @Test(expected=IllegalArgumentException.class)
+    public void detectsInvalidContextClass() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        repo.setSecurityContextClass(String.class);
+    }
+
+    @Test(expected=IllegalArgumentException.class)
+    public void cannotSetNullContextClass() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        repo.setSecurityContextClass(null);
+    }
+
+    @Test
+    public void sessionIsntCreatedIfContextDoesntChange() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContext context = repo.loadContext(holder);
+        assertNull(request.getSession(false));
+        repo.saveContext(context, holder.getRequest(), holder.getResponse());
+        assertNull(request.getSession(false));
+    }
+
+    @Test
+    public void sessionIsntCreatedIfAllowSessionCreationIsFalse() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        repo.setAllowSessionCreation(false);
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContext context = repo.loadContext(holder);
+        // Change context
+        context.setAuthentication(testToken);
+        repo.saveContext(context, holder.getRequest(), holder.getResponse());
+        assertNull(request.getSession(false));
+    }
+
+    @Test
+    public void existingContextIsSuccessFullyLoadedFromSessionAndSavedBack() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        SecurityContextHolder.getContext().setAuthentication(testToken);
+        request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContext context = repo.loadContext(holder);
+        assertNotNull(context);
+        assertEquals(testToken, context.getAuthentication());
+        // Won't actually be saved as it hasn't changed, but go through the use case anyway
+        repo.saveContext(context, holder.getRequest(), holder.getResponse());
+        assertEquals(context, request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY));
+    }
+
+    @Test
+    public void nonSecurityContextInSessionIsIgnored() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        SecurityContextHolder.getContext().setAuthentication(testToken);
+        request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, "NotASecurityContextInstance");
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContext context = repo.loadContext(holder);
+        assertNotNull(context);
+        assertNull(context.getAuthentication());
+    }
+
+    @Test
+    public void sessionIsCreatedAndContextStoredWhenContextChanges() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContext context = repo.loadContext(holder);
+        assertNull(request.getSession(false));
+        // Simulate authentication during the request
+        context.setAuthentication(testToken);
+        repo.saveContext(context, holder.getRequest(), holder.getResponse());
+        assertNotNull(request.getSession(false));
+        assertEquals(context, request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY));
+    }
+
+    @Test
+    public void redirectCausesEarlySaveOfContext() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContextHolder.setContext(repo.loadContext(holder));
+        SecurityContextHolder.getContext().setAuthentication(testToken);
+        holder.getResponse().sendRedirect("/doesntmatter");
+        assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY));
+        assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved());
+        repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+        // Check it's still the same
+        assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY));
+    }
+
+    @Test
+    public void sendErrorCausesEarlySaveOfContext() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContextHolder.setContext(repo.loadContext(holder));
+        SecurityContextHolder.getContext().setAuthentication(testToken);
+        holder.getResponse().sendError(404);
+        assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY));
+        assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved());
+        repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+        // Check it's still the same
+        assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY));
+    }
+
+    @Test
+    public void noSessionIsCreatedIfSessionWasInvalidatedDuringTheRequest() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        request.getSession();
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContextHolder.setContext(repo.loadContext(holder));
+        SecurityContextHolder.getContext().setAuthentication(testToken);
+        request.getSession().invalidate();
+        repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+        assertNull(request.getSession(false));
+    }
+
+    @Test
+    public void settingCloneFromContextLoadsClonedContextObject() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        repo.setCloneFromHttpSession(true);
+        MockHttpServletRequest request = new MockHttpServletRequest();
+        MockContext contextBefore = new MockContext();
+        request.getSession().setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, contextBefore);
+        contextBefore.setAuthentication(testToken);
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+        SecurityContext loadedContext = repo.loadContext(holder);
+        assertTrue(loadedContext instanceof MockContext);
+        assertFalse(loadedContext == contextBefore);
+    }
+
+    @Test
+    public void generateNewContextWorksWithContextClass() throws Exception {
+        HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+        repo.setSecurityContextClass(MockContext.class);
+        assertTrue(repo.generateNewContext() instanceof MockContext);
+    }
+
+    static class MockContext implements Cloneable, SecurityContext {
+        Authentication a;
+
+        public Authentication getAuthentication() {
+            return a;
+        }
+
+        public void setAuthentication(Authentication authentication) {
+            a = authentication;
+        }
+
+        public Object clone() {
+            MockContext mc = new MockContext();
+            mc.setAuthentication(this.getAuthentication());
+            return mc;
+        }
+    }
+
+}

+ 138 - 0
core/src/test/java/org/springframework/security/context/SecurityContextPersistenceFilterTests.java

@@ -0,0 +1,138 @@
+package org.springframework.security.context;
+
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+
+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 org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.integration.junit4.JUnit4Mockery;
+import org.junit.After;
+import org.junit.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.security.providers.TestingAuthenticationToken;
+import org.springframework.security.ui.FilterChainOrder;
+
+public class SecurityContextPersistenceFilterTests {
+    Mockery jmock = new JUnit4Mockery();
+    TestingAuthenticationToken testToken = new TestingAuthenticationToken("someone", "passwd", "ROLE_A");
+
+    @After
+    public void clearContext() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    public void contextIsClearedAfterChainProceeds() throws Exception {
+        final FilterChain chain = jmock.mock(FilterChain.class);
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        final MockHttpServletResponse response = new MockHttpServletResponse();
+        SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter();
+        SecurityContextHolder.getContext().setAuthentication(testToken);
+        jmock.checking(new Expectations() {{
+            oneOf(chain).doFilter(with(aNonNull(HttpServletRequest.class)), with(aNonNull(HttpServletResponse.class)));
+        }});
+
+        filter.doFilter(request, response, chain);
+        assertNull(SecurityContextHolder.getContext().getAuthentication());
+    }
+
+    @Test
+    public void contextIsStillClearedIfExceptionIsThrowByFilterChain() throws Exception {
+        final FilterChain chain = jmock.mock(FilterChain.class);
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        final MockHttpServletResponse response = new MockHttpServletResponse();
+        SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter();
+        SecurityContextHolder.getContext().setAuthentication(testToken);
+
+        jmock.checking(new Expectations() {{
+            oneOf(chain).doFilter(with(aNonNull(HttpServletRequest.class)), with(aNonNull(HttpServletResponse.class)));
+            will(throwException(new IOException()));
+        }});
+        try {
+            filter.doFilter(request, response, chain);
+            fail();
+        } catch(IOException expected) {
+        }
+
+        assertNull(SecurityContextHolder.getContext().getAuthentication());
+    }
+
+    @Test
+    public void loadedContextContextIsCopiedToSecurityContextHolderAndUpdatedContextIsStored() throws Exception {
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        final MockHttpServletResponse response = new MockHttpServletResponse();
+        SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter();
+        final TestingAuthenticationToken beforeAuth = new TestingAuthenticationToken("someoneelse", "passwd", "ROLE_B");
+        final SecurityContext scBefore = new SecurityContextImpl();
+        final SecurityContext scExpectedAfter = new SecurityContextImpl();
+        scExpectedAfter.setAuthentication(testToken);
+        scBefore.setAuthentication(beforeAuth);
+        final SecurityContextRepository repo = jmock.mock(SecurityContextRepository.class);
+        filter.setSecurityContextRepository(repo);
+
+        jmock.checking(new Expectations() {{
+            oneOf(repo).loadContext(with(aNonNull(HttpRequestResponseHolder.class))); will(returnValue(scBefore));
+            oneOf(repo).saveContext(scExpectedAfter, request, response);
+        }});
+
+        final FilterChain chain = new FilterChain() {
+            public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
+                assertEquals(beforeAuth, SecurityContextHolder.getContext().getAuthentication());
+                // Change the context here
+                SecurityContextHolder.setContext(scExpectedAfter);
+            }
+        };
+
+        filter.doFilter(request, response, chain);
+
+        jmock.assertIsSatisfied();
+    }
+
+    @Test
+    public void filterIsOnlyAppliedOncePerRequest() throws Exception {
+        final FilterChain chain = jmock.mock(FilterChain.class);
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        final MockHttpServletResponse response = new MockHttpServletResponse();
+        SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter();
+        final SecurityContextRepository repo = jmock.mock(SecurityContextRepository.class);
+        filter.setSecurityContextRepository(repo);
+        final SecurityContext sc = SecurityContextHolder.getContext();
+
+        jmock.checking(new Expectations() {{
+            oneOf(repo).loadContext(with(aNonNull(HttpRequestResponseHolder.class))); will(returnValue(sc));
+            oneOf(repo).saveContext(sc, request, response);
+            exactly(2).of(chain).doFilter(request, response);
+        }});
+
+        filter.doFilter(request, response, chain);
+        assertNotNull(request.getAttribute(SecurityContextPersistenceFilter.FILTER_APPLIED));
+        filter.doFilter(request, response, chain);
+        jmock.assertIsSatisfied();
+    }
+
+    @Test
+    public void sessionIsEagerlyCreatedWhenConfigured() throws Exception {
+        final FilterChain chain = jmock.mock(FilterChain.class);
+        jmock.checking(new Expectations() {{ ignoring(chain); }});
+        final MockHttpServletRequest request = new MockHttpServletRequest();
+        final MockHttpServletResponse response = new MockHttpServletResponse();
+        SecurityContextPersistenceFilter filter = new SecurityContextPersistenceFilter();
+        filter.setForceEagerSessionCreation(true);
+        filter.doFilter(request, response, chain);
+        assertNotNull(request.getSession(false));
+    }
+
+    @Test
+    public void filterOrderHasExpectedValue() throws Exception {
+        assertEquals(FilterChainOrder.SECURITY_CONTEXT_FILTER, (new SecurityContextPersistenceFilter()).getOrder());
+    }
+}