diff --git a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java index 6e1de830f9..5dd4f14627 100644 --- a/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/http/LogoutBeanDefinitionParser.java @@ -4,8 +4,10 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.ManagedList; +import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.util.StringUtils; @@ -20,11 +22,11 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { static final String DEF_LOGOUT_SUCCESS_URL = "/"; static final String ATT_INVALIDATE_SESSION = "invalidate-session"; - static final String DEF_INVALIDATE_SESSION = "true"; static final String ATT_LOGOUT_URL = "logout-url"; static final String DEF_LOGOUT_URL = "/j_spring_security_logout"; static final String ATT_LOGOUT_HANDLER = "success-handler-ref"; + static final String ATT_DELETE_COOKIES = "delete-cookies"; final String rememberMeServices; @@ -38,6 +40,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { String successHandlerRef = null; String logoutSuccessUrl = null; String invalidateSession = null; + String deleteCookies = null; BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class); @@ -50,6 +53,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { logoutSuccessUrl = element.getAttribute(ATT_LOGOUT_SUCCESS_URL); WebConfigUtils.validateHttpRedirect(logoutSuccessUrl, pc, source); invalidateSession = element.getAttribute(ATT_INVALIDATE_SESSION); + deleteCookies = element.getAttribute(ATT_DELETE_COOKIES); } if (!StringUtils.hasText(logoutUrl)) { @@ -71,23 +75,22 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser { builder.addConstructorArgValue(logoutSuccessUrl); } - if (!StringUtils.hasText(invalidateSession)) { - invalidateSession = DEF_INVALIDATE_SESSION; - } - ManagedList handlers = new ManagedList(); - SecurityContextLogoutHandler sclh = new SecurityContextLogoutHandler(); - if ("true".equals(invalidateSession)) { - sclh.setInvalidateHttpSession(true); - } else { - sclh.setInvalidateHttpSession(false); - } + BeanDefinition sclh = new RootBeanDefinition(SecurityContextLogoutHandler.class); + sclh.getPropertyValues().addPropertyValue("invalidateHttpSession", !"false".equals(invalidateSession)); handlers.add(sclh); if (rememberMeServices != null) { handlers.add(new RuntimeBeanReference(rememberMeServices)); } + if (StringUtils.hasText(deleteCookies)) { + BeanDefinition cookieDeleter = new RootBeanDefinition(CookieClearingLogoutHandler.class); + String[] names = StringUtils.commaDelimitedListToStringArray(deleteCookies); + cookieDeleter.getConstructorArgumentValues().addGenericArgumentValue(names); + handlers.add(cookieDeleter); + } + builder.addConstructorArgValue(handlers); return builder.getBeanDefinition(); diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc index b138557177..8b2294ce23 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.rnc @@ -355,6 +355,9 @@ logout.attlist &= logout.attlist &= ## A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. attribute success-handler-ref {xsd:token}? +logout.attlist &= + ## A comma-separated list of the names of cookies which should be deleted when the user logs out + attribute delete-cookies {xsd:token}? request-cache = ## Allow the RequestCache used for saving requests during the login process to be set diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd index e950e484f0..9844c01e6e 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd @@ -881,6 +881,11 @@ A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out. + + + A comma-separated list of the names of cookies which should be deleted when the user logs out + + Allow the RequestCache used for saving requests during the login process to be set diff --git a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy index 919d2d386b..5ab2134811 100644 --- a/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy +++ b/config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy @@ -43,6 +43,7 @@ import org.springframework.security.web.savedrequest.HttpSessionRequestCache import org.springframework.security.web.savedrequest.RequestCacheAwareFilter import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter import org.springframework.security.web.session.SessionManagementFilter +import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler class MiscHttpConfigTests extends AbstractHttpConfigTests { def 'Minimal configuration parses'() { @@ -312,8 +313,6 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { } createAppContext() - def filters = getFilters("/someurl") - expect: getFilters("/someurl")[2] instanceof X509AuthenticationFilter } @@ -343,6 +342,20 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests { BeanCreationException e = thrown() } + def cookiesToDeleteOnLogoutUrlAddsCorrectLogoutHandler() { + xml.http { + 'logout'('delete-cookies': 'JSESSIONID, mycookie') + 'form-login'() + } + createAppContext() + def handlers = getFilter(LogoutFilter).handlers + + expect: + handlers[1] instanceof CookieClearingLogoutHandler + handlers[1].cookiesToClear[0] = 'JSESSIONID' + handlers[1].cookiesToClear[1] = 'mycookie' + } + def invalidLogoutUrlIsDetected() { when: xml.http { diff --git a/docs/manual/src/docbook/appendix-namespace.xml b/docs/manual/src/docbook/appendix-namespace.xml index 63f3573b8a..533ddb1cf4 100644 --- a/docs/manual/src/docbook/appendix-namespace.xml +++ b/docs/manual/src/docbook/appendix-namespace.xml @@ -496,11 +496,22 @@ The destination URL which the user will be taken to after logging out. Defaults to "/". +
+ The <literal>success-handler-ref</literal> attribute + May be used to supply an instance of LogoutSuccessHandler + which will be invoked to control the navigation after logging out. + +
The <literal>invalidate-session</literal> attribute Maps to the invalidateHttpSession of the SecurityContextLogoutHandler. Defaults to "true", so the - session will be invalidated on logout. + session will be invalidated on logout. +
+
+ The <literal>delete-cookies</literal> attribute + A comma-separated list of the names of cookies which should be deleted when the user logs out. +
diff --git a/docs/manual/src/docbook/namespace-config.xml b/docs/manual/src/docbook/namespace-config.xml index 51a3baf3bd..a532ba5842 100644 --- a/docs/manual/src/docbook/namespace-config.xml +++ b/docs/manual/src/docbook/namespace-config.xml @@ -338,6 +338,14 @@ information on how to customize the flow when authentication fails.
+
+ Logout Handling + The logout element adds support for logging out by navigating + to a particular URL. The default logout URL is /j_spring_security_logout, + but you can set it to something else using the logout-url attribute. + More information on other available attributes may be found in the namespace appendix. + +
Using other Authentication Providers In practice you will need a more scalable source of user information than a few @@ -465,8 +473,28 @@ the session-management element: ... - - ]]> + + ]]>Note that if you use this mechanism to detect session timeouts, it + may falsely report an error if the user logs out and then logs back in without + closing the browser. This is because the session cookie is not cleared when you + invalidate the session and will be resubmitted even if the user has logged out. + You may be able to explicitly delete the JSESSIONID cookie on logging out, for + example by using the following syntax in the logout handler: + + + ]]> Unfortunately this can't be guaranteed to work with every servlet container, + so you will need to test it in your environment + If you are running your application behind a proxy, you may also be able + to remove the session cookie by configuring the proxy server. For example, + using Apache HTTPD's mod_headers, the following directive would delete the + JSESSIONID cookie by expiring it in the response to a + logout request (assuming the application is deployed under the path + /tutorial): + <LocationMatch "/tutorial/j_spring_security_logout"> + Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT" + </LocationMatch> + .
Concurrent Session Control diff --git a/web/src/main/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.java b/web/src/main/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.java index 20c98a974f..cb25f0a8cb 100644 --- a/web/src/main/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.java +++ b/web/src/main/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandler.java @@ -1,7 +1,34 @@ package org.springframework.security.web.authentication.logout; +import java.util.*; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.security.core.Authentication; +import org.springframework.util.Assert; + /** + * A logout handler which clears a defined list of cookies, using the context path as the + * cookie path. + * * @author Luke Taylor + * @since 3.1 */ -public class CookieClearingLogoutHandler { +public final class CookieClearingLogoutHandler implements LogoutHandler { + private final List cookiesToClear; + + public CookieClearingLogoutHandler(String... cookiesToClear) { + Assert.notNull(cookiesToClear, "List of cookies cannot be null"); + this.cookiesToClear = Arrays.asList(cookiesToClear); + } + + public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + for (String cookieName : cookiesToClear) { + Cookie cookie = new Cookie(cookieName, null); + cookie.setPath(request.getContextPath()); + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } } diff --git a/web/src/test/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandlerTests.java b/web/src/test/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandlerTests.java index c7b04f7e5c..7583c451eb 100644 --- a/web/src/test/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandlerTests.java +++ b/web/src/test/java/org/springframework/security/web/authentication/logout/CookieClearingLogoutHandlerTests.java @@ -1,7 +1,30 @@ package org.springframework.security.web.authentication.logout; +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +import javax.servlet.http.Cookie; + +import org.junit.Test; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.core.Authentication; + /** * @author Luke Taylor */ public class CookieClearingLogoutHandlerTests { + @Test + public void configuredCookiesAreCleared() { + MockHttpServletResponse response = new MockHttpServletResponse(); + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setContextPath("/app"); + CookieClearingLogoutHandler handler = new CookieClearingLogoutHandler("my_cookie", "my_cookie_too"); + handler.logout(request, response, mock(Authentication.class)); + assertEquals(2, response.getCookies().length); + for (Cookie c : response.getCookies()) { + assertEquals("/app", c.getPath()); + assertEquals(0, c.getMaxAge()); + } + } }