diff --git a/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java b/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java
index 158169fbc7..e60f3bbad0 100644
--- a/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java
+++ b/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java
@@ -20,14 +20,17 @@ import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
/**
- * Base class for response wrappers which encapsulate the logic for storing a security context and which
- * store the SecurityContext when a sendError(), sendRedirect,
+ * Base class for response wrappers which encapsulate the logic for storing a security context and which store the
+ * SecurityContext when a sendError(), sendRedirect,
* getOutputStream().close(), getOutputStream().flush(), getWriter().close(), or
- * getWriter().flush() happens. See issue SEC-398 and SEC-2005.
+ * getWriter().flush() happens on the same thread that this
+ * {@link SaveContextOnUpdateOrErrorResponseWrapper} was created. See issue SEC-398 and SEC-2005.
*
* Sub-classes should implement the {@link #saveContext(SecurityContext context)} method. *
@@ -39,6 +42,9 @@ import org.springframework.security.core.context.SecurityContextHolder;
* @since 3.0
*/
public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServletResponseWrapper {
+ private final Log logger = LogFactory.getLog(getClass());
+
+ private final Thread SUPPORTED_THREAD = Thread.currentThread();
private boolean contextSaved = false;
/* See SEC-1052 */
@@ -120,11 +126,19 @@ public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServ
}
/**
- * Calls saveContext() with the current contents of the SecurityContextHolder.
+ * Calls saveContext() with the current contents of the SecurityContextHolder as long as
+ * {@link #doSaveContext()} is invoked on the same Thread that {@link SaveContextOnUpdateOrErrorResponseWrapper} was
+ * created on. This prevents issues when dealing with Async Web Requests where the {@link SecurityContext} is not
+ * present on the Thread that processes the response.
*/
private void doSaveContext() {
- saveContext(SecurityContextHolder.getContext());
- contextSaved = true;
+ Thread currentThread = Thread.currentThread();
+ if(SUPPORTED_THREAD == currentThread) {
+ saveContext(SecurityContextHolder.getContext());
+ contextSaved = true;
+ } else if(logger.isDebugEnabled()){
+ logger.debug("Skip saving SecurityContext since processing the HttpServletResponse on a different Thread than the original HttpServletRequest");
+ }
}
@Override
diff --git a/web/src/test/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapperTests.java b/web/src/test/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapperTests.java
new file mode 100644
index 0000000000..2f39db55ac
--- /dev/null
+++ b/web/src/test/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapperTests.java
@@ -0,0 +1,261 @@
+/*
+ * 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;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+import java.io.IOException;
+
+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.mock.web.MockHttpServletResponse;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+
+/**
+ *
+ * @author Rob Winch
+ *
+ */
+
+@RunWith(MockitoJUnitRunner.class)
+public class SaveContextOnUpdateOrErrorResponseWrapperTests {
+ @Mock
+ private SecurityContext securityContext;
+
+ private MockHttpServletResponse response;
+ private SaveContextOnUpdateOrErrorResponseWrapperStub wrappedResponse;
+
+ @Before
+ public void setUp() {
+ response = new MockHttpServletResponse();
+ wrappedResponse = new SaveContextOnUpdateOrErrorResponseWrapperStub(response, true);
+ SecurityContextHolder.setContext(securityContext);
+ }
+
+ @After
+ public void clearContext() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ public void sendErrorSavesSecurityContext() throws Exception {
+ int error = HttpServletResponse.SC_FORBIDDEN;
+ wrappedResponse.sendError(error);
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ assertThat(response.getStatus()).isEqualTo(error);
+ }
+
+ @Test
+ public void sendErrorSkipsSaveSecurityContextOnNewThread() throws Exception {
+ final int error = HttpServletResponse.SC_FORBIDDEN;
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.sendError(error);
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ assertThat(response.getStatus()).isEqualTo(error);
+ }
+
+ @Test
+ public void sendErrorWithMessageSavesSecurityContext() throws Exception {
+ int error = HttpServletResponse.SC_FORBIDDEN;
+ String message = "Forbidden";
+ wrappedResponse.sendError(error, message);
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ assertThat(response.getStatus()).isEqualTo(error);
+ assertThat(response.getErrorMessage()).isEqualTo(message);
+ }
+
+ @Test
+ public void sendErrorWithMessageSkipsSaveSecurityContextOnNewThread() throws Exception {
+ final int error = HttpServletResponse.SC_FORBIDDEN;
+ final String message = "Forbidden";
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.sendError(error, message);
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ assertThat(response.getStatus()).isEqualTo(error);
+ assertThat(response.getErrorMessage()).isEqualTo(message);
+ }
+
+ @Test
+ public void sendRedirectSavesSecurityContext() throws Exception {
+ String url = "/location";
+ wrappedResponse.sendRedirect(url);
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ assertThat(response.getRedirectedUrl()).isEqualTo(url);
+ }
+
+ @Test
+ public void sendRedirectSkipsSaveSecurityContextOnNewThread() throws Exception {
+ final String url = "/location";
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.sendRedirect(url);
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ assertThat(response.getRedirectedUrl()).isEqualTo(url);
+ }
+
+ @Test
+ public void outputFlushSavesSecurityContext() throws Exception {
+ wrappedResponse.getOutputStream().flush();
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ }
+
+ @Test
+ public void outputFlushSkipsSaveSecurityContextOnNewThread() throws Exception {
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.getOutputStream().flush();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ }
+
+ @Test
+ public void outputCloseSavesSecurityContext() throws Exception {
+ wrappedResponse.getOutputStream().close();
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ }
+
+ @Test
+ public void outputCloseSkipsSaveSecurityContextOnNewThread() throws Exception {
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.getOutputStream().close();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ }
+
+ @Test
+ public void writerFlushSavesSecurityContext() throws Exception {
+ wrappedResponse.getWriter().flush();
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ }
+
+ @Test
+ public void writerFlushSkipsSaveSecurityContextOnNewThread() throws Exception {
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.getWriter().flush();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ }
+
+ @Test
+ public void writerCloseSavesSecurityContext() throws Exception {
+ wrappedResponse.getWriter().close();
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ }
+
+ @Test
+ public void writerCloseSkipsSaveSecurityContextOnNewThread() throws Exception {
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.getWriter().close();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ }
+
+ @Test
+ public void flushBufferSavesSecurityContext() throws Exception {
+ wrappedResponse.flushBuffer();
+ assertThat(wrappedResponse.securityContext).isEqualTo(securityContext);
+ }
+
+ @Test
+ public void flushBufferSkipsSaveSecurityContextOnNewThread() throws Exception {
+ Thread t = new Thread() {
+ public void run() {
+ try {
+ wrappedResponse.flushBuffer();
+ } catch(IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ };
+ t.start();
+ t.join();
+ assertThat(wrappedResponse.securityContext).isNull();
+ }
+
+ private static class SaveContextOnUpdateOrErrorResponseWrapperStub extends SaveContextOnUpdateOrErrorResponseWrapper {
+ private SecurityContext securityContext;
+
+ public SaveContextOnUpdateOrErrorResponseWrapperStub(HttpServletResponse response, boolean disableUrlRewriting) {
+ super(response, disableUrlRewriting);
+ }
+
+ @Override
+ protected void saveContext(SecurityContext context) {
+ securityContext = context;
+ }
+ }
+}