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

This commit is contained in:
Luke Taylor 2008-11-27 20:18:54 +00:00
parent 789be71d8c
commit 4d81d750cd
9 changed files with 928 additions and 13 deletions

View File

@ -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;
}
}

View File

@ -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 + "'");
}
}
}
}
}
}

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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++;

View File

@ -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;
}
}
}

View File

@ -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());
}
}