Pārlūkot izejas kodu

SEC-1998: Provide integration with WebAsyncManager#startCallableProcessing

Support integration of the Spring SecurityContext on Callable's used with
WebAsyncManager by registering SecurityContextCallableProcessingInterceptor.
Rob Winch 12 gadi atpakaļ
vecāks
revīzija
1ed643ca1f
29 mainītis faili ar 583 papildinājumiem un 35 dzēšanām
  1. 2 2
      cas/cas.gradle
  2. 1 1
      config/config.gradle
  3. 17 1
      config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java
  4. 17 0
      config/src/main/java/org/springframework/security/config/http/SecurityFilters.java
  5. 18 1
      config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy
  6. 6 3
      config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy
  7. 1 1
      config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy
  8. 49 0
      core/src/test/java/org/springframework/security/core/JavaVersionTests.java
  9. 23 2
      gradle/ide-integration.gradle
  10. 1 0
      gradle/javaprojects.gradle
  11. 1 1
      itest/context/itest-context.gradle
  12. 1 1
      itest/web/itest-web.gradle
  13. 1 1
      openid/openid.gradle
  14. 1 1
      samples/cas/sample/cassample.gradle
  15. 1 1
      samples/contacts/contacts.gradle
  16. 1 1
      samples/gae/gae.gradle
  17. 1 1
      samples/jaas/jaas.gradle
  18. 1 1
      samples/openid/openid.gradle
  19. 1 1
      samples/tutorial/tutorial.gradle
  20. 2 2
      sandbox/heavyduty/build.gradle
  21. 3 2
      taglibs/taglibs.gradle
  22. 79 3
      web/src/main/java/org/springframework/security/web/FilterInvocation.java
  23. 79 0
      web/src/main/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptor.java
  24. 52 0
      web/src/main/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.java
  25. 1 1
      web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java
  26. 18 6
      web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java
  27. 77 0
      web/src/test/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptorTests.java
  28. 127 0
      web/src/test/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilterTests.java
  29. 1 1
      web/web.gradle

+ 2 - 2
cas/cas.gradle

@@ -8,5 +8,5 @@ dependencies {
             "org.jasig.cas.client:cas-client-core:3.1.12",
             "net.sf.ehcache:ehcache:$ehcacheVersion"
 
-    provided 'javax.servlet:servlet-api:2.5'
-}            
+    provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
+}

+ 1 - 1
config/config.gradle

