Selaa lähdekoodia

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.
Luke Taylor 15 vuotta sitten
vanhempi
commit
1b2b371970

+ 14 - 11
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();

+ 3 - 0
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

+ 5 - 0
config/src/main/resources/org/springframework/security/config/spring-security-3.1.xsd

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

+ 15 - 2
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 {

+ 12 - 1
docs/manual/src/docbook/appendix-namespace.xml

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

+ 30 - 2
docs/manual/src/docbook/namespace-config.xml

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

+ 28 - 1
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<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);
+        }
+    }
 }

+ 23 - 0
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());
+        }
+    }
 }