diff --git a/core/src/main/java/org/springframework/security/context/HttpRequestResponseHolder.java b/core/src/main/java/org/springframework/security/context/HttpRequestResponseHolder.java new file mode 100644 index 0000000000..40ca0d3a09 --- /dev/null +++ b/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 SecurityContext + * 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; + } +} diff --git a/core/src/main/java/org/springframework/security/context/HttpSessionSecurityContextRepository.java b/core/src/main/java/org/springframework/security/context/HttpSessionSecurityContextRepository.java new file mode 100644 index 0000000000..aae4ec177d --- /dev/null +++ b/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 SecurityContextRepository implementation which stores the security context in the HttpSession between + * requests. + *

+ * The HttpSession will be queried to retrieve the SecurityContext in the loadContext + * method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY}). If a valid SecurityContext cannot be + * obtained from the HttpSession for whatever reason, a fresh SecurityContext 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. + *

+ * When saveContext is called, the context will be stored under the same key, provided + *

    + *
  1. The value has changed
  2. + *
  3. The configured AuthenticationTrustResolver does not report that the contents represent an anonymous + * user
  4. + *
+ *

+ * With the standard configuration, no HttpSession will be created during loadContext if one does + * not already exist. When saveContext is called at the end of the web request, and no session exists, a new + * HttpSession will only be created if the supplied SecurityContext is not equal + * to a new instance of the {@link #setContextClass(Class) contextClass} (or an empty + * SecurityContextImpl if the class has not been set. This avoids needless HttpSession 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 allowSessionCreation property of this class is set to true (the default). + *

+ * If for whatever reason no HttpSession should ever be created (e.g. Basic authentication is being + * used or similar clients that will never present the same jsessionid etc), then + * {@link #setAllowSessionCreation(boolean) allowSessionCreation} should be set to false. + * Only do this if you really need to conserve server memory and ensure all classes using the + * SecurityContextHolder are designed to have no persistence of the SecurityContext + * 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 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. + *

+ * 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. + *

+ * If cloneFromHttpSession 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 SecurityContext implementation is in use (i.e. the securityContextClass 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 HttpSession with + * the SecurityContext when a sendError() or sendRedirect + * happens. See SEC-398. + *

+ * 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 saveContext() 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 + "'"); + } + } + } + } + } +} diff --git a/core/src/main/java/org/springframework/security/context/SaveContextOnUpdateOrErrorResponseWrapper.java b/core/src/main/java/org/springframework/security/context/SaveContextOnUpdateOrErrorResponseWrapper.java new file mode 100644 index 0000000000..1250ca281b --- /dev/null +++ b/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 SecurityContext when a sendError() or sendRedirect + * happens. See SEC-398. + *

+ * 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 SecurityContext instance to store + */ + abstract void saveContext(SecurityContext context); + + /** + * Makes sure the session is updated before calling the + * superclass sendError() + */ + public void sendError(int sc) throws IOException { + doSaveContext(); + super.sendError(sc); + } + + /** + * Makes sure the session is updated before calling the + * superclass sendError() + */ + public void sendError(int sc, String msg) throws IOException { + doSaveContext(); + super.sendError(sc, msg); + } + + /** + * Makes sure the context is stored before calling the + * superclass sendRedirect() + */ + public void sendRedirect(String location) throws IOException { + doSaveContext(); + super.sendRedirect(location); + } + + /** + * Calls saveContext() with the current contents of the SecurityContextHolder. + */ + private void doSaveContext() { + saveContext(SecurityContextHolder.getContext()); + contextSaved = true; + } + + /** + * Tells if the response wrapper has called saveContext() because of an error or redirect. + */ + public boolean isContextSaved() { + return contextSaved; + } + +} diff --git a/core/src/main/java/org/springframework/security/context/SecurityContextHolder.java b/core/src/main/java/org/springframework/security/context/SecurityContextHolder.java index f8e3580324..0248749a89 100644 --- a/core/src/main/java/org/springframework/security/context/SecurityContextHolder.java +++ b/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.

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. + *

+ * 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 static to facilitate ease of use in - * calling code.

- *

To specify which strategy should be used, you must provide a mode setting. A mode setting is one of the + * calling code. + *

+ * To specify which strategy should be used, you must provide a mode setting. A mode setting is one of the * three valid MODE_ settings defined as static final fields, or a fully qualified classname - * to a concrete implementation of {@link org.springframework.security.context.SecurityContextHolderStrategy} that provides a - * public no-argument constructor.

- *

There are two ways to specify the desired strategy mode String. 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. + *

+ * There are two ways to specify the desired strategy mode String. 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).

+ * #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 * SecurityContextHolderStrategy. * * @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 NOT 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; diff --git a/core/src/main/java/org/springframework/security/context/SecurityContextPersistenceFilter.java b/core/src/main/java/org/springframework/security/context/SecurityContextPersistenceFilter.java new file mode 100644 index 0000000000..da20fbef6e --- /dev/null +++ b/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 HttpSession related configuration options. + *

+ * This filter will only execute once per request, to resolve servlet container (specifically Weblogic) + * incompatibilities. + *

+ * This filter MUST be executed BEFORE any authentication processing mechanisms. Authentication processing mechanisms + * (e.g. BASIC, CAS processing filters etc) expect the SecurityContextHolder to contain a valid + * SecurityContext by the time they execute. + *

+ * This is essentially a refactoring of the old HttpSessionContextIntegrationFilter to delegate + * the storage issues to a separate strategy, allowing for more customization in the way the security context is + * maintained between requests. + *

+ * The forceEagerSessionCreation property can be used to ensure that a session is always available before + * the filter chain executes (the default is false, 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; + } +} diff --git a/core/src/main/java/org/springframework/security/context/SecurityContextRepository.java b/core/src/main/java/org/springframework/security/context/SecurityContextRepository.java new file mode 100644 index 0000000000..613746cec1 --- /dev/null +++ b/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. + *

+ * 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. + *

+ * The persistence mechanism used will depend on the implementation, but most commonly the HttpSession 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. + *

+ * The use of the HttpRequestResponseHolder 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 saveContext + * 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); +} diff --git a/core/src/main/java/org/springframework/security/ui/FilterChainOrder.java b/core/src/main/java/org/springframework/security/ui/FilterChainOrder.java index beeb2533f3..c1fab4c66a 100644 --- a/core/src/main/java/org/springframework/security/ui/FilterChainOrder.java +++ b/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++; diff --git a/core/src/test/java/org/springframework/security/context/HttpSessionSecurityContextRepositoryTests.java b/core/src/test/java/org/springframework/security/context/HttpSessionSecurityContextRepositoryTests.java new file mode 100644 index 0000000000..ee5769d3ab --- /dev/null +++ b/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; + } + } + +} diff --git a/core/src/test/java/org/springframework/security/context/SecurityContextPersistenceFilterTests.java b/core/src/test/java/org/springframework/security/context/SecurityContextPersistenceFilterTests.java new file mode 100644 index 0000000000..9931905b5e --- /dev/null +++ b/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()); + } +}