SEC-1307: Modify context saving logic in HttpSessionSecurityContextRepository to check the SecurityContext and its contents (the Authentication) against the respective values when the request first arrived at the SecurityContextPersistenceFilter. As explained in the issue, this allows a definite decision to be made about whether the current thread has modified the context information during the request, indicating that it should be saved.

Also removed deprecated HttpSessionContextIntegrationFilter and tests.
This commit is contained in:
Luke Taylor 2010-02-25 21:51:46 +00:00
parent 5aae545949
commit e2f9be9015
3 changed files with 22 additions and 521 deletions

View File

@ -1,194 +0,0 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.context;
import javax.servlet.ServletException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
/**
* Populates the {@link SecurityContextHolder} with information obtained from
* the <code>HttpSession</code>.
* <p/>
* <p/>
* The <code>HttpSession</code> will be queried to retrieve the
* <code>SecurityContext</code> that should be stored against the
* <code>SecurityContextHolder</code> for the duration of the web request. At
* the end of the web request, any updates made to the
* <code>SecurityContextHolder</code> will be persisted back to the
* <code>HttpSession</code> by this filter.
* </p>
* <p/>
* 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 used instead. The created
* object will be of the instance defined by the {@link #setContextClass(Class)}
* method (which defaults to {@link org.springframework.security.core.context.SecurityContextImpl}.
* </p>
* <p/>
* No <code>HttpSession</code> will be created by this filter if one does not
* already exist. If at the end of the web request the <code>HttpSession</code>
* does not exist, a <code>HttpSession</code> will <b>only</b> be created if
* the current contents of the <code>SecurityContextHolder</code> are not
* {@link java.lang.Object#equals(java.lang.Object)} to a <code>new</code>
* instance of {@link #setContextClass(Class)}. This avoids needless
* <code>HttpSession</code> creation, but automates the storage of changes
* made to the <code>SecurityContextHolder</code>. There is one exception to
* this rule, that is if the {@link #forceEagerSessionCreation} property is
* <code>true</code>, in which case sessions will always be created
* irrespective of normal session-minimisation logic (the default is
* <code>false</code>, as this is resource intensive and not recommended).
* </p>
* <p/>
* This filter will only execute once per request, to resolve servlet container
* (specifically Weblogic) incompatibilities.
* </p>
* <p/>
* If for whatever reason no <code>HttpSession</code> should <b>ever</b> be
* created (eg this filter is only being used with Basic authentication or
* similar clients that will never present the same <code>jsessionid</code>
* etc), the {@link #setAllowSessionCreation(boolean)} 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. Please note that if {@link #forceEagerSessionCreation}
* is <code>true</code>, the <code>allowSessionCreation</code> must also be
* <code>true</code> (setting it to <code>false</code> will cause a startup
* time error).
* </p>
* <p/>
* This filter MUST be executed BEFORE any authentication processing mechanisms.
* Authentication processing mechanisms (eg BASIC, CAS processing filters etc)
* expect the <code>SecurityContextHolder</code> to contain a valid
* <code>SecurityContext</code> by the time they execute.
* </p>
*
* @author Ben Alex
* @author Patrick Burleson
* @author Luke Taylor
* @author Martin Algesten
*
* @deprecated Use SecurityContextPersistenceFilter instead.
*
*/
public class HttpSessionContextIntegrationFilter extends SecurityContextPersistenceFilter implements InitializingBean {
//~ Static fields/initializers =====================================================================================
public static final String SPRING_SECURITY_CONTEXT_KEY = HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY;
//~ Instance fields ================================================================================================
private Class<? extends SecurityContext> contextClass = SecurityContextImpl.class;
/**
* Indicates if this filter can create a <code>HttpSession</code> if
* needed (sessions are always created sparingly, but setting this value to
* <code>false</code> will prohibit sessions from ever being created).
* Defaults to <code>true</code>. Do not set to <code>false</code> if
* you are have set {@link #forceEagerSessionCreation} to <code>true</code>,
* as the properties would be in conflict.
*/
private boolean allowSessionCreation = true;
/**
* Indicates if this filter is required to create a <code>HttpSession</code>
* for every request before proceeding through the filter chain, even if the
* <code>HttpSession</code> would not ordinarily have been created. By
* default this is <code>false</code>, which is entirely appropriate for
* most circumstances as you do not want a <code>HttpSession</code>
* created unless the filter actually needs one. It is envisaged the main
* situation in which this property would be set to <code>true</code> is
* if using other filters that depend on a <code>HttpSession</code>
* already existing, such as those which need to obtain a session ID. This
* is only required in specialised cases, so leave it set to
* <code>false</code> unless you have an actual requirement and are
* conscious of the session creation overhead.
*/
private boolean forceEagerSessionCreation = false;
/**
* Indicates whether the <code>SecurityContext</code> will be cloned from
* the <code>HttpSession</code>. The default is to simply reference (ie
* the default is <code>false</code>). The default may cause issues if
* concurrent threads need to have a different security identity from other
* threads being concurrently processed that share the same
* <code>HttpSession</code>. In most normal environments this does not
* represent an issue, as changes to the security identity in one thread is
* allowed to affect the security identitiy in other threads associated with
* the same <code>HttpSession</code>. For unusual cases where this is not
* permitted, change this value to <code>true</code> and ensure the
* {@link #contextClass} is set to a <code>SecurityContext</code> that
* implements {@link Cloneable} and overrides the <code>clone()</code>
* method.
*/
private boolean cloneFromHttpSession = false;
// private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
private HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
public HttpSessionContextIntegrationFilter() throws ServletException {
super.setSecurityContextRepository(repo);
}
public boolean isCloneFromHttpSession() {
return cloneFromHttpSession;
}
public void setCloneFromHttpSession(boolean cloneFromHttpSession) {
this.cloneFromHttpSession = cloneFromHttpSession;
repo.setCloneFromHttpSession(cloneFromHttpSession);
}
public boolean isAllowSessionCreation() {
return allowSessionCreation;
}
public void setAllowSessionCreation(boolean allowSessionCreation) {
this.allowSessionCreation = allowSessionCreation;
repo.setAllowSessionCreation(allowSessionCreation);
}
protected Class<? extends SecurityContext> getContextClass() {
return contextClass;
}
@SuppressWarnings("unchecked")
public void setContextClass(Class secureContext) {
this.contextClass = secureContext;
repo.setSecurityContextClass(secureContext);
}
public boolean isForceEagerSessionCreation() {
return forceEagerSessionCreation;
}
public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
this.forceEagerSessionCreation = forceEagerSessionCreation;
super.setForceEagerSessionCreation(forceEagerSessionCreation);
}
//~ Methods ========================================================================================================
public void afterPropertiesSet() {
if (forceEagerSessionCreation && !allowSessionCreation) {
throw new IllegalArgumentException(
"If using forceEagerSessionCreation, you must set allowSessionCreation to also be true");
}
}
}