@@ -21,7 +21,7 @@ dependencies {
             "org.springframework:spring-web:$springVersion",
             "org.springframework:spring-beans:$springVersion"
 
-    provided "javax.servlet:servlet-api:2.5"
+    provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     groovy 'org.codehaus.groovy:groovy:1.8.7'
 

+ 17 - 1
config/src/main/java/org/springframework/security/config/http/HttpConfigurationBuilder.java

@@ -21,6 +21,8 @@ import static org.springframework.security.config.http.SecurityFilters.*;
 import java.util.ArrayList;
 import java.util.List;
 
+import javax.servlet.ServletRequest;
+
 import org.springframework.beans.factory.config.BeanDefinition;
 import org.springframework.beans.factory.config.BeanReference;
 import org.springframework.beans.factory.config.RuntimeBeanReference;
@@ -53,6 +55,7 @@ import org.springframework.security.web.authentication.session.SessionFixationPr
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
 import org.springframework.security.web.context.NullSecurityContextRepository;
 import org.springframework.security.web.context.SecurityContextPersistenceFilter;
+import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter;
 import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
 import org.springframework.security.web.savedrequest.NullRequestCache;
@@ -61,6 +64,7 @@ import org.springframework.security.web.servletapi.SecurityContextHolderAwareReq
 import org.springframework.security.web.session.ConcurrentSessionFilter;
 import org.springframework.security.web.session.SessionManagementFilter;
 import org.springframework.security.web.session.SimpleRedirectInvalidSessionStrategy;
+import org.springframework.util.ClassUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.util.xml.DomUtils;
 import org.w3c.dom.Element;
@@ -102,6 +106,7 @@ class HttpConfigurationBuilder {
     private BeanReference contextRepoRef;
     private BeanReference sessionRegistryRef;
     private BeanDefinition concurrentSessionFilter;
+    private BeanDefinition webAsyncManagerFilter;
     private BeanDefinition requestCacheAwareFilter;
     private BeanReference sessionStrategyRef;
     private RootBeanDefinition sfpf;
@@ -114,7 +119,6 @@ class HttpConfigurationBuilder {
 
     public HttpConfigurationBuilder(Element element, ParserContext pc,
             BeanReference portMapper, BeanReference portResolver, BeanReference authenticationManager) {
-
         this.httpElt = element;
         this.pc = pc;
         this.portMapper = portMapper;
@@ -140,6 +144,7 @@ class HttpConfigurationBuilder {
 
         createSecurityContextPersistenceFilter();
         createSessionManagementFilters();
+        createWebAsyncManagerFilter();
         createRequestCacheFilter();
         createServletApiFilter();
         createJaasApiFilter();
@@ -350,6 +355,13 @@ class HttpConfigurationBuilder {
         sessionRegistryRef = new RuntimeBeanReference(sessionRegistryId);
     }
 
+    private void createWebAsyncManagerFilter() {
+        boolean asyncSupported = ClassUtils.hasMethod(ServletRequest.class, "startAsync");
+        if(asyncSupported) {
+            webAsyncManagerFilter = new RootBeanDefinition(WebAsyncManagerIntegrationFilter.class);
+        }
+    }
+
     // Adds the servlet-api integration filter if required
     private void createServletApiFilter() {
         final String ATT_SERVLET_API_PROVISION = "servlet-api-provision";
@@ -552,6 +564,10 @@ class HttpConfigurationBuilder {
             filters.add(new OrderDecorator(concurrentSessionFilter, CONCURRENT_SESSION_FILTER));
         }
 
+        if (webAsyncManagerFilter != null) {
+            filters.add(new OrderDecorator(webAsyncManagerFilter, WEB_ASYNC_MANAGER_FILTER));
+        }
+
         filters.add(new OrderDecorator(securityContextPersistenceFilter, SECURITY_CONTEXT_FILTER));
 
         if (servApiFilter != null) {

+ 17 - 0
config/src/main/java/org/springframework/security/config/http/SecurityFilters.java

@@ -1,10 +1,25 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
 package org.springframework.security.config.http;
 
+import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
+
 
 /**
  * Stores the default order numbers of all Spring Security filters for use in configuration.
  *
  * @author Luke Taylor
+ * @author Rob Winch
  */
 
 enum SecurityFilters {
@@ -12,6 +27,8 @@ enum SecurityFilters {
     CHANNEL_FILTER,
     SECURITY_CONTEXT_FILTER,
     CONCURRENT_SESSION_FILTER,
+    /** {@link WebAsyncManagerIntegrationFilter} */
+    WEB_ASYNC_MANAGER_FILTER,
     LOGOUT_FILTER,
     X509_FILTER,
     PRE_AUTH_FILTER,

+ 18 - 1
config/src/test/groovy/org/springframework/security/config/http/AbstractHttpConfigTests.groovy

@@ -1,3 +1,15 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
 package org.springframework.security.config.http
 
 import javax.servlet.Filter
@@ -8,8 +20,13 @@ import org.springframework.security.config.AbstractXmlConfigTests
 import org.springframework.security.config.BeanIds
 import org.springframework.security.web.FilterInvocation
 
+/**
+ *
+ * @author Rob Winch
+ *
+ */
 abstract class AbstractHttpConfigTests extends AbstractXmlConfigTests {
-    final int AUTO_CONFIG_FILTERS = 11;
+    final int AUTO_CONFIG_FILTERS = 12;
 
     def httpAutoConfig(Closure c) {
         xml.http('auto-config': 'true', c)

+ 6 - 3
config/src/test/groovy/org/springframework/security/config/http/MiscHttpConfigTests.groovy

@@ -54,6 +54,7 @@ import org.springframework.security.web.authentication.www.BasicAuthenticationEn
 import org.springframework.security.web.authentication.www.BasicAuthenticationFilter
 import org.springframework.security.web.context.HttpSessionSecurityContextRepository
 import org.springframework.security.web.context.SecurityContextPersistenceFilter
+import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
 import org.springframework.security.web.jaasapi.JaasApiIntegrationFilter
 import org.springframework.security.web.savedrequest.HttpSessionRequestCache
 import org.springframework.security.web.savedrequest.RequestCacheAwareFilter
@@ -79,6 +80,7 @@ import org.springframework.security.authentication.AuthenticationManager
  * @author Rob Winch
  */
 class MiscHttpConfigTests extends AbstractHttpConfigTests {
+
     def 'Minimal configuration parses'() {
         setup:
         xml.http {
@@ -101,6 +103,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
         Iterator<Filter> filters = filterList.iterator();
 
         assert filters.next() instanceof SecurityContextPersistenceFilter
+        assert filters.next() instanceof WebAsyncManagerIntegrationFilter
         assert filters.next() instanceof LogoutFilter
         Object authProcFilter = filters.next();
         assert authProcFilter instanceof UsernamePasswordAuthenticationFilter
@@ -181,7 +184,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
         createAppContext()
 
         expect:
-        getFilters("/anything")[5] instanceof AnonymousAuthenticationFilter
+        getFilters("/anything")[6] instanceof AnonymousAuthenticationFilter
     }
 
     def anonymousFilterIsRemovedIfDisabledFlagSet() {
@@ -354,7 +357,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
         AUTO_CONFIG_FILTERS + 3 == filters.size();
         filters[0] instanceof SecurityContextHolderAwareRequestFilter
         filters[1] instanceof SecurityContextPersistenceFilter
-        filters[4] instanceof SecurityContextHolderAwareRequestFilter
+        filters[5] instanceof SecurityContextHolderAwareRequestFilter
         filters[1] instanceof SecurityContextPersistenceFilter
     }
 
@@ -377,7 +380,7 @@ class MiscHttpConfigTests extends AbstractHttpConfigTests {
         createAppContext()
 
         expect:
-        getFilters("/someurl")[2] instanceof X509AuthenticationFilter
+        getFilters("/someurl")[3] instanceof X509AuthenticationFilter
     }
 
     def x509SubjectPrincipalRegexCanBeSetUsingPropertyPlaceholder() {

+ 1 - 1
config/src/test/groovy/org/springframework/security/config/http/SessionManagementConfigTests.groovy

@@ -305,7 +305,7 @@ class SessionManagementConfigTests extends AbstractHttpConfigTests {
             'session-management'('session-fixation-protection': 'none', 'invalid-session-url': '/timeoutUrl')
         }
         createAppContext()
-        def filter = getFilters("/someurl")[8]
+        def filter = getFilters("/someurl")[9]
 
         expect:
         filter instanceof SessionManagementFilter

+ 49 - 0
core/src/test/java/org/springframework/security/core/JavaVersionTests.java

@@ -0,0 +1,49 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.springframework.security.core;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.io.DataInputStream;
+import java.io.InputStream;
+
+import org.junit.Test;
+
+/**
+ *
+ * @author Rob Winch
+ *
+ */
+public class JavaVersionTests {
+
+    private static final int JDK5_CLASS_VERSION = 49;
+
+    @Test
+    public void authenticationCorrectJdkCompatibility() throws Exception {
+        assertClassVersion(Authentication.class);
+    }
+
+    private void assertClassVersion(Class<?> clazz) throws Exception {
+        String classResourceName = clazz.getName().replaceAll("\\.", "/") + ".class";
+        InputStream input = Thread.currentThread().getContextClassLoader().getResourceAsStream(classResourceName);
+        try {
+            DataInputStream data = new DataInputStream(input);
+            data.readInt();
+            data.readShort(); // minor
+            int major = data.readShort();
+            assertThat(major).isEqualTo(JDK5_CLASS_VERSION);
+        } finally {
+            try { input.close(); } catch(Exception e) {}
+        }
+    }
+}

+ 23 - 2
gradle/ide-integration.gradle

@@ -39,8 +39,8 @@ configure(javaProjects) {
     }
 }
 
-// STS-2723
-project(':spring-security-samples-aspectj') {
+// STS-3057
+configure(allprojects) {
     task afterEclipseImport {
         ext.srcFile = file('.classpath')
         inputs.file srcFile
@@ -48,6 +48,25 @@ project(':spring-security-samples-aspectj') {
 
         onlyIf { srcFile.exists() }
 
+        doLast {
+            def classpath = new XmlParser().parse(srcFile)
+            classpath.classpathentry.findAll{ it.@path == 'GROOVY_SUPPORT' }.each { classpath.remove(it) }
+
+            def writer = new FileWriter(srcFile)
+            new XmlNodePrinter(new PrintWriter(writer)).print(classpath)
+        }
+    }
+}
+
+// STS-2723
+project(':spring-security-samples-aspectj') {
+    task afterEclipseImportAjdtFix {
+        ext.srcFile = afterEclipseImport.srcFile
+        inputs.file srcFile
+        outputs.dir srcFile
+
+        onlyIf { srcFile.exists() }
+
         doLast {
             def classpath = new XmlParser().parse(srcFile)
 
@@ -63,4 +82,6 @@ project(':spring-security-samples-aspectj') {
             new XmlNodePrinter(new PrintWriter(writer)).print(classpath)
         }
     }
+    afterEclipseImport.dependsOn afterEclipseImportAjdtFix
 }
+

+ 1 - 0
gradle/javaprojects.gradle

@@ -16,6 +16,7 @@ ext.slf4jVersion = '1.6.1'
 ext.logbackVersion = '0.9.29'
 ext.cglibVersion = '2.2'
 ext.powerMockVersion = '1.4.12'
+ext.servletApiVersion = '7.0.33'
 
 ext.powerMockDependencies = [
     "org.powermock:powermock-core:$powerMockVersion",

+ 1 - 1
itest/context/itest-context.gradle

@@ -10,7 +10,7 @@ dependencies {
                 "org.springframework:spring-beans:$springVersion"
 
     testCompile project(':spring-security-web'),
-                'javax.servlet:servlet-api:2.5',
+                "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion",
                 "org.springframework:spring-web:$springVersion"
     testRuntime project(':spring-security-config')
 

+ 1 - 1
itest/web/itest-web.gradle

@@ -3,7 +3,7 @@ dependencies {
     compile     "org.springframework:spring-context:$springVersion",
                 "org.springframework:spring-web:$springVersion"
 
-    provided    'javax.servlet:servlet-api:2.5'
+    provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     testCompile project(':spring-security-core'),
                 project(':spring-security-web'),

+ 1 - 1
openid/openid.gradle

@@ -16,7 +16,7 @@ dependencies {
     }
     compile 'com.google.inject:guice:2.0'
 
-    provided 'javax.servlet:servlet-api:2.5'
+    provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     runtime 'org.apache.httpcomponents:httpclient:4.1.1'
 }

+ 1 - 1
samples/cas/sample/cassample.gradle

@@ -30,7 +30,7 @@ eclipse.classpath.plusConfigurations += configurations.integrationTestRuntime
 dependencies {
     groovy 'org.codehaus.groovy:groovy:1.8.7'
 
-    providedCompile 'javax.servlet:servlet-api:2.5@jar'
+    providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     compile project(':spring-security-core'),
             project(':spring-security-cas'),

+ 1 - 1
samples/contacts/contacts.gradle

@@ -9,7 +9,7 @@ configurations {
 }
 
 dependencies {
-    providedCompile 'javax.servlet:servlet-api:2.5@jar'
+    providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     compile project(':spring-security-core'),
             project(':spring-security-acl'),

+ 1 - 1
samples/gae/gae.gradle

@@ -21,7 +21,7 @@ repositories {
 configurations.runtime.exclude(group: 'ch.qos.logback')
 
 dependencies {
-    providedCompile 'javax.servlet:servlet-api:2.5@jar'
+    providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     compile project(':spring-security-core'),
             project(':spring-security-web'),

+ 1 - 1
samples/jaas/jaas.gradle

@@ -14,7 +14,7 @@ configurations {
 }
 
 dependencies {
-    providedCompile 'javax.servlet:servlet-api:2.5@jar'
+    providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     compile project(':spring-security-core'),
             "org.springframework:spring-beans:$springVersion",

+ 1 - 1
samples/openid/openid.gradle

@@ -7,7 +7,7 @@ dependencies {
     compile project(':spring-security-core'),
             project(':spring-security-openid')
 
-    providedCompile 'javax.servlet:servlet-api:2.5@jar'
+    providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     runtime project(':spring-security-config'),
             project(':spring-security-taglibs'),

+ 1 - 1
samples/tutorial/tutorial.gradle

@@ -14,7 +14,7 @@ configurations {
 }
 
 dependencies {
-    providedCompile 'javax.servlet:servlet-api:2.5@jar'
+    providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     compile project(':spring-security-core'),
             "org.springframework:spring-beans:$springVersion",

+ 2 - 2
sandbox/heavyduty/build.gradle

@@ -24,9 +24,9 @@ dependencies {
             'org.aspectj:aspectjrt:1.6.8',
             'org.hibernate:ejb3-persistence:1.0.2.GA',
             'javax.persistence:persistence-api:1.0',
-            'org.slf4j:jcl-over-slf4j:1.5.11'            
+            'org.slf4j:jcl-over-slf4j:1.5.11'
 
-    providedCompile 'javax.servlet:servlet-api:2.5'
+    providedCompile "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     runtime 'org.hibernate:hibernate-entitymanager:3.4.0.GA',
             "org.springframework:spring-context-support:$springVersion",

+ 3 - 2
taglibs/taglibs.gradle

@@ -10,7 +10,8 @@ dependencies {
             "org.springframework:spring-expression:$springVersion",
             "org.springframework:spring-web:$springVersion"
 
-    provided 'javax.servlet:jsp-api:2.0', 'javax.servlet:servlet-api:2.5'
-    
+    provided 'javax.servlet:jsp-api:2.0',
+             "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
+
     testRuntime "javax.servlet:jstl:$jstlVersion"
 }

+ 79 - 3
web/src/main/java/org/springframework/security/web/FilterInvocation.java

@@ -1,4 +1,5 @@
-/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
+/* Copyright 2002-2012 the original author or authors.
+ * Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -20,12 +21,16 @@ import java.io.IOException;
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.security.Principal;
+import java.util.Collection;
 import java.util.Enumeration;
 import java.util.Locale;
 import java.util.Map;
 
+import javax.servlet.AsyncContext;
+import javax.servlet.DispatcherType;
 import javax.servlet.FilterChain;
 import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.ServletInputStream;
 import javax.servlet.ServletOutputStream;
@@ -35,6 +40,7 @@ import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
+import javax.servlet.http.Part;
 
 import org.springframework.security.web.util.UrlUtils;
 
@@ -50,6 +56,7 @@ import org.springframework.security.web.util.UrlUtils;
  * @author Ben Alex
  * @author colin sampaleanu
  * @author Luke Taylor
+ * @author Rob Winch
  */
 public class FilterInvocation {
     //~ Static fields ==================================================================================================
@@ -147,7 +154,7 @@ public class FilterInvocation {
     }
 }
 
-@SuppressWarnings({"unchecked", "deprecation"})
+@SuppressWarnings({"unchecked"})
 class DummyRequest implements HttpServletRequest {
     private String requestURI;
     private String contextPath = "";
@@ -221,10 +228,12 @@ class DummyRequest implements HttpServletRequest {
         throw new UnsupportedOperationException();
     }
 
+    @SuppressWarnings("rawtypes")
     public Enumeration getHeaderNames() {
         throw new UnsupportedOperationException();
     }
 
+    @SuppressWarnings("rawtypes")
     public Enumeration getHeaders(String name) {
         throw new UnsupportedOperationException();
     }
@@ -285,6 +294,7 @@ class DummyRequest implements HttpServletRequest {
         throw new UnsupportedOperationException();
     }
 
+    @SuppressWarnings("rawtypes")
     public Enumeration getAttributeNames() {
         throw new UnsupportedOperationException();
     }
@@ -322,6 +332,7 @@ class DummyRequest implements HttpServletRequest {
         throw new UnsupportedOperationException();
     }
 
+    @SuppressWarnings("rawtypes")
     public Enumeration getLocales() {
         throw new UnsupportedOperationException();
     }
@@ -330,10 +341,12 @@ class DummyRequest implements HttpServletRequest {
         throw new UnsupportedOperationException();
     }
 
+    @SuppressWarnings("rawtypes")
     public Map getParameterMap() {
         throw new UnsupportedOperationException();
     }
 
+    @SuppressWarnings("rawtypes")
     public Enumeration getParameterNames() {
         throw new UnsupportedOperationException();
     }
@@ -397,9 +410,56 @@ class DummyRequest implements HttpServletRequest {
     public void setCharacterEncoding(String env) throws UnsupportedEncodingException {
         throw new UnsupportedOperationException();
     }
+
+    public ServletContext getServletContext() {
+        throw new UnsupportedOperationException();
+    }
+
+    public AsyncContext startAsync() {
+        throw new UnsupportedOperationException();
+    }
+
+    public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isAsyncStarted() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean isAsyncSupported() {
+        throw new UnsupportedOperationException();
+    }
+
+    public AsyncContext getAsyncContext() {
+        throw new UnsupportedOperationException();
+    }
+
+    public DispatcherType getDispatcherType() {
+        throw new UnsupportedOperationException();
+    }
+
+    public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void login(String username, String password) throws ServletException {
+        throw new UnsupportedOperationException();
+    }
+
+    public void logout() throws ServletException {
+        throw new UnsupportedOperationException();
+    }
+
+    public Collection<Part> getParts() throws IOException, IllegalStateException, ServletException {
+        throw new UnsupportedOperationException();
+    }
+
+    public Part getPart(String name) throws IOException, IllegalStateException, ServletException {
+        throw new UnsupportedOperationException();
+    }
 }
 
-@SuppressWarnings({"deprecation"})
 class DummyResponse implements HttpServletResponse {
     public void addCookie(Cookie cookie) {
         throw new UnsupportedOperationException();
@@ -529,4 +589,20 @@ class DummyResponse implements HttpServletResponse {
     public void setLocale(Locale loc) {
         throw new UnsupportedOperationException();
     }
+
+    public int getStatus() {
+        throw new UnsupportedOperationException();
+    }
+
+    public String getHeader(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Collection<String> getHeaders(String name) {
+        throw new UnsupportedOperationException();
+    }
+
+    public Collection<String> getHeaderNames() {
+        throw new UnsupportedOperationException();
+    }
 }

+ 79 - 0
web/src/main/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptor.java

@@ -0,0 +1,79 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.springframework.security.web.context.request.async;
+
+import java.util.concurrent.Callable;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.util.Assert;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.context.request.async.CallableProcessingInterceptor;
+import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter;
+
+/**
+ * <p>
+ * Allows for integration with Spring MVC's {@link Callable} support.
+ * </p>
+ * <p>
+ * A {@link CallableProcessingInterceptor} that establishes the injected {@link SecurityContext} on the
+ * {@link SecurityContextHolder} when {@link #preProcess(NativeWebRequest, Callable)} is invoked. It also clear out the
+ * {@link SecurityContextHolder} by invoking {@link SecurityContextHolder#clearContext()} in the
+ * {@link #afterCompletion(NativeWebRequest, Callable)} method.
+ * </p>
+ *
+ * @author Rob Winch
+ * @since 3.2
+ */
+public final class SecurityContextCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter {
+    private SecurityContext securityContext;
+
+    /**
+     * Create a new {@link SecurityContextCallableProcessingInterceptor} that uses the {@link SecurityContext} from the
+     * {@link SecurityContextHolder} at the time {@link #beforeConcurrentHandling(NativeWebRequest, Callable)} is invoked.
+     */
+    public SecurityContextCallableProcessingInterceptor() {
+    }
+
+    /**
+     * Creates a new {@link SecurityContextCallableProcessingInterceptor} with the specified {@link SecurityContext}.
+     * @param securityContext the {@link SecurityContext} to set on the {@link SecurityContextHolder} in
+     * {@link #preProcess(NativeWebRequest, Callable)}. Cannot be null.
+     * @throws IllegalArgumentException if {@link SecurityContext} is null.
+     */
+    public SecurityContextCallableProcessingInterceptor(SecurityContext securityContext) {
+        Assert.notNull(securityContext, "securityContext cannot be null");
+        setSecurityContext(securityContext);
+    }
+
+    @Override
+    public <T> void beforeConcurrentHandling(NativeWebRequest request, Callable<T> task) throws Exception {
+        if(securityContext == null) {
+            setSecurityContext(SecurityContextHolder.getContext());
+        }
+    }
+
+    @Override
+    public <T> void afterCompletion(NativeWebRequest request, Callable<T> task) throws Exception {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Override
+    public <T> void preProcess(NativeWebRequest request, Callable<T> task) throws Exception {
+        SecurityContextHolder.setContext(securityContext);
+    }
+
+    private void setSecurityContext(SecurityContext securityContext) {
+        this.securityContext = securityContext;
+    }
+}

+ 52 - 0
web/src/main/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilter.java

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.springframework.security.web.context.request.async;
+
+import java.io.IOException;
+import java.util.concurrent.Callable;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.web.context.request.async.WebAsyncManager;
+import org.springframework.web.context.request.async.WebAsyncUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+/**
+ * Provides integration between the {@link SecurityContext} and Spring Web's {@link WebAsyncManager} by using the
+ * {@link SecurityContextCallableProcessingInterceptor#beforeConcurrentHandling(org.springframework.web.context.request.NativeWebRequest, Callable)}
+ * to populate the {@link SecurityContext} on the {@link Callable}.
+ *
+ * @author Rob Winch
+ * @see SecurityContextCallableProcessingInterceptor
+ */
+public final class WebAsyncManagerIntegrationFilter extends OncePerRequestFilter {
+    private static final Object CALLABLE_INTERCEPTOR_KEY = new Object();
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
+            throws ServletException, IOException {
+        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
+
+        SecurityContextCallableProcessingInterceptor securityProcessingInterceptor = (SecurityContextCallableProcessingInterceptor) asyncManager.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY);
+        if (securityProcessingInterceptor == null) {
+            asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY,
+                    new SecurityContextCallableProcessingInterceptor());
+        }
+
+        filterChain.doFilter(request, response);
+    }
+}

+ 1 - 1
web/src/main/java/org/springframework/security/web/savedrequest/DefaultSavedRequest.java

@@ -110,7 +110,7 @@ public class DefaultSavedRequest implements SavedRequest {
         }
 
         // Parameters
-        Map<String,Object> parameters = request.getParameterMap();
+        Map<String,String[]> parameters = request.getParameterMap();
 
         for(String paramName : parameters.keySet()) {
             Object paramValues = parameters.get(paramName);

+ 18 - 6
web/src/test/java/org/springframework/security/web/authentication/rememberme/AbstractRememberMeServicesTests.java

@@ -1,13 +1,23 @@
 package org.springframework.security.web.authentication.rememberme;
 
-import static org.junit.Assert.*;
-import static org.mockito.Mockito.*;
+import static org.powermock.api.mockito.PowerMockito.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
 
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.core.classloader.annotations.PrepareOnlyThisForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
 import org.springframework.security.authentication.AccountStatusUserDetailsChecker;
@@ -19,17 +29,16 @@ import org.springframework.security.core.userdetails.User;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
-import org.springframework.security.web.authentication.rememberme.AbstractRememberMeServices;
-import org.springframework.security.web.authentication.rememberme.CookieTheftException;
-import org.springframework.security.web.authentication.rememberme.InvalidCookieException;
-import org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationException;
 import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.util.ReflectionUtils;
 import org.springframework.util.StringUtils;
 
 /**
  * @author Luke Taylor
  */
 @SuppressWarnings("unchecked")
+@RunWith(PowerMockRunner.class)
+@PrepareOnlyThisForTest(ReflectionUtils.class)
 public class AbstractRememberMeServicesTests {
     static User joe = new User("joe", "password", true, true,true,true, AuthorityUtils.createAuthorityList("ROLE_A"));
 
@@ -333,6 +342,9 @@ public class AbstractRememberMeServicesTests {
 
     @Test
     public void setHttpOnlyIgnoredForServlet25() throws Exception {
+        spy(ReflectionUtils.class);
+        when(ReflectionUtils.findMethod(Cookie.class,"setHttpOnly", boolean.class)).thenReturn(null);
+
         MockRememberMeServices services = new MockRememberMeServices();
         assertNull(ReflectionTestUtils.getField(services, "setHttpOnlyMethod"));
 

+ 77 - 0
web/src/test/java/org/springframework/security/web/context/request/async/SecurityContextCallableProcessingInterceptorTests.java

@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.springframework.security.web.context.request.async;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.util.concurrent.Callable;
+
+import org.junit.After;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.context.request.NativeWebRequest;
+
+/**
+ *
+ * @author Rob Winch
+ *
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class SecurityContextCallableProcessingInterceptorTests {
+    @Mock
+    private SecurityContext securityContext;
+    @Mock
+    private Callable<?> callable;
+    @Mock
+    private NativeWebRequest webRequest;
+
+    @After
+    public void clearSecurityContext() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void constructorNull() {
+        new SecurityContextCallableProcessingInterceptor(null);
+    }
+
+    @Test
+    public void currentSecurityContext() throws Exception {
+        SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor();
+        SecurityContextHolder.setContext(securityContext);
+        interceptor.beforeConcurrentHandling(webRequest, callable);
+        SecurityContextHolder.clearContext();
+
+        interceptor.preProcess(webRequest, callable);
+        assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext);
+
+        interceptor.afterCompletion(webRequest, callable);
+        assertThat(SecurityContextHolder.getContext()).isNotSameAs(securityContext);
+    }
+
+    @Test
+    public void specificSecurityContext() throws Exception {
+        SecurityContextCallableProcessingInterceptor interceptor = new SecurityContextCallableProcessingInterceptor(
+                securityContext);
+
+        interceptor.preProcess(webRequest, callable);
+        assertThat(SecurityContextHolder.getContext()).isSameAs(securityContext);
+
+        interceptor.afterCompletion(webRequest, callable);
+        assertThat(SecurityContextHolder.getContext()).isNotSameAs(securityContext);
+    }
+}

+ 127 - 0
web/src/test/java/org/springframework/security/web/context/request/async/WebAsyncManagerIntegrationFilterTests.java

@@ -0,0 +1,127 @@
+/*
+ * Copyright 2002-2012 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.springframework.security.web.context.request.async;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.ThreadFactory;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.core.task.SimpleAsyncTaskExecutor;
+import org.springframework.mock.web.MockFilterChain;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.context.request.async.AsyncWebRequest;
+import org.springframework.web.context.request.async.WebAsyncManager;
+import org.springframework.web.context.request.async.WebAsyncUtils;
+
+
+/**
+ *
+ * @author Rob Winch
+ *
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class WebAsyncManagerIntegrationFilterTests {
+    @Mock
+    private SecurityContext securityContext;
+    @Mock
+    private HttpServletRequest request;
+    @Mock
+    private HttpServletResponse response;
+    @Mock
+    private AsyncWebRequest asyncWebRequest;
+    private WebAsyncManager asyncManager;
+    private JoinableThreadFactory threadFactory;
+
+    private MockFilterChain filterChain;
+
+    private WebAsyncManagerIntegrationFilter filter;
+
+    @Before
+    public void setUp() {
+        when(asyncWebRequest.getNativeRequest(HttpServletRequest.class)).thenReturn(request);
+        when(request.getRequestURI()).thenReturn("/");
+        filterChain = new MockFilterChain();
+
+        threadFactory = new JoinableThreadFactory();
+        SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
+        executor.setThreadFactory(threadFactory);
+
+        asyncManager = WebAsyncUtils.getAsyncManager(request);
+        asyncManager.setAsyncWebRequest(asyncWebRequest);
+        asyncManager.setTaskExecutor(executor);
+        when(request.getAttribute(WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE)).thenReturn(asyncManager);
+
+        filter = new WebAsyncManagerIntegrationFilter();
+    }
+
+    @After
+    public void clearSecurityContext() {
+        SecurityContextHolder.clearContext();
+    }
+
+    @Test
+    public void doFilterInternalRegistersSecurityContextCallableProcessor() throws Exception {
+        SecurityContextHolder.setContext(securityContext);
+        filter.doFilterInternal(request, response, filterChain);
+
+        VerifyingCallable verifyingCallable = new VerifyingCallable();
+        asyncManager.startCallableProcessing(verifyingCallable);
+        threadFactory.join();
+        assertThat(asyncManager.getConcurrentResult()).isSameAs(securityContext);
+    }
+
+    @Test
+    public void doFilterInternalRegistersSecurityContextCallableProcessorContextUpdated() throws Exception {
+        SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
+        filter.doFilterInternal(request, response, filterChain);
+        SecurityContextHolder.setContext(securityContext);
+
+        VerifyingCallable verifyingCallable = new VerifyingCallable();
+        asyncManager.startCallableProcessing(verifyingCallable);
+        threadFactory.join();
+        assertThat(asyncManager.getConcurrentResult()).isSameAs(securityContext);
+    }
+
+    private static final class JoinableThreadFactory implements ThreadFactory {
+        private Thread t;
+
+        public Thread newThread(Runnable r) {
+            t =  new Thread(r);
+            return t;
+        }
+
+        public void join() throws InterruptedException {
+            t.join();
+        }
+    }
+
+    private class VerifyingCallable implements Callable<SecurityContext> {
+
+        public SecurityContext call() throws Exception {
+            return SecurityContextHolder.getContext();
+        }
+
+    }
+}

+ 1 - 1
web/web.gradle

@@ -9,7 +9,7 @@ dependencies {
             "org.springframework:spring-tx:$springVersion",
             "org.springframework:spring-web:$springVersion"
 
-    provided 'javax.servlet:servlet-api:2.5'
+    provided "org.apache.tomcat:tomcat-servlet-api:$servletApiVersion"
 
     testCompile project(':spring-security-core').sourceSets.test.output,
                 'commons-codec:commons-codec:1.3',