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:
parent
383211561c
commit
1b2b371970
|
@ -4,8 +4,10 @@ import org.springframework.beans.factory.config.BeanDefinition;
|
||||||
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
import org.springframework.beans.factory.config.RuntimeBeanReference;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||||
import org.springframework.beans.factory.support.ManagedList;
|
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.BeanDefinitionParser;
|
||||||
import org.springframework.beans.factory.xml.ParserContext;
|
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.LogoutFilter;
|
||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
@ -20,11 +22,11 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
|
||||||
static final String DEF_LOGOUT_SUCCESS_URL = "/";
|
static final String DEF_LOGOUT_SUCCESS_URL = "/";
|
||||||
|
|
||||||
static final String ATT_INVALIDATE_SESSION = "invalidate-session";
|
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 ATT_LOGOUT_URL = "logout-url";
|
||||||
static final String DEF_LOGOUT_URL = "/j_spring_security_logout";
|
static final String DEF_LOGOUT_URL = "/j_spring_security_logout";
|
||||||
static final String ATT_LOGOUT_HANDLER = "success-handler-ref";
|
static final String ATT_LOGOUT_HANDLER = "success-handler-ref";
|
||||||
|
static final String ATT_DELETE_COOKIES = "delete-cookies";
|
||||||
|
|
||||||
final String rememberMeServices;
|
final String rememberMeServices;
|
||||||
|
|
||||||
|
@ -38,6 +40,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
|
||||||
String successHandlerRef = null;
|
String successHandlerRef = null;
|
||||||
String logoutSuccessUrl = null;
|
String logoutSuccessUrl = null;
|
||||||
String invalidateSession = null;
|
String invalidateSession = null;
|
||||||
|
String deleteCookies = null;
|
||||||
|
|
||||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class);
|
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(LogoutFilter.class);
|
||||||
|
|
||||||
|
@ -50,6 +53,7 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
|
||||||
logoutSuccessUrl = element.getAttribute(ATT_LOGOUT_SUCCESS_URL);
|
logoutSuccessUrl = element.getAttribute(ATT_LOGOUT_SUCCESS_URL);
|
||||||
WebConfigUtils.validateHttpRedirect(logoutSuccessUrl, pc, source);
|
WebConfigUtils.validateHttpRedirect(logoutSuccessUrl, pc, source);
|
||||||
invalidateSession = element.getAttribute(ATT_INVALIDATE_SESSION);
|
invalidateSession = element.getAttribute(ATT_INVALIDATE_SESSION);
|
||||||
|
deleteCookies = element.getAttribute(ATT_DELETE_COOKIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StringUtils.hasText(logoutUrl)) {
|
if (!StringUtils.hasText(logoutUrl)) {
|
||||||
|
@ -71,23 +75,22 @@ class LogoutBeanDefinitionParser implements BeanDefinitionParser {
|
||||||
builder.addConstructorArgValue(logoutSuccessUrl);
|
builder.addConstructorArgValue(logoutSuccessUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StringUtils.hasText(invalidateSession)) {
|
|
||||||
invalidateSession = DEF_INVALIDATE_SESSION;
|
|
||||||
}
|
|
||||||
|
|
||||||
ManagedList handlers = new ManagedList();
|
ManagedList handlers = new ManagedList();
|
||||||
SecurityContextLogoutHandler sclh = new SecurityContextLogoutHandler();
|
BeanDefinition sclh = new RootBeanDefinition(SecurityContextLogoutHandler.class);
|
||||||
if ("true".equals(invalidateSession)) {
|
sclh.getPropertyValues().addPropertyValue("invalidateHttpSession", !"false".equals(invalidateSession));
|
||||||
sclh.setInvalidateHttpSession(true);
|
|
||||||
} else {
|
|
||||||
sclh.setInvalidateHttpSession(false);
|
|
||||||
}
|
|
||||||
handlers.add(sclh);
|
handlers.add(sclh);
|
||||||
|
|
||||||
if (rememberMeServices != null) {
|
if (rememberMeServices != null) {
|
||||||
handlers.add(new RuntimeBeanReference(rememberMeServices));
|
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);
|
builder.addConstructorArgValue(handlers);
|
||||||
|
|
||||||
return builder.getBeanDefinition();
|
return builder.getBeanDefinition();
|
||||||
|
|
|
@ -355,6 +355,9 @@ logout.attlist &=
|
||||||
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.
|
## 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}?
|
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 =
|
request-cache =
|
||||||
## Allow the RequestCache used for saving requests during the login process to be set
|
## Allow the RequestCache used for saving requests during the login process to be set
|
||||||
|
|
|
@ -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: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:annotation>
|
||||||
</xs:attribute>
|
</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:attributeGroup>
|
||||||
<xs:element name="request-cache"><xs:annotation>
|
<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>
|
<xs:documentation>Allow the RequestCache used for saving requests during the login process to be set</xs:documentation>
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.springframework.security.web.savedrequest.HttpSessionRequestCache
|
||||||
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
|
import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
|
||||||
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
|
import org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter
|
||||||
import org.springframework.security.web.session.SessionManagementFilter
|
import org.springframework.security.web.session.SessionManagementFilter
|
||||||
|
import org.springframework.security.web.authentication.logout.CookieClearingLogoutHandler
|
||||||
|
|
||||||
class MiscHttpConfigTests extends AbstractHttpConfigTests {
|
class MiscHttpConfigTests extends AbstractHttpConfigTests {
|
||||||
def 'Minimal configuration parses'() {
|
def 'Minimal configuration parses'() {
|
||||||
|
@ -312,8 +313,6 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
|
||||||
}
|
}
|
||||||
createAppContext()
|
createAppContext()
|
||||||
|
|
||||||
def filters = getFilters("/someurl")
|
|
||||||
|
|
||||||
expect:
|
expect:
|
||||||
getFilters("/someurl")[2] instanceof X509AuthenticationFilter
|
getFilters("/someurl")[2] instanceof X509AuthenticationFilter
|
||||||
}
|
}
|
||||||
|
@ -343,6 +342,20 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
|
||||||
BeanCreationException e = thrown()
|
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() {
|
def invalidLogoutUrlIsDetected() {
|
||||||
when:
|
when:
|
||||||
xml.http {
|
xml.http {
|
||||||
|
|
|
@ -496,11 +496,22 @@
|
||||||
<para> The destination URL which the user will be taken to after logging out.
|
<para> The destination URL which the user will be taken to after logging out.
|
||||||
Defaults to "/". </para>
|
Defaults to "/". </para>
|
||||||
</section>
|
</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>
|
<section>
|
||||||
<title>The <literal>invalidate-session</literal> attribute</title>
|
<title>The <literal>invalidate-session</literal> attribute</title>
|
||||||
<para> Maps to the <literal>invalidateHttpSession</literal> of the
|
<para> Maps to the <literal>invalidateHttpSession</literal> of the
|
||||||
<classname>SecurityContextLogoutHandler</classname>. Defaults to "true", so 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>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
|
|
|
@ -338,6 +338,14 @@
|
||||||
information on how to customize the flow when authentication fails. </para>
|
information on how to customize the flow when authentication fails. </para>
|
||||||
</section>
|
</section>
|
||||||
</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">
|
<section xml:id="ns-auth-providers">
|
||||||
<title>Using other Authentication Providers</title>
|
<title>Using other Authentication Providers</title>
|
||||||
<para> In practice you will need a more scalable source of user information than a few
|
<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[
|
the <literal>session-management</literal> element: <programlisting language="xml"><![CDATA[
|
||||||
<http>
|
<http>
|
||||||
...
|
...
|
||||||
<session-management invalid-session-url="/sessionTimeout.htm" />
|
<session-management invalid-session-url="/invalidSession.htm" />
|
||||||
</http>]]></programlisting></para>
|
</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> <LocationMatch "/tutorial/j_spring_security_logout">
|
||||||
|
Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"
|
||||||
|
</LocationMatch></programlisting></para>
|
||||||
|
</footnote>. </para>
|
||||||
</section>
|
</section>
|
||||||
<section xml:id="ns-concurrent-sessions">
|
<section xml:id="ns-concurrent-sessions">
|
||||||
<title>Concurrent Session Control</title>
|
<title>Concurrent Session Control</title>
|
||||||
|
|
|
@ -1,7 +1,34 @@
|
||||||
package org.springframework.security.web.authentication.logout;
|
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
|
* @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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,30 @@
|
||||||
package org.springframework.security.web.authentication.logout;
|
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
|
* @author Luke Taylor
|
||||||
*/
|
*/
|
||||||
public class CookieClearingLogoutHandlerTests {
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue