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 cd38e7d765..8a4165b3f3 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
@@ -1,7 +1,22 @@
+/*
+ * 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 java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
@@ -10,8 +25,9 @@ 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 with the SecurityContext when a sendError() or sendRedirect
- * happens. See issue SEC-398.
+ * store the SecurityContext when a sendError(), sendRedirect,
+ * getOutputStream().close(), getOutputStream().flush(), getWriter().close(), or
+ * getWriter().flush() happens. See issue SEC-398 and SEC-2005.
*
* Sub-classes should implement the {@link #saveContext(SecurityContext context)} method. *
@@ -19,6 +35,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
*
* @author Luke Taylor
* @author Marten Algesten
+ * @author Rob Winch
* @since 3.0
*/
public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServletResponseWrapper {
@@ -74,6 +91,34 @@ public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServ
super.sendRedirect(location);
}
+ /**
+ * Makes sure the context is stored before calling getOutputStream().close() or
+ * getOutputStream().flush()
+ */
+ @Override
+ public ServletOutputStream getOutputStream() throws IOException {
+ return new SaveContextServletOutputStream(super.getOutputStream());
+ }
+
+ /**
+ * Makes sure the context is stored before calling getWriter().close() or
+ * getWriter().flush()
+ */
+ @Override
+ public PrintWriter getWriter() throws IOException {
+ return new SaveContextPrintWriter(super.getWriter());
+ }
+
+ /**
+ * Makes sure the context is stored before calling the
+ * superclass flushBuffer()
+ */
+ @Override
+ public void flushBuffer() throws IOException {
+ doSaveContext();
+ super.flushBuffer();
+ }
+
/**
* Calls saveContext() with the current contents of the SecurityContextHolder.
*/
@@ -115,10 +160,59 @@ public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServ
}
/**
- * Tells if the response wrapper has called saveContext() because of an error or redirect.
+ * Tells if the response wrapper has called saveContext() because of this wrapper.
*/
public final boolean isContextSaved() {
return contextSaved;
}
+ /**
+ * Ensures the {@link SecurityContext} is updated prior to methods that commit the response.
+ * @author Rob Winch
+ */
+ private class SaveContextPrintWriter extends PrintWriter {
+
+ public SaveContextPrintWriter(Writer out) {
+ super(out);
+ }
+
+ public void flush() {
+ doSaveContext();
+ super.flush();
+ }
+
+ public void close() {
+ doSaveContext();
+ super.close();
+ }
+ }
+
+ /**
+ * Ensures the {@link SecurityContext} is updated prior to methods that commit the response.
+ *
+ * @author Rob Winch
+ */
+ private class SaveContextServletOutputStream extends ServletOutputStream {
+ private final ServletOutputStream delegate;
+
+ public SaveContextServletOutputStream(ServletOutputStream delegate) {
+ this.delegate = delegate;
+ }
+
+ public void write(int b) throws IOException {
+ this.delegate.write(b);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ doSaveContext();
+ super.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+ doSaveContext();
+ super.close();
+ }
+ }
}
diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java
index 76f0bdfcf2..19e61b6060 100644
--- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java
+++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java
@@ -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.web.context;
import static org.junit.Assert.*;
@@ -16,6 +28,10 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
+/**
+ * @author Luke Taylor
+ * @author Rob Winch
+ */
public class HttpSessionSecurityContextRepositoryTests {
private final TestingAuthenticationToken testToken = new TestingAuthenticationToken("someone", "passwd", "ROLE_A");
@@ -151,6 +167,96 @@ public class HttpSessionSecurityContextRepositoryTests {
assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
}
+ // SEC-2005
+ @Test
+ public void flushBufferCausesEarlySaveOfContext() throws Exception {
+ HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+ repo.setSpringSecurityContextKey("imTheContext");
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+ SecurityContextHolder.setContext(repo.loadContext(holder));
+ SecurityContextHolder.getContext().setAuthentication(testToken);
+ holder.getResponse().flushBuffer();
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved());
+ repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+ // Check it's still the same
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ }
+
+ // SEC-2005
+ @Test
+ public void writerFlushCausesEarlySaveOfContext() throws Exception {
+ HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+ repo.setSpringSecurityContextKey("imTheContext");
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+ SecurityContextHolder.setContext(repo.loadContext(holder));
+ SecurityContextHolder.getContext().setAuthentication(testToken);
+ holder.getResponse().getWriter().flush();
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved());
+ repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+ // Check it's still the same
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ }
+
+ // SEC-2005
+ @Test
+ public void writerCloseCausesEarlySaveOfContext() throws Exception {
+ HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+ repo.setSpringSecurityContextKey("imTheContext");
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+ SecurityContextHolder.setContext(repo.loadContext(holder));
+ SecurityContextHolder.getContext().setAuthentication(testToken);
+ holder.getResponse().getWriter().close();
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved());
+ repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+ // Check it's still the same
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ }
+
+ // SEC-2005
+ @Test
+ public void outputStreamFlushCausesEarlySaveOfContext() throws Exception {
+ HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+ repo.setSpringSecurityContextKey("imTheContext");
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+ SecurityContextHolder.setContext(repo.loadContext(holder));
+ SecurityContextHolder.getContext().setAuthentication(testToken);
+ holder.getResponse().getOutputStream().flush();
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved());
+ repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+ // Check it's still the same
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ }
+
+ // SEC-2005
+ @Test
+ public void outputStreamCloseCausesEarlySaveOfContext() throws Exception {
+ HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
+ repo.setSpringSecurityContextKey("imTheContext");
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
+ SecurityContextHolder.setContext(repo.loadContext(holder));
+ SecurityContextHolder.getContext().setAuthentication(testToken);
+ holder.getResponse().getOutputStream().close();
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved());
+ repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse());
+ // Check it's still the same
+ assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
+ }
+
@Test
public void noSessionIsCreatedIfSessionWasInvalidatedDuringTheRequest() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();