diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java index 1751a699af..0984a942d3 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java @@ -102,7 +102,8 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { /** * Enables mode in which only the HttpServletRequest is modified. This means that * {@link HttpServletResponse#sendRedirect(String)} will only work when the application is configured to use - * relative redirects. This can be done with Servlet Container specific setup. For example, using Tomcat's + * relative redirects. This can be done by placing {@link RelativeRedirectFilter} after this Filter or Servlet + * Container specific setup. For example, using Tomcat's * useRelativeRedirects * attribute. * diff --git a/spring-web/src/main/java/org/springframework/web/filter/RelativeRedirectFilter.java b/spring-web/src/main/java/org/springframework/web/filter/RelativeRedirectFilter.java new file mode 100644 index 0000000000..c2f893d2b2 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/web/filter/RelativeRedirectFilter.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2017 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.web.filter; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.util.Assert; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpServletResponseWrapper; +import java.io.IOException; + +/** + * Overrides the {@link HttpServletResponse#sendRedirect(String)} to set the "Location" header and the HTTP Status + * directly to avoid the Servlet Container from creating an absolute URL. This allows redirects that have a relative + * "Location" that ensures support for RFC 7231 Section + * 7.1.2. It should be noted that while relative redirects are more efficient, they may not work with reverse + * proxies under some configurations. + * + * @author Rob Winch + * @since 4.3.10 + */ +public class RelativeRedirectFilter extends OncePerRequestFilter { + private HttpStatus sendRedirectHttpStatus = HttpStatus.FOUND; + + /** + * Sets the HTTP Status to be used when {@code HttpServletResponse#sendRedirect(String)} is invoked. + * @param sendRedirectHttpStatus the 3xx HTTP Status to be used when + * {@code HttpServletResponse#sendRedirect(String)} is invoked. The default is {@code HttpStatus.FOUND}. + */ + public void setSendRedirectHttpStatus(HttpStatus sendRedirectHttpStatus) { + Assert.notNull(sendRedirectHttpStatus, "HttpStatus is required"); + if(!sendRedirectHttpStatus.is3xxRedirection()) { + throw new IllegalArgumentException("sendRedirectHttpStatus should be for redirection. Got " + sendRedirectHttpStatus); + } + this.sendRedirectHttpStatus = sendRedirectHttpStatus; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + filterChain.doFilter(request, new RelativeRedirectResponse(response)); + } + + /** + * Modifies {@link #sendRedirect(String)} to explicitly set the "Location" header and an HTTP status code to avoid + * containers from rewriting the location to be an absolute URL. + * + * @author Rob Winch + * @since 4.3.10 + */ + private class RelativeRedirectResponse extends HttpServletResponseWrapper { + + /** + * Constructs a response adaptor wrapping the given response. + * + * @param response + * @throws IllegalArgumentException if the response is null + */ + public RelativeRedirectResponse(HttpServletResponse response) { + super(response); + } + + @Override + public void sendRedirect(String location) throws IOException { + setHeader(HttpHeaders.LOCATION, location); + setStatus(sendRedirectHttpStatus.value()); + } + } +} diff --git a/spring-web/src/test/java/org/springframework/web/filter/RelativeRedirectFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/RelativeRedirectFilterTests.java new file mode 100644 index 0000000000..ffcb036033 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/web/filter/RelativeRedirectFilterTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2017 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.web.filter; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.test.MockFilterChain; +import org.springframework.mock.web.test.MockHttpServletRequest; + +import javax.servlet.http.HttpServletResponse; + +/** + * @author Rob Winch + * @since 4.3.10 + */ +@RunWith(MockitoJUnitRunner.class) +public class RelativeRedirectFilterTests { + @Mock + HttpServletResponse response; + + RelativeRedirectFilter filter = new RelativeRedirectFilter(); + + @Test(expected = IllegalArgumentException.class) + public void sendRedirectHttpStatusWhenNullThenIllegalArgumentException() { + this.filter.setSendRedirectHttpStatus(null); + } + + @Test(expected = IllegalArgumentException.class) + public void sendRedirectHttpStatusWhenNot3xxThenIllegalArgumentException() { + this.filter.setSendRedirectHttpStatus(HttpStatus.OK); + } + + @Test + public void doFilterSendRedirectWhenDefaultsThenLocationAnd302() throws Exception { + String location = "/foo"; + + sendRedirect(location); + + InOrder inOrder = Mockito.inOrder(this.response); + inOrder.verify(this.response).setHeader(HttpHeaders.LOCATION, location); + inOrder.verify(this.response).setStatus(HttpStatus.FOUND.value()); + } + + @Test + public void doFilterSendRedirectWhenCustomSendRedirectHttpStatusThenLocationAnd301() throws Exception { + String location = "/foo"; + HttpStatus status = HttpStatus.MOVED_PERMANENTLY; + this.filter.setSendRedirectHttpStatus(status); + sendRedirect(location); + + InOrder inOrder = Mockito.inOrder(this.response); + inOrder.verify(this.response).setHeader(HttpHeaders.LOCATION, location); + inOrder.verify(this.response).setStatus(status.value()); + } + + private void sendRedirect(String location) throws Exception { + MockFilterChain chain = new MockFilterChain(); + + filter.doFilterInternal(new MockHttpServletRequest(), response, chain); + + HttpServletResponse wrappedResponse = (HttpServletResponse) chain.getResponse(); + wrappedResponse.sendRedirect(location); + } +} \ No newline at end of file