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();