diff --git a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc index b17e9b697f4..e012ac6ddcb 100644 --- a/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc +++ b/documentation/spring-boot-docs/src/docs/antora/modules/how-to/pages/webserver.adoc @@ -562,8 +562,9 @@ server: NOTE: You can trust all proxies by setting the `internal-proxies` to empty (but do not do so in production). -TIP: If you are using Tomcat and terminating SSL at the proxy, configprop:server.tomcat.redirect-context-root[] should be set to `false`. +TIP: If you are using Tomcat, terminating SSL at the proxy, and have set configprop:server.tomcat.use-relative-redirects[] to `false`, then configprop:server.tomcat.redirect-context-root[] should also be set to `false`. This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed. +When relative redirects are in use, which is Tomcat's default, the context root redirect carries no scheme so there is nothing for the header to correct. You can take complete control of the configuration of Tomcat's javadoc:org.apache.catalina.valves.RemoteIpValve[] by switching the automatic one off (to do so, set `server.forward-headers-strategy=NONE`) and adding a new valve instance using a javadoc:org.springframework.boot.web.server.WebServerFactoryCustomizer[] bean. diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerProperties.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerProperties.java index 90c6e551820..be1df52d03b 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerProperties.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerProperties.java @@ -56,6 +56,7 @@ * @author Florian Storz * @author Michael Weidmann * @author Lasse Wulff + * @author Tiziano Basile * @since 4.0.0 */ @ConfigurationProperties("server.tomcat") @@ -103,9 +104,11 @@ public class TomcatServerProperties { /** * Whether HTTP 1.1 and later location headers generated by a call to sendRedirect - * will use relative or absolute redirects. + * will use relative or absolute redirects. When not set, Tomcat's own default is + * used, which is relative unless strict servlet compliance is enabled. Has no effect + * on a reactive web server. */ - private boolean useRelativeRedirects; + private @Nullable Boolean useRelativeRedirects; /** * Character encoding to use to decode the URI. @@ -235,11 +238,11 @@ public void setRedirectContextRoot(Boolean redirectContextRoot) { this.redirectContextRoot = redirectContextRoot; } - public boolean isUseRelativeRedirects() { + public @Nullable Boolean getUseRelativeRedirects() { return this.useRelativeRedirects; } - public void setUseRelativeRedirects(boolean useRelativeRedirects) { + public void setUseRelativeRedirects(@Nullable Boolean useRelativeRedirects) { this.useRelativeRedirects = useRelativeRedirects; } diff --git a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizer.java b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizer.java index 7af0d96e6d3..9d8c95bfcba 100644 --- a/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizer.java +++ b/module/spring-boot-tomcat/src/main/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizer.java @@ -29,6 +29,7 @@ * * @author Brian Clozel * @author Phillip Webb + * @author Tiziano Basile */ class TomcatServletWebServerFactoryCustomizer implements WebServerFactoryCustomizer, Ordered { @@ -52,7 +53,9 @@ public void customize(TomcatServletWebServerFactory factory) { if (this.tomcatProperties.getRedirectContextRoot() != null) { customizeRedirectContextRoot(factory, this.tomcatProperties.getRedirectContextRoot()); } - customizeUseRelativeRedirects(factory, this.tomcatProperties.isUseRelativeRedirects()); + if (this.tomcatProperties.getUseRelativeRedirects() != null) { + customizeUseRelativeRedirects(factory, this.tomcatProperties.getUseRelativeRedirects()); + } } private void customizeRedirectContextRoot(ConfigurableTomcatWebServerFactory factory, boolean redirectContextRoot) { diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerPropertiesTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerPropertiesTests.java index e9280187a80..e863068909e 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerPropertiesTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/TomcatServerPropertiesTests.java @@ -45,6 +45,7 @@ * Tests for {@link TomcatServerProperties}. * * @author Andy Wilkinson + * @author Tiziano Basile */ class TomcatServerPropertiesTests { @@ -95,7 +96,7 @@ void testTomcatBinding() { assertThat(this.properties.getBackgroundProcessorDelay()).hasSeconds(10); assertThat(this.properties.getRelaxedPathChars()).containsExactly('|', '<'); assertThat(this.properties.getRelaxedQueryChars()).containsExactly('^', '|'); - assertThat(this.properties.isUseRelativeRedirects()).isTrue(); + assertThat(this.properties.getUseRelativeRedirects()).isTrue(); } @Test @@ -235,8 +236,8 @@ void tomcatInternalProxiesMatchesDefault() { } @Test - void tomcatUseRelativeRedirectsDefaultsToFalse() { - assertThat(this.properties.isUseRelativeRedirects()).isFalse(); + void tomcatUseRelativeRedirectsIsNotSetByDefault() { + assertThat(this.properties.getUseRelativeRedirects()).isNull(); } @Test diff --git a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizerTests.java b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizerTests.java index 2a39dc33b44..576c99f183b 100644 --- a/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizerTests.java +++ b/module/spring-boot-tomcat/src/test/java/org/springframework/boot/tomcat/autoconfigure/servlet/TomcatServletWebServerFactoryCustomizerTests.java @@ -17,6 +17,7 @@ package org.springframework.boot.tomcat.autoconfigure.servlet; import org.apache.catalina.Context; +import org.apache.catalina.core.StandardContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -30,11 +31,16 @@ import org.springframework.test.context.support.TestPropertySourceUtils; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; /** * Tests for {@link TomcatServletWebServerFactoryCustomizer}. * * @author Phillip Webb + * @author Tiziano Basile */ class TomcatServletWebServerFactoryCustomizerTests { @@ -80,14 +86,39 @@ void redirectContextRootCanBeConfigured() { } @Test - void useRelativeRedirectsCanBeConfigured() { + void useRelativeRedirectsWhenNotSetDoesNotCustomizeContext() { + TomcatServletWebServerFactory factory = customizeAndGetFactory(); + Context context = mock(Context.class); + factory.getContextCustomizers().forEach((customizer) -> customizer.customize(context)); + then(context).should(never()).setUseRelativeRedirects(anyBoolean()); + } + + @Test + void useRelativeRedirectsWhenNotSetUsesTomcatsDefault() { + assertThat(this.tomcatProperties.getUseRelativeRedirects()).isNull(); + TomcatWebServer server = customizeAndGetServer(); + Context context = (Context) server.getTomcat().getHost().findChildren()[0]; + assertThat(context.getUseRelativeRedirects()).isEqualTo(new StandardContext().getUseRelativeRedirects()); + } + + @Test + void useRelativeRedirectsCanBeEnabled() { bind("server.tomcat.use-relative-redirects=true"); - assertThat(this.tomcatProperties.isUseRelativeRedirects()).isTrue(); + assertThat(this.tomcatProperties.getUseRelativeRedirects()).isTrue(); TomcatWebServer server = customizeAndGetServer(); Context context = (Context) server.getTomcat().getHost().findChildren()[0]; assertThat(context.getUseRelativeRedirects()).isTrue(); } + @Test + void useRelativeRedirectsCanBeDisabled() { + bind("server.tomcat.use-relative-redirects=false"); + assertThat(this.tomcatProperties.getUseRelativeRedirects()).isFalse(); + TomcatWebServer server = customizeAndGetServer(); + Context context = (Context) server.getTomcat().getHost().findChildren()[0]; + assertThat(context.getUseRelativeRedirects()).isFalse(); + } + private void bind(String... inlinedProperties) { TestPropertySourceUtils.addInlinedPropertiesToEnvironment(this.environment, inlinedProperties); new Binder(ConfigurationPropertySources.get(this.environment)).bind("server.tomcat", diff --git a/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java b/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java index 243b55a2429..b39f6d3c581 100644 --- a/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-oauth2-authorization-server/src/test/java/smoketest/oauth2/server/SampleOAuth2AuthorizationServerApplicationTests.java @@ -112,7 +112,7 @@ void authServerMetadataShouldAllowAccess() { void anonymousShouldRedirectToLogin() { RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange(); response.expectStatus().isFound(); - response.expectHeader().location("http://localhost:" + this.port + "/login"); + response.expectHeader().location("/login"); } @Test @@ -181,7 +181,7 @@ void anonymousTokenRequestWithAcceptHeaderTextHtmlShouldRedirectToLogin() { .body(body) .exchange(); response.expectStatus().isFound(); - response.expectHeader().location("http://localhost:" + this.port + "/login"); + response.expectHeader().location("/login"); } } diff --git a/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java b/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java index 63e68d23806..d1039065b33 100644 --- a/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-oauth2-client/src/test/java/smoketest/oauth2/client/SampleOAuth2ClientApplicationTests.java @@ -53,7 +53,7 @@ private RestTestClient nonFollowingRedirect() { void everythingShouldRedirectToLogin() { RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange(); response.expectStatus().isFound(); - response.expectHeader().location("http://localhost:" + this.port + "/login"); + response.expectHeader().location("/login"); } @Test diff --git a/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java b/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java index e3e130bfbb9..188ff44b384 100644 --- a/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-saml2-service-provider/src/test/java/smoketest/saml2/serviceprovider/SampleSaml2RelyingPartyApplicationTests.java @@ -51,7 +51,7 @@ private RestTestClient nonFollowingRedirect() { void everythingShouldRedirectToLogin() { RestTestClient.ResponseSpec response = nonFollowingRedirect().get().uri("/").exchange(); response.expectStatus().isFound(); - response.expectHeader().location("http://localhost:" + this.port + "/login"); + response.expectHeader().location("/login"); } @Test diff --git a/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java index 141b4983855..ac26a57d60e 100644 --- a/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-groovy-templates/src/test/java/smoketest/groovytemplates/SampleGroovyTemplateApplicationTests.java @@ -22,7 +22,6 @@ import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -38,9 +37,6 @@ @AutoConfigureRestTestClient class SampleGroovyTemplateApplicationTests { - @LocalServerPort - private int port; - @Autowired private RestTestClient restTestClient; @@ -62,7 +58,7 @@ void testCreate() { .body(map) .exchange() .expectHeader() - .value("Location", (location) -> assertThat(location).contains("localhost:" + this.port)); + .value("Location", (location) -> assertThat(location).matches("/\\d+(;jsessionid=[\\w.]+)?")); } @Test diff --git a/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java index 552182ae9b0..873bb201250 100644 --- a/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-method-security/src/test/java/smoketest/security/method/SampleMethodSecurityApplicationTests.java @@ -91,7 +91,7 @@ void testLogin() { assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).endsWith(this.port + "/"); + assertThat(location.toString()).isEqualTo("/"); } @Test diff --git a/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java index b16c34ab450..565a71cd1cc 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure-custom/src/test/java/smoketest/web/secure/custom/SampleWebSecureCustomApplicationTests.java @@ -71,7 +71,7 @@ void testHome() { assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).endsWith(this.port + "/login"); + assertThat(location.toString()).isEqualTo("/login"); } @Test @@ -99,7 +99,7 @@ void testLogin() { assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).endsWith(this.port + "/"); + assertThat(location.toString()).isEqualTo("/"); } } diff --git a/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java index 82ba02cf198..799aa10a703 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure-jdbc/src/test/java/smoketest/web/secure/jdbc/SampleWebSecureJdbcApplicationTests.java @@ -71,7 +71,7 @@ void testHome() { assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).endsWith(this.port + "/login"); + assertThat(location.toString()).isEqualTo("/login"); } @Test @@ -99,7 +99,7 @@ void testLogin() { assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).endsWith(this.port + "/"); + assertThat(location.toString()).isEqualTo("/"); } } diff --git a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java index 781d02fed73..c31a55fa650 100644 --- a/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-secure/src/test/java/smoketest/web/secure/SampleWebSecureApplicationTests.java @@ -78,7 +78,7 @@ void testHome() { assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).endsWith(this.port + "/login"); + assertThat(location.toString()).isEqualTo("/login"); } @Test @@ -106,7 +106,7 @@ void testLogin() { assertThat(result.getStatus()).isEqualTo(HttpStatus.FOUND); URI location = result.getResponseHeaders().getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).endsWith(this.port + "/"); + assertThat(location.toString()).isEqualTo("/"); } @org.springframework.boot.test.context.TestConfiguration(proxyBeanMethods = false) diff --git a/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java b/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java index 8c21af9d78a..64f8f8e5714 100644 --- a/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java +++ b/smoke-test/spring-boot-smoke-test-web-thymeleaf/src/test/java/smoketest/web/thymeleaf/SampleWebUiApplicationTests.java @@ -24,7 +24,6 @@ import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.test.web.servlet.client.RestTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -43,9 +42,6 @@ class SampleWebUiApplicationTests { @Autowired private RestTestClient restTestClient; - @LocalServerPort - private int port; - @Test void testHome() { this.restTestClient.get().uri("/").exchangeSuccessfully().expectBody(String.class).value((body) -> { @@ -67,7 +63,7 @@ void testCreate() { .getResponseHeaders() .getLocation(); assertThat(location).isNotNull(); - assertThat(location.toString()).contains("localhost:" + this.port); + assertThat(location.toString()).matches("/\\d+(;jsessionid=[\\w.]+)?"); } }