From e2f9be901541ff7c1f219d878fbd82d6289fd81a Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Thu, 25 Feb 2010 21:51:46 +0000 Subject: [PATCH] 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. --- .../HttpSessionContextIntegrationFilter.java | 194 ----------- .../HttpSessionSecurityContextRepository.java | 38 ++- ...pSessionContextIntegrationFilterTests.java | 313 ------------------ 3 files changed, 23 insertions(+), 522 deletions(-) delete mode 100644 web/src/main/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilter.java delete mode 100644 web/src/test/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilterTests.java diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilter.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilter.java deleted file mode 100644 index 6b807e5b9b..0000000000 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilter.java +++ /dev/null @@ -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 HttpSession. - *

- *

- * The HttpSession will be queried to retrieve the - * SecurityContext that should be stored against the - * SecurityContextHolder for the duration of the web request. At - * the end of the web request, any updates made to the - * SecurityContextHolder will be persisted back to the - * HttpSession by this filter. - *

- *

- * If a valid SecurityContext cannot be obtained from the - * HttpSession for whatever reason, a fresh - * SecurityContext 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}. - *

- *

- * No HttpSession will be created by this filter if one does not - * already exist. If at the end of the web request the HttpSession - * does not exist, a HttpSession will only be created if - * the current contents of the SecurityContextHolder are not - * {@link java.lang.Object#equals(java.lang.Object)} to a new - * instance of {@link #setContextClass(Class)}. This avoids needless - * HttpSession creation, but automates the storage of changes - * made to the SecurityContextHolder. There is one exception to - * this rule, that is if the {@link #forceEagerSessionCreation} property is - * true, in which case sessions will always be created - * irrespective of normal session-minimisation logic (the default is - * false, as this is resource intensive and not recommended). - *

- *

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

- *

- * If for whatever reason no HttpSession should ever be - * created (eg this filter is only being used with Basic authentication or - * similar clients that will never present the same jsessionid - * etc), the {@link #setAllowSessionCreation(boolean)} 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. Please note that if {@link #forceEagerSessionCreation} - * is true, the allowSessionCreation must also be - * true (setting it to false will cause a startup - * time error). - *

- *

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

- * - * @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 contextClass = SecurityContextImpl.class; - - /** - * Indicates if this filter can create a HttpSession if - * needed (sessions are always created sparingly, but setting this value to - * false will prohibit sessions from ever being created). - * Defaults to true. Do not set to false if - * you are have set {@link #forceEagerSessionCreation} to true, - * as the properties would be in conflict. - */ - private boolean allowSessionCreation = true; - - /** - * Indicates if this filter is required to create a HttpSession - * for every request before proceeding through the filter chain, even if the - * HttpSession would not ordinarily have been created. By - * default this is false, which is entirely appropriate for - * most circumstances as you do not want a HttpSession - * created unless the filter actually needs one. It is envisaged the main - * situation in which this property would be set to true is - * if using other filters that depend on a HttpSession - * already existing, such as those which need to obtain a session ID. This - * is only required in specialised cases, so leave it set to - * false unless you have an actual requirement and are - * conscious of the session creation overhead. - */ - private boolean forceEagerSessionCreation = false; - - /** - * Indicates whether the SecurityContext will be cloned from - * the HttpSession. The default is to simply reference (ie - * the default is false). 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 - * HttpSession. 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 HttpSession. For unusual cases where this is not - * permitted, change this value to true and ensure the - * {@link #contextClass} is set to a SecurityContext that - * implements {@link Cloneable} and overrides the clone() - * 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 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"); - } - } -} diff --git a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java index 89521f3615..227d8b76f5 100644 --- a/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java +++ b/web/src/main/java/org/springframework/security/web/context/HttpSessionSecurityContextRepository.java @@ -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 saveContext() 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); - - if (logger.isDebugEnabled()) { - logger.debug("SecurityContext stored to HttpSession: '" + 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 (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 + "'"); + } + } } } } diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilterTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilterTests.java deleted file mode 100644 index 9be42a694f..0000000000 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionContextIntegrationFilterTests.java +++ /dev/null @@ -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; - } - - } - } -}