View File

@ -10,6 +10,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
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;
@ -91,8 +92,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
}
requestResponseHolder.setResponse(new SaveToSessionResponseWrapper(response, request,
httpSession != null, context.hashCode()));
requestResponseHolder.setResponse(
new SaveToSessionResponseWrapper(response, request, httpSession != null, context));
return context;
}
@ -293,7 +294,8 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
private HttpServletRequest request;
private boolean httpSessionExistedAtStartOfRequest;
private int contextHashBeforeChainExecution;
private SecurityContext contextBeforeExecution;
private Authentication authBeforeExecution;
/**
* Takes the parameters required to call <code>saveContext()</code> successfully in
@ -303,17 +305,17 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
* @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.
* @param context the context before the filter chain executed.
* The context will only be stored if it or its contents changed during the request.
*/
SaveToSessionResponseWrapper(HttpServletResponse response, HttpServletRequest request,
boolean httpSessionExistedAtStartOfRequest,
int contextHashBeforeChainExecution) {
SecurityContext context) {
super(response, disableUrlRewriting);
this.request = request;
this.httpSessionExistedAtStartOfRequest = httpSessionExistedAtStartOfRequest;
this.contextHashBeforeChainExecution = contextHashBeforeChainExecution;
this.contextBeforeExecution = context;
this.authBeforeExecution = context.getAuthentication();
}
/**
@ -331,7 +333,7 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
// See SEC-776
if (authenticationTrustResolver.isAnonymous(context.getAuthentication())) {
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext contents are anonymous - context will not be stored in HttpSession. ");
logger.debug("SecurityContext contents are anonymous - context will not be stored in HttpSession.");
}
return;
}
@ -343,12 +345,18 @@ public class HttpSessionSecurityContextRepository implements SecurityContextRepo
}
// 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) {
httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
// the SecurityContext has actually changed in this thread (see JIRA SEC-37, SEC-1307)
if (httpSession != null) {
SecurityContext contextFromSession = (SecurityContext) httpSession.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext stored to HttpSession: '" + context + "'");
if (context != contextFromSession) {
if (context != contextBeforeExecution || context.getAuthentication() != authBeforeExecution) {
httpSession.setAttribute(SPRING_SECURITY_CONTEXT_KEY, context);
if (logger.isDebugEnabled()) {
logger.debug("SecurityContext stored to HttpSession: '" + context + "'");
}
}
}
}
}

View File

@ -1,313 +0,0 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.web.context;
import static org.junit.Assert.*;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import org.junit.Test;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.MockFilterConfig;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextImpl;
/**
* Tests {@link HttpSessionContextIntegrationFilter}.
*
* @author Ben Alex
*/
@SuppressWarnings("deprecation")
public class HttpSessionContextIntegrationFilterTests {
// Build an Authentication object we simulate came from HttpSession
private UsernamePasswordAuthenticationToken sessionPrincipal = new UsernamePasswordAuthenticationToken(
"someone",
"password",
AuthorityUtils.createAuthorityList("SOME_ROLE"));
//~ Methods ========================================================================================================
private static void executeFilterInContainerSimulator(
FilterConfig filterConfig, Filter filter, ServletRequest request,
ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
// filter.init(filterConfig);
filter.doFilter(request, response, filterChain);
// filter.destroy();
}
@Test
public void testDetectsIncompatibleSessionProperties() throws Exception {
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
try {
filter.setAllowSessionCreation(false);
filter.setForceEagerSessionCreation(true);
filter.afterPropertiesSet();
fail("Shown have thrown IllegalArgumentException");
} catch (IllegalArgumentException expected) {
assertTrue(true);
}
filter.setAllowSessionCreation(true);
filter.afterPropertiesSet();
assertTrue(true);
}
@Test
public void testDetectsMissingOrInvalidContext() throws Exception {
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
try {
filter.setContextClass(null);
filter.afterPropertiesSet();
fail("Shown have thrown IllegalArgumentException");
} catch (IllegalArgumentException expected) {
assertTrue(true);
}
try {
filter.setContextClass(Integer.class);
assertEquals(Integer.class, filter.getContextClass());
filter.afterPropertiesSet();
fail("Shown have thrown IllegalArgumentException");
} catch (IllegalArgumentException expected) {
assertTrue(true);
}
}
@Test
public void testExceptionWithinFilterChainStillClearsSecurityContextHolder() throws Exception {
// Build a Context to store in HttpSession (simulating prior request)
SecurityContext sc = new SecurityContextImpl();
sc.setAuthentication(sessionPrincipal);
// Build a mock request
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession().setAttribute(
HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY,
sc);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = new MockFilterChain(sessionPrincipal, null,
new IOException());
// Prepare filter
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
filter.setContextClass(SecurityContextImpl.class);
filter.afterPropertiesSet();
// Execute filter
try {
executeFilterInContainerSimulator(new MockFilterConfig(), filter,
request, response, chain);
fail("We should have received the IOException thrown inside the filter chain here");
} catch (IOException ioe) {
assertTrue(true);
}
// Check the SecurityContextHolder is null, even though an exception was
// thrown during chain
assertEquals(new SecurityContextImpl(), SecurityContextHolder.getContext());
assertNull("Should have cleared FILTER_APPLIED",
request.getAttribute(HttpSessionContextIntegrationFilter.FILTER_APPLIED));
}
@Test
public void testExistingContextContentsCopiedIntoContextHolderFromSessionAndChangesToContextCopiedBackToSession()
throws Exception {
// Build an Authentication object we simulate came from HttpSession
UsernamePasswordAuthenticationToken updatedPrincipal = new UsernamePasswordAuthenticationToken(
"someone",
"password",
AuthorityUtils.createAuthorityList("SOME_DIFFERENT_ROLE"));
// Build a Context to store in HttpSession (simulating prior request)
SecurityContext sc = new SecurityContextImpl();
sc.setAuthentication(sessionPrincipal);
// Build a mock request
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession().setAttribute(
HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY,
sc);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = new MockFilterChain(sessionPrincipal,
updatedPrincipal, null);
// Prepare filter
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
filter.setContextClass(SecurityContextImpl.class);
filter.afterPropertiesSet();
// Execute filter
executeFilterInContainerSimulator(new MockFilterConfig(), filter,
request, response, chain);
// Obtain new/update Authentication from HttpSession
SecurityContext context = (SecurityContext) request.getSession().getAttribute(
HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY);
assertEquals(updatedPrincipal, ((SecurityContext) context).getAuthentication());
}
@Test
public void testHttpSessionCreatedWhenContextHolderChanges() throws Exception {
// Build an Authentication object we simulate our Authentication changed it to
UsernamePasswordAuthenticationToken updatedPrincipal = new UsernamePasswordAuthenticationToken(
"someone",
"password",
AuthorityUtils.createAuthorityList("SOME_ROLE"));
// Build a mock request
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = new MockFilterChain(null, updatedPrincipal, null);
// Prepare filter
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
filter.setContextClass(SecurityContextImpl.class);
// don't call afterPropertiesSet to test case when Spring filter.afterPropertiesSet(); isn't called
// Execute filter
executeFilterInContainerSimulator(new MockFilterConfig(), filter, request, response, chain);
// Obtain new/updated Authentication from HttpSession
SecurityContext context = (SecurityContext) request.getSession(false).getAttribute(
HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY);
assertEquals(updatedPrincipal, ((SecurityContext) context).getAuthentication());
}
@Test
public void testHttpSessionEagerlyCreatedWhenDirected() throws Exception {
// Build a mock request
MockHttpServletRequest request = new MockHttpServletRequest(null, null);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = new MockFilterChain(null, null, null);
// Prepare filter
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
filter.setContextClass(SecurityContextImpl.class);
filter.setForceEagerSessionCreation(true); // non-default
filter.afterPropertiesSet();
// Execute filter
executeFilterInContainerSimulator(new MockFilterConfig(), filter,
request, response, chain);
// Check the session is not null
assertNotNull(request.getSession(false));
}
@Test
public void testHttpSessionNotCreatedUnlessContextHolderChanges() throws Exception {
// Build a mock request
MockHttpServletRequest request = new MockHttpServletRequest(null, null);
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = new MockFilterChain(null, null, null);
// Prepare filter
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
filter.setContextClass(SecurityContextImpl.class);
filter.afterPropertiesSet();
// Execute filter
executeFilterInContainerSimulator(new MockFilterConfig(), filter,
request, response, chain);
// Check the session is null
assertNull(request.getSession(false));
}
@Test
public void testHttpSessionWithNonContextInWellKnownLocationIsOverwritten() throws Exception {
// Build an Authentication object we simulate our Authentication changed it to
UsernamePasswordAuthenticationToken updatedPrincipal = new UsernamePasswordAuthenticationToken(
"someone",
"password",
AuthorityUtils.createAuthorityList("SOME_DIFFERENT_ROLE"));
// Build a mock request
MockHttpServletRequest request = new MockHttpServletRequest();
request.getSession().setAttribute(
HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY,
"NOT_A_CONTEXT_OBJECT");
MockHttpServletResponse response = new MockHttpServletResponse();
FilterChain chain = new MockFilterChain(null, updatedPrincipal, null);
// Prepare filter
HttpSessionContextIntegrationFilter filter = new HttpSessionContextIntegrationFilter();
filter.setContextClass(SecurityContextImpl.class);
filter.afterPropertiesSet();
// Execute filter
executeFilterInContainerSimulator(new MockFilterConfig(), filter, request, response, chain);
// Obtain new/update Authentication from HttpSession
SecurityContext context = (SecurityContext) request.getSession().getAttribute(
HttpSessionContextIntegrationFilter.SPRING_SECURITY_CONTEXT_KEY);
assertEquals(updatedPrincipal, ((SecurityContext) context).getAuthentication());
}
//~ Inner Classes ==================================================================================================
private class MockFilterChain implements FilterChain {
private Authentication changeContextHolder;
private Authentication expectedOnContextHolder;
private IOException toThrowDuringChain;
public MockFilterChain(Authentication expectedOnContextHolder,
Authentication changeContextHolder,
IOException toThrowDuringChain) {
this.expectedOnContextHolder = expectedOnContextHolder;
this.changeContextHolder = changeContextHolder;
this.toThrowDuringChain = toThrowDuringChain;
}
public void doFilter(ServletRequest arg0, ServletResponse arg1) throws IOException, ServletException {
if (expectedOnContextHolder != null) {
assertEquals(expectedOnContextHolder, SecurityContextHolder.getContext().getAuthentication());
}
if (changeContextHolder != null) {
SecurityContext sc = SecurityContextHolder.getContext();
sc.setAuthentication(changeContextHolder);
SecurityContextHolder.setContext(sc);
}
if (toThrowDuringChain != null) {
throw toThrowDuringChain;
}
}
}
}