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.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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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> <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 xml:id="ns-concurrent-sessions">
|
||||
<title>Concurrent Session Control</title>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue