浏览代码

SEC-1761: Support HttpOnly Flag for Cookies when using Servlet 3.0

Rob Winch 14 年之前
父节点
当前提交
825f0061fb

+ 14 - 1
web/src/main/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServices.java

@@ -1,5 +1,7 @@
 package org.springframework.security.web.authentication.rememberme;
 package org.springframework.security.web.authentication.rememberme;
 
 
+import java.lang.reflect.Method;
+
 import javax.servlet.http.Cookie;
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpServletResponse;
@@ -25,12 +27,14 @@ import org.springframework.security.web.authentication.RememberMeServices;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
 import org.springframework.security.web.authentication.logout.LogoutHandler;
 import org.springframework.security.web.authentication.logout.LogoutHandler;
 import org.springframework.util.Assert;
 import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 
 
 /**
 /**
  * Base class for RememberMeServices implementations.
  * Base class for RememberMeServices implementations.
  *
  *
  * @author Luke Taylor
  * @author Luke Taylor
+ * @author Rob Winch
  * @since 2.0
  * @since 2.0
  */
  */
 public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
 public abstract class AbstractRememberMeServices implements RememberMeServices, InitializingBean, LogoutHandler {
@@ -57,6 +61,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
     private String key;
     private String key;
     private int tokenValiditySeconds = TWO_WEEKS_S;
     private int tokenValiditySeconds = TWO_WEEKS_S;
     private Boolean useSecureCookie = null;
     private Boolean useSecureCookie = null;
+    private Method setHttpOnlyMethod;
     private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
     private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
 
 
     /**
     /**
@@ -64,6 +69,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
      */
      */
     @Deprecated
     @Deprecated
     protected AbstractRememberMeServices() {
     protected AbstractRememberMeServices() {
+        this.setHttpOnlyMethod = ReflectionUtils.findMethod(Cookie.class,"setHttpOnly", boolean.class);
     }
     }
 
 
     protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
     protected AbstractRememberMeServices(String key, UserDetailsService userDetailsService) {
@@ -71,6 +77,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
         Assert.notNull(userDetailsService, "UserDetailsService cannot be null");
         Assert.notNull(userDetailsService, "UserDetailsService cannot be null");
         this.key = key;
         this.key = key;
         this.userDetailsService = userDetailsService;
         this.userDetailsService = userDetailsService;
+        this.setHttpOnlyMethod = ReflectionUtils.findMethod(Cookie.class,"setHttpOnly", boolean.class);
     }
     }
 
 
     public void afterPropertiesSet() throws Exception {
     public void afterPropertiesSet() throws Exception {
@@ -325,7 +332,7 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
      *
      *
      * By default a secure cookie will be used if the connection is secure. You can set the {@code useSecureCookie}
      * By default a secure cookie will be used if the connection is secure. You can set the {@code useSecureCookie}
      * property to {@code false} to override this. If you set it to {@code true}, the cookie will always be flagged
      * property to {@code false} to override this. If you set it to {@code true}, the cookie will always be flagged
-     * as secure.
+     * as secure. If Servlet 3.0 is used, the cookie will be marked as HttpOnly.
      *
      *
      * @param tokens the tokens which will be encoded to make the cookie value.
      * @param tokens the tokens which will be encoded to make the cookie value.
      * @param maxAge the value passed to {@link Cookie#setMaxAge(int)}
      * @param maxAge the value passed to {@link Cookie#setMaxAge(int)}
@@ -344,6 +351,12 @@ public abstract class AbstractRememberMeServices implements RememberMeServices,
             cookie.setSecure(useSecureCookie);
             cookie.setSecure(useSecureCookie);
         }
         }
 
 
+        if(setHttpOnlyMethod != null) {
+            ReflectionUtils.invokeMethod(setHttpOnlyMethod, cookie, Boolean.TRUE);
+        } else if (logger.isDebugEnabled()) {
+            logger.debug("Note: Cookie will not be marked as HttpOnly because you are not using Servlet 3.0 (Cookie#setHttpOnly(boolean) was not found).");
+        }
+
         response.addCookie(cookie);
         response.addCookie(cookie);
     }
     }
 
 

+ 50 - 0
web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesServlet3Tests.java

@@ -0,0 +1,50 @@
+package org.springframework.security.web.authentication.rememberme;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServicesTests.MockRememberMeServices;
+import org.springframework.util.ReflectionUtils;
+
+/**
+ * Note: This test will fail in the IDE since it needs to be ran with servlet 3.0 and servlet 2.5 is also on the classpath.
+ *
+ * @author Rob Winch
+ */
+public class AbstractRememberMeServicesServlet3Tests {
+
+    @Test
+    public void httpOnlySetInServlet30DefaultConstructor() throws Exception {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getContextPath()).thenReturn("/contextpath");
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        ArgumentCaptor<Cookie> cookie = ArgumentCaptor.forClass(Cookie.class);
+        MockRememberMeServices services = new MockRememberMeServices();
+        services.setCookie(new String[] {"mycookie"}, 1000, request, response);
+        verify(response).addCookie(cookie.capture());
+        Cookie rememberme = cookie.getValue();
+        assertTrue((Boolean)ReflectionUtils.invokeMethod(rememberme.getClass().getMethod("isHttpOnly"),rememberme));
+    }
+
+    @Test
+    public void httpOnlySetInServlet30() throws Exception {
+        HttpServletRequest request = mock(HttpServletRequest.class);
+        when(request.getContextPath()).thenReturn("/contextpath");
+        HttpServletResponse response = mock(HttpServletResponse.class);
+        ArgumentCaptor<Cookie> cookie = ArgumentCaptor.forClass(Cookie.class);
+        MockRememberMeServices services = new MockRememberMeServices("key",mock(UserDetailsService.class));
+        services.setCookie(new String[] {"mycookie"}, 1000, request, response);
+        verify(response).addCookie(cookie.capture());
+        Cookie rememberme = cookie.getValue();
+        assertTrue((Boolean)ReflectionUtils.invokeMethod(rememberme.getClass().getMethod("isHttpOnly"),rememberme));
+    }
+}

+ 16 - 2
web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java

@@ -23,6 +23,7 @@ import org.springframework.security.web.authentication.rememberme.AbstractRememb
 import org.springframework.security.web.authentication.rememberme.CookieTheftException;
 import org.springframework.security.web.authentication.rememberme.CookieTheftException;
 import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
 import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
 import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
 import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
+import org.springframework.test.util.ReflectionTestUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.util.StringUtils;
 
 
 /**
 /**
@@ -330,6 +331,15 @@ public class AbstractRememberMeServicesTests {
         assertTrue(cookie.getSecure());
         assertTrue(cookie.getSecure());
     }
     }
 
 
+    @Test
+    public void setHttpOnlyIgnoredForServlet25() throws Exception {
+        MockRememberMeServices services = new MockRememberMeServices();
+        assertNull(ReflectionTestUtils.getField(services, "setHttpOnlyMethod"));
+
+        services = new MockRememberMeServices("key",new MockUserDetailsService(joe, false));
+        assertNull(ReflectionTestUtils.getField(services, "setHttpOnlyMethod"));
+    }
+
     private Cookie[] createLoginCookie(String cookieToken) {
     private Cookie[] createLoginCookie(String cookieToken) {
         MockRememberMeServices services = new MockRememberMeServices();
         MockRememberMeServices services = new MockRememberMeServices();
         Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
         Cookie cookie = new Cookie(AbstractRememberMeServices.SPRING_SECURITY_REMEMBER_ME_COOKIE_KEY,
@@ -346,10 +356,14 @@ public class AbstractRememberMeServicesTests {
 
 
     //~ Inner Classes ==================================================================================================
     //~ Inner Classes ==================================================================================================
 
 
-    private class MockRememberMeServices extends AbstractRememberMeServices {
+    static class MockRememberMeServices extends AbstractRememberMeServices {
         boolean loginSuccessCalled;
         boolean loginSuccessCalled;
 
 
-        private MockRememberMeServices() {
+        MockRememberMeServices(String key, UserDetailsService userDetailsService) {
+            super(key,userDetailsService);
+        }
+
+        MockRememberMeServices() {
             setKey("key");
             setKey("key");
         }
         }
 
 

+ 26 - 0
web/web.gradle

@@ -1,4 +1,8 @@
 // Web module build file
 // Web module build file
+configurations {
+    servlet3Test
+    servlet3Test.exclude group: 'javax.servlet', name: 'sevlet-api'
+}
 
 
 dependencies {
 dependencies {
     compile project(':spring-security-core'),
     compile project(':spring-security-core'),
@@ -13,8 +17,30 @@ dependencies {
 
 
     provided 'javax.servlet:servlet-api:2.5'
     provided 'javax.servlet:servlet-api:2.5'
 
 
+    servlet3Test 'org.jboss.spec.javax.servlet:jboss-servlet-api_3.0_spec:1.0.0.Final'
+
     testCompile project(':spring-security-core').sourceSets.test.classes,
     testCompile project(':spring-security-core').sourceSets.test.classes,
                 'commons-codec:commons-codec:1.3',
                 'commons-codec:commons-codec:1.3',
                 "org.springframework:spring-test:$springVersion"
                 "org.springframework:spring-test:$springVersion"
     testRuntime "hsqldb:hsqldb:$hsqlVersion"
     testRuntime "hsqldb:hsqldb:$hsqlVersion"
 }
 }
+
+configurations.testRuntime.allDependencies.each {
+    if( !(it.group == 'javax.servlet' && it.name == 'servlet-api') ) {
+        configurations.servlet3Test.addDependency it
+    }
+}
+
+test {
+    exclude '**/*Servlet3Tests.class'
+}
+
+task servlet3Test(type: Test, dependsOn: testClasses) {
+    testClassesDir = sourceSets.test.classesDir
+    logging.captureStandardOutput(LogLevel.INFO)
+    classpath = sourceSets.main.classes + sourceSets.test.classes + configurations.servlet3Test
+    maxParallelForks = 1
+    testReport = false
+    include '**/*Servlet3Tests.class'
+}
+check.dependsOn servlet3Test