SEC-1695: Allow customization of the session key under which the SecurityContext is stored.

This commit is contained in:
Luke Taylor 2011-05-25 19:51:47 +01:00
parent 42e0e158b4
commit 6d04670f87
8 changed files with 108 additions and 65 deletions

View File

@ -17,9 +17,7 @@ package org.springframework.security.authentication.jaas;
import java.io.IOException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
import java.util.*;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
@ -54,7 +52,7 @@ import org.springframework.util.ObjectUtils;
* <p>This implementation is backed by a <a
* href="http://java.sun.com/j2se/1.5.0/docs/guide/security/jaas/JAASRefGuide.html">JAAS</a> configuration that is provided by
* a subclass's implementation of {@link #createLoginContext(CallbackHandler)}.
*
*
* <p>When using JAAS login modules as the authentication source, sometimes the
* <a href="http://java.sun.com/j2se/1.5.0/docs/api/javax/security/auth/login/LoginContext.html">LoginContext</a> will
* require <i>CallbackHandler</i>s. The AbstractJaasAuthenticationProvider uses an internal
@ -91,7 +89,7 @@ import org.springframework.util.ObjectUtils;
* &lt;/list&gt;
* &lt;/property&gt;
* </pre>
*
*
* @author Ray Krueger
* @author Rob Winch
*/
@ -190,7 +188,7 @@ ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDes
/**
* Creates the LoginContext to be used for authentication.
*
*
* @param handler The CallbackHandler that should be used for the LoginContext (never <code>null</code>).
* @return the LoginContext to use for authentication.
* @throws LoginException
@ -198,39 +196,42 @@ ApplicationEventPublisherAware, InitializingBean, ApplicationListener<SessionDes
protected abstract LoginContext createLoginContext(CallbackHandler handler) throws LoginException;
/**
* Handles the logout by getting the SecurityContext for the session that was destroyed. <b>MUST NOT use
* SecurityContextHolder as we are logging out a session that is not related to the current user.</b>
* Handles the logout by getting the security contexts for the destroyed session and invoking
* {@code LoginContext.logout()} for any which contain a {@code JaasAuthenticationToken}.
*
* @param event
*
* @param event the session event which contains the current session
*/
protected void handleLogout(SessionDestroyedEvent event) {
SecurityContext context = event.getSecurityContext();
List<SecurityContext> contexts = event.getSecurityContexts();
if (context == null) {
log.debug("The destroyed session has no SecurityContext");
if (contexts.isEmpty()) {
log.debug("The destroyed session has no SecurityContexts");
return;
}
Authentication auth = context.getAuthentication();
for(SecurityContext context : contexts) {
Authentication auth = context.getAuthentication();
if ((auth != null) && (auth instanceof JaasAuthenticationToken)) {
JaasAuthenticationToken token = (JaasAuthenticationToken) auth;
if ((auth != null) && (auth instanceof JaasAuthenticationToken)) {
JaasAuthenticationToken token = (JaasAuthenticationToken) auth;
try {
LoginContext loginContext = token.getLoginContext();
boolean debug = log.isDebugEnabled();
if (loginContext != null) {
if (debug) {
log.debug("Logging principal: [" + token.getPrincipal() + "] out of LoginContext");
try {
LoginContext loginContext = token.getLoginContext();
boolean debug = log.isDebugEnabled();
if (loginContext != null) {
if (debug) {
log.debug("Logging principal: [" + token.getPrincipal() + "] out of LoginContext");
}
loginContext.logout();
} else if (debug) {
log.debug("Cannot logout principal: [" + token.getPrincipal() + "] from LoginContext. "
+ "The LoginContext is unavailable");
}
loginContext.logout();
} else if (debug) {
log.debug("Cannot logout principal: [" + token.getPrincipal() + "] from LoginContext. "
+ "The LoginContext is unavailable");
} catch (LoginException e) {
log.warn("Error error logging out of LoginContext", e);
}
} catch (LoginException e) {
log.warn("Error error logging out of LoginContext", e);
}
}
}

View File

@ -3,6 +3,8 @@ package org.springframework.security.core.session;
import org.springframework.context.ApplicationEvent;
import org.springframework.security.core.context.SecurityContext;
import java.util.*;
/**
* Generic "session termination" event which indicates that a session (potentially
* represented by a security context) has ended.
@ -17,11 +19,13 @@ public abstract class SessionDestroyedEvent extends ApplicationEvent {
}
/**
* Provides the <tt>SecurityContext</tt> under which the session was running.
* Provides the {@code SecurityContext} instances which were associated with the destroyed session. Usually there
* will be only one security context per session.
*
* @return the <tt>SecurityContext</tt> associated with the session, or null if there is no context.
* @return the {@code SecurityContext} instances which were stored in the current session (an empty list if there
* are none).
*/
public abstract SecurityContext getSecurityContext();
public abstract List<SecurityContext> getSecurityContexts();
/**
* @return the identifier associated with the destroyed session.

View File

@ -27,7 +27,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.util.Collections;
import java.util.*;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
@ -136,13 +136,13 @@ public class DefaultJaasAuthenticationProviderTests {
JaasAuthenticationToken token = mock(JaasAuthenticationToken.class);
LoginContext context = mock(LoginContext.class);
when(event.getSecurityContext()).thenReturn(securityContext);
when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext));
when(securityContext.getAuthentication()).thenReturn(token);
when(token.getLoginContext()).thenReturn(context);
provider.onApplicationEvent(event);
verify(event).getSecurityContext();
verify(event).getSecurityContexts();
verify(securityContext).getAuthentication();
verify(token).getLoginContext();
verify(context).logout();
@ -155,7 +155,7 @@ public class DefaultJaasAuthenticationProviderTests {
provider.handleLogout(event);
verify(event).getSecurityContext();
verify(event).getSecurityContexts();
verify(log).debug(anyString());
verifyNoMoreInteractions(event);
}
@ -165,12 +165,12 @@ public class DefaultJaasAuthenticationProviderTests {
SessionDestroyedEvent event = mock(SessionDestroyedEvent.class);
SecurityContext securityContext = mock(SecurityContext.class);
when(event.getSecurityContext()).thenReturn(securityContext);
when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext));
provider.handleLogout(event);
verify(event).getSecurityContext();
verify(event).getSecurityContext();
verify(event).getSecurityContexts();
verify(event).getSecurityContexts();
verify(securityContext).getAuthentication();
verifyNoMoreInteractions(event, securityContext);
}
@ -180,13 +180,13 @@ public class DefaultJaasAuthenticationProviderTests {
SessionDestroyedEvent event = mock(SessionDestroyedEvent.class);
SecurityContext securityContext = mock(SecurityContext.class);
when(event.getSecurityContext()).thenReturn(securityContext);
when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext));
when(securityContext.getAuthentication()).thenReturn(token);
provider.handleLogout(event);
verify(event).getSecurityContext();
verify(event).getSecurityContext();
verify(event).getSecurityContexts();
verify(event).getSecurityContexts();
verify(securityContext).getAuthentication();
verifyNoMoreInteractions(event, securityContext);
}
@ -197,11 +197,11 @@ public class DefaultJaasAuthenticationProviderTests {
SecurityContext securityContext = mock(SecurityContext.class);
JaasAuthenticationToken token = mock(JaasAuthenticationToken.class);
when(event.getSecurityContext()).thenReturn(securityContext);
when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext));
when(securityContext.getAuthentication()).thenReturn(token);
provider.onApplicationEvent(event);
verify(event).getSecurityContext();
verify(event).getSecurityContexts();
verify(securityContext).getAuthentication();
verify(token).getLoginContext();
@ -216,14 +216,14 @@ public class DefaultJaasAuthenticationProviderTests {
LoginContext context = mock(LoginContext.class);
LoginException loginException = new LoginException("Failed Login");
when(event.getSecurityContext()).thenReturn(securityContext);
when(event.getSecurityContexts()).thenReturn(Arrays.asList(securityContext));
when(securityContext.getAuthentication()).thenReturn(token);
when(token.getLoginContext()).thenReturn(context);
doThrow(loginException).when(context).logout();
provider.onApplicationEvent(event);
verify(event).getSecurityContext();
verify(event).getSecurityContexts();
verify(securityContext).getAuthentication();
verify(token).getLoginContext();
verify(context).logout();

View File

@ -41,6 +41,8 @@ import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
import org.springframework.security.core.session.SessionDestroyedEvent;
@ -244,11 +246,11 @@ public class JaasAuthenticationProviderTests {
JaasAuthenticationToken token = new JaasAuthenticationToken(null, null, loginContext);
SecurityContextImpl context = new SecurityContextImpl();
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(token);
SessionDestroyedEvent event = mock(SessionDestroyedEvent.class);
when(event.getSecurityContext()).thenReturn(context);
when(event.getSecurityContexts()).thenReturn(Arrays.asList(context));
jaasProvider.handleLogout(event);

View File

@ -58,7 +58,7 @@ public class SessionRegistryImplTests {
}
@Override
public SecurityContext getSecurityContext() {
public List<SecurityContext> getSecurityContexts() {
return null;
}
});

View File

@ -8,6 +8,7 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import javax.servlet.http.HttpServletRequest;
@ -19,7 +20,7 @@ import javax.servlet.http.HttpSession;
* between requests.
* <p>
* The {@code HttpSession} will be queried to retrieve the {@code SecurityContext} in the <tt>loadContext</tt>
* method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY}). If a valid {@code SecurityContext} cannot be
* method (using the key {@link #SPRING_SECURITY_CONTEXT_KEY} by default). If a valid {@code SecurityContext} cannot be
* obtained from the {@code HttpSession} for whatever reason, a fresh {@code SecurityContext} will be created
* by calling by {@link SecurityContextHolder#createEmptyContext()} and this instance will be returned instead.
* <p>
@ -50,6 +51,9 @@ import javax.servlet.http.HttpSession;
* @since 3.0
*/
public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
/**
* The default key under which the security context will be stored in the session.
*/
public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
protected final Log logger = LogFactory.getLog(this.getClass());
@ -59,6 +63,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
private final Object contextObject = SecurityContextHolder.createEmptyContext();
private boolean allowSessionCreation = true;
private boolean disableUrlRewriting = false;
private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;
private final AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
@ -108,7 +113,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
return false;
}
return session.getAttribute(SPRING_SECURITY_CONTEXT_KEY) != null;
return session.getAttribute(springSecurityContextKey) != null;
}
/**
@ -128,7 +133,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
// Session exists, so try to obtain a context from it.
Object contextFromSession = httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
if (contextFromSession == null) {
if (debug) {
@ -141,7 +146,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
// 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: '"
logger.warn(springSecurityContextKey + " 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?");
@ -151,7 +156,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
}
if (debug) {
logger.debug("Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: '" + contextFromSession + "'");
logger.debug("Obtained a valid SecurityContext from " + springSecurityContextKey + ": '" + contextFromSession + "'");
}
// Everything OK. The only non-null return from this method.
@ -212,6 +217,17 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
this.disableUrlRewriting = disableUrlRewriting;
}
/**
* Allows the session attribute name to be customized for this repository instance.
*
* @param springSecurityContextKey the key under which the security context will be stored. Defaults to
* {@link #SPRING_SECURITY_CONTEXT_KEY}.
*/
public void setSpringSecurityContextKey(String springSecurityContextKey) {
Assert.hasText(springSecurityContextKey, "springSecurityContextKey cannot be empty");
this.springSecurityContextKey = springSecurityContextKey;
}
//~ Inner Classes ==================================================================================================
/**
@ -273,7 +289,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
if (httpSession != null) {
// SEC-1587 A non-anonymous context may still be in the session
httpSession.removeAttribute(SPRING_SECURITY_CONTEXT_KEY);
httpSession.removeAttribute(springSecurityContextKey);
}
return;
}
@ -286,8 +302,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
// actually changed in this thread (see SEC-37, SEC-1307, SEC-1528)
if (httpSession != null) {
// We may have a new session, so check also whether the context attribute is set SEC-1561
if (contextChanged(context) || httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY) == null) {
httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
if (contextChanged(context) || httpSession.getAttribute(springSecurityContextKey) == null) {
httpSession.setAttribute(springSecurityContextKey, context);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext stored to HttpSession: '" + context + "'");

View File

@ -17,10 +17,12 @@ package org.springframework.security.web.session;
import javax.servlet.http.HttpSession;
import com.sun.xml.internal.ws.encoding.ContentType;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import java.util.*;
/**
* Published by the {@link HttpSessionEventPublisher} when a HttpSession is created in the container
@ -39,9 +41,23 @@ public class HttpSessionDestroyedEvent extends SessionDestroyedEvent {
return (HttpSession) getSource();
}
@SuppressWarnings("unchecked")
@Override
public SecurityContext getSecurityContext() {
return (SecurityContext) ((HttpSession)getSource()).getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
public List<SecurityContext> getSecurityContexts() {
HttpSession session = (HttpSession)getSource();
Enumeration<String> attributes = session.getAttributeNames();
ArrayList<SecurityContext> contexts = new ArrayList<SecurityContext>();
while(attributes.hasMoreElements()) {
Object attribute = attributes.nextElement();
if (attribute instanceof SecurityContext) {
contexts.add((SecurityContext) attribute);
}
}
return contexts;
}
@Override

View File

@ -47,9 +47,10 @@ public class HttpSessionSecurityContextRepositoryTests {
@Test
public void existingContextIsSuccessFullyLoadedFromSessionAndSavedBack() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
MockHttpServletRequest request = new MockHttpServletRequest();
SecurityContextHolder.getContext().setAuthentication(testToken);
request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
request.getSession().setAttribute("imTheContext", SecurityContextHolder.getContext());
MockHttpServletResponse response = new MockHttpServletResponse();
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
SecurityContext context = repo.loadContext(holder);
@ -57,7 +58,7 @@ public class HttpSessionSecurityContextRepositoryTests {
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(SPRING_SECURITY_CONTEXT_KEY));
assertEquals(context, request.getSession().getAttribute("imTheContext"));
}
// SEC-1528
@ -113,33 +114,35 @@ public class HttpSessionSecurityContextRepositoryTests {
@Test
public void redirectCausesEarlySaveOfContext() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
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(SPRING_SECURITY_CONTEXT_KEY));
assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
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(SPRING_SECURITY_CONTEXT_KEY));
assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
}
@Test
public void sendErrorCausesEarlySaveOfContext() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
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(SPRING_SECURITY_CONTEXT_KEY));
assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
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(SPRING_SECURITY_CONTEXT_KEY));
assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
}
@Test
@ -188,15 +191,16 @@ public class HttpSessionSecurityContextRepositoryTests {
@Test
public void contextIsRemovedFromSessionIfCurrentContextIsEmpty() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
MockHttpServletRequest request = new MockHttpServletRequest();
SecurityContext ctxInSession = SecurityContextHolder.createEmptyContext();
ctxInSession.setAuthentication(testToken);
request.getSession().setAttribute(SPRING_SECURITY_CONTEXT_KEY, ctxInSession);
request.getSession().setAttribute("imTheContext", ctxInSession);
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, new MockHttpServletResponse());
repo.loadContext(holder);
// Save an empty context
repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
assertNull(request.getSession().getAttribute(SPRING_SECURITY_CONTEXT_KEY));
assertNull(request.getSession().getAttribute("imTheContext"));
}
@Test