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 + *
+ * 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 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.
+ *
+ * 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
+ * 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
+ * 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 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 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 There are two ways to specify the desired strategy mode
+ * There are two ways to specify the desired strategy mode
+ * 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
+ * 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
+ * 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());
+ }
+}
HttpSession with
+ * the SecurityContext when a sendError() or sendRedirect
+ * happens. See SEC-398.
+ * 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.
+ * 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.static to facilitate ease of use in
- * calling code.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.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.
+ * 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).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.
+ * SecurityContextHolder to contain a valid
+ * SecurityContext by the time they execute.
+ * 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.
+ *