SEC-1544: Added CookieClearingLogoutHandler and 'delete-cookies' attribute to the 'logout' namespace element.

When the user logs out, the handler will attempt to delete the named cookies (which it is constructor-injected with) by expiring them in the response.

Also added documentation on the feature and a suggestion for deleting JSESSIONID through an Apache proxy server, if the servlet container doesn't allow clearing the session cookie.
This commit is contained in:
Luke Taylor 2010-09-16 16:03:24 +01:00
parent 383211561c
commit 1b2b371970
8 changed files with 130 additions and 17 deletions

View File

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

View File

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

View File

@ -881,6 +881,11 @@
<xs:documentation>A reference to a LogoutSuccessHandler implementation which will be used to determine the destination to which the user is taken after logging out.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="delete-cookies" type="xs:token">
<xs:annotation>
<xs:documentation>A comma-separated list of the names of cookies which should be deleted when the user logs out</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="request-cache"><xs:annotation>
<xs:documentation>Allow the RequestCache used for saving requests during the login process to be set</xs:documentation>

View File

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

View File

@ -496,11 +496,22 @@
<para> The destination URL which the user will be taken to after logging out.
Defaults to "/". </para>
</section>
<section>
<title>The <literal>success-handler-ref</literal> attribute</title>
<para>May be used to supply an instance of <interfacename>LogoutSuccessHandler</interfacename>
which will be invoked to control the navigation after logging out.
</para>
</section>
<section>
<title>The <literal>invalidate-session</literal> attribute</title>
<para> Maps to the <literal>invalidateHttpSession</literal> of the
<classname>SecurityContextLogoutHandler</classname>. Defaults to "true", so the
session will be invalidated on logout. </para>
session will be invalidated on logout.</para>
</section>
<section>
<title>The <literal>delete-cookies</literal> attribute</title>
<para>A comma-separated list of the names of cookies which should be deleted when the user logs out.
</para>
</section>
</section>
<section>

View File

@ -338,6 +338,14 @@
information on how to customize the flow when authentication fails. </para>
</section>
</section>
<section xml:id="ns-logout">
<title>Logout Handling</title>
<para>The <literal>logout</literal> element adds support for logging out by navigating
to a particular URL. The default logout URL is <literal>/j_spring_security_logout</literal>,
but you can set it to something else using the <literal>logout-url</literal> attribute.
More information on other available attributes may be found in the namespace appendix.
</para>
</section>
<section xml:id="ns-auth-providers">
<title>Using other Authentication Providers</title>
<para> In practice you will need a more scalable source of user information than a few
@ -465,8 +473,28 @@
the <literal>session-management</literal> element: <programlisting language="xml"><![CDATA[
<http>
...
<session-management invalid-session-url="/sessionTimeout.htm" />
</http>]]></programlisting></para>
<session-management invalid-session-url="/invalidSession.htm" />
</http>]]></programlisting>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: <programlisting language="xml"><![CDATA[
<http>
<logout delete-cookies="JSESSIONID" />
</http>
]]></programlisting> Unfortunately this can't be guaranteed to work with every servlet container,
so you will need to test it in your environment<footnote>
<para>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
<literal>JSESSIONID</literal> cookie by expiring it in the response to a
logout request (assuming the application is deployed under the path
<literal>/tutorial</literal>):
<programlisting> &lt;LocationMatch "/tutorial/j_spring_security_logout">
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
&lt;/LocationMatch></programlisting></para>
</footnote>. </para>
</section>
<section xml:id="ns-concurrent-sessions">
<title>Concurrent Session Control</title>

View File

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

View File

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