SEC-2111: Disable auto save of SecurityContext when response committed after startAsync invoked

Previously Spring Security would disable automatically saving the
SecurityContext when the Thread was different than the Thread that
created the SaveContextOnUpdateOrErrorResponseWrapper. This worked for
many cases, but could cause issues when a timeout occurred. The problem
is that a Thread can be reused to process the timeout since the Threads
are pooled. This means that a timeout of a request trigger an apparent
logout as described in the following workflow:

  - The SecurityContext was established on the SecurityContextHolder
  - An Async request was made
  - The SecurityContextHolder would be cleared out
  - The Async request times out
  - The Async request would be dispatched back to the container upon
    timing out. If the container reused the same Thread to process the
    timeout as the original request, Spring Security would attempt to
    save the SecurityContext when the response was committed. Since the
    SecurityContextHolder was still cleared out it removes the
    SecurityContext from the HttpSession

Spring Security will now prevent the SecurityContext from automatically
being saved when the response is committed as soon as
HttpServletRequest#startAsync() or
ServletRequest#startAsync(ServletRequest,ServletResponse) is called.
This commit is contained in:
Rob Winch
2013-01-10 12:54:56 -06:00
parent 66d13642b7
commit 5f9dfb73be
4 changed files with 139 additions and 114 deletions

View File

@@ -12,16 +12,26 @@
*/
package org.springframework.security.web.context;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import static org.springframework.security.web.context.HttpSessionSecurityContextRepository.*;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Matchers.*;
import static org.powermock.api.mockito.PowerMockito.*;
import javax.servlet.ServletOutputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
@@ -29,11 +39,14 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.ClassUtils;
/**
* @author Luke Taylor
* @author Rob Winch
*/
@RunWith(PowerMockRunner.class)
@PrepareForTest({ClassUtils.class})
public class HttpSessionSecurityContextRepositoryTests {
private final TestingAuthenticationToken testToken = new TestingAuthenticationToken("someone", "passwd", "ROLE_A");
@@ -42,6 +55,53 @@ public class HttpSessionSecurityContextRepositoryTests {
SecurityContextHolder.clearContext();
}
@Test
public void servlet25Compatability() throws Exception {
spy(ClassUtils.class);
when(ClassUtils.class,"hasMethod", ServletRequest.class, "startAsync", new Class[] {}).thenReturn(false);
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
repo.loadContext(holder);
assertThat(holder.getRequest()).isSameAs(request);
}
@Test
public void startAsyncDisablesSaveOnCommit() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
HttpServletRequest request = mock(HttpServletRequest.class);
MockHttpServletResponse response = new MockHttpServletResponse();
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
repo.loadContext(holder);
reset(request);
holder.getRequest().startAsync();
holder.getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST);
// ensure that sendError did cause interaction with the HttpSession
verify(request, never()).getSession(anyBoolean());
verify(request, never()).getSession();
}
@Test
public void startAsyncRequestResponseDisablesSaveOnCommit() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
HttpServletRequest request = mock(HttpServletRequest.class);
MockHttpServletResponse response = new MockHttpServletResponse();
HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
repo.loadContext(holder);
reset(request);
holder.getRequest().startAsync(request,response);
holder.getResponse().sendError(HttpServletResponse.SC_BAD_REQUEST);
// ensure that sendError did cause interaction with the HttpSession
verify(request, never()).getSession(anyBoolean());
verify(request, never()).getSession();
}
@Test
public void sessionIsntCreatedIfContextDoesntChange() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();

View File

@@ -14,8 +14,6 @@ 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;
@@ -63,19 +61,10 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@Test
public void sendErrorSkipsSaveSecurityContextOnNewThread() throws Exception {
public void sendErrorSkipsSaveSecurityContextDisables() 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();
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.sendError(error);
assertThat(wrappedResponse.securityContext).isNull();
assertThat(response.getStatus()).isEqualTo(error);
}
@@ -91,20 +80,11 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@Test
public void sendErrorWithMessageSkipsSaveSecurityContextOnNewThread() throws Exception {
public void sendErrorWithMessageSkipsSaveSecurityContextDisables() 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();
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.sendError(error, message);
assertThat(wrappedResponse.securityContext).isNull();
assertThat(response.getStatus()).isEqualTo(error);
assertThat(response.getErrorMessage()).isEqualTo(message);
@@ -119,19 +99,10 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@Test
public void sendRedirectSkipsSaveSecurityContextOnNewThread() throws Exception {
public void sendRedirectSkipsSaveSecurityContextDisables() 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();
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.sendRedirect(url);
assertThat(wrappedResponse.securityContext).isNull();
assertThat(response.getRedirectedUrl()).isEqualTo(url);
}
@@ -143,18 +114,9 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@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();
public void outputFlushSkipsSaveSecurityContextDisables() throws Exception {
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.getOutputStream().flush();
assertThat(wrappedResponse.securityContext).isNull();
}
@@ -165,18 +127,9 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@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();
public void outputCloseSkipsSaveSecurityContextDisables() throws Exception {
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.getOutputStream().close();
assertThat(wrappedResponse.securityContext).isNull();
}
@@ -187,18 +140,9 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@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();
public void writerFlushSkipsSaveSecurityContextDisables() throws Exception {
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.getWriter().flush();
assertThat(wrappedResponse.securityContext).isNull();
}
@@ -209,18 +153,9 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@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();
public void writerCloseSkipsSaveSecurityContextDisables() throws Exception {
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.getWriter().close();
assertThat(wrappedResponse.securityContext).isNull();
}
@@ -231,18 +166,9 @@ public class SaveContextOnUpdateOrErrorResponseWrapperTests {
}
@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();
public void flushBufferSkipsSaveSecurityContextDisables() throws Exception {
wrappedResponse.disableSaveOnResponseCommitted();
wrappedResponse.flushBuffer();
assertThat(wrappedResponse.securityContext).isNull();
}