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:
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user