mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Update Forwarded headers support for Framework
Prior to this commit, Spring Boot would auto-configure the `ForwardedHeaderFilter`/`ForwardedHeaderTransformer` when the "NATIVE" strategy is chosen. Spring Framework now requires an explicit choice between the supported HTTP header variants as of spring-projects/spring-framework#37072. This commit adapts to this new behavior with the following: * the "FRAMEWORK" strategy now only applies to Spring MVC and Spring WebFlux applications, since "NATIVE" strategies are now a good choice for most deployments. * the format of HTTP headers is now configured with `spring.mvc.forwarded-headers.header-format` and `spring.webflux.forwarded-headers.header-format`, with additional options. The default header format is now "X-Forwarded-*" for both NATIVE and FRAMEWORK strategies. The reference documentation also reflects those changes. Closes gh-51030
This commit is contained in:
@@ -498,18 +498,36 @@ For more details, see the Jetty documentation.
|
||||
If your application is running behind a proxy, a load-balancer or in the cloud, the request information (like the host, port, scheme...) might change along the way.
|
||||
Your application may be running on `10.10.10.10:8080`, but HTTP clients should only see `example.org`.
|
||||
|
||||
https://tools.ietf.org/html/rfc7239[RFC7239 "Forwarded Headers"] defines the `Forwarded` HTTP header; proxies can use this header to provide information about the original request.
|
||||
You can configure your application to read those headers and automatically use that information when creating links and sending them to clients in HTTP 302 responses, JSON documents or HTML pages.
|
||||
There are also non-standard headers, like `X-Forwarded-Host`, `X-Forwarded-Port`, `X-Forwarded-Proto`, `X-Forwarded-Ssl`, and `X-Forwarded-Prefix`.
|
||||
There are two sets of HTTP headers that intermediaries can use to provide information about the original request:
|
||||
|
||||
If the proxy adds the commonly used `X-Forwarded-For` and `X-Forwarded-Proto` headers, setting `server.forward-headers-strategy` to `NATIVE` is enough to support those.
|
||||
With this option, the Web servers themselves natively support this feature; you can check their specific documentation to learn about specific behavior.
|
||||
1. the well-known `"X-Forwarded-*"` headers (like `"X-Forwarded-Host"`, `"X-Forwarded-Port"`, `"X-Forwarded-Proto"`, `"X-Forwarded-For"`)
|
||||
2. the `"Forwarded"` header, as defined by https://tools.ietf.org/html/rfc7239[RFC7239 "Forwarded Headers"]
|
||||
|
||||
If this is not enough, Spring Framework provides a {url-spring-framework-docs}/web/webmvc/filters.html#filters-forwarded-headers[ForwardedHeaderFilter] for the servlet stack and a {url-spring-framework-docs}/web/webflux/reactive-spring.html#webflux-forwarded-headers[ForwardedHeaderTransformer] for the reactive stack.
|
||||
You can use them in your application by setting configprop:server.forward-headers-strategy[] to `FRAMEWORK`.
|
||||
As a first step, application developers need to look up which set of headers is supported by their proxy, load-balancer or cloud platform.
|
||||
While RFC7239 is a standard, its adoption is quite low in the industry and the well-known `"X-Forwarded-*"` headers are probably the ones you will need.
|
||||
You can then configure your application to read those headers and automatically use that information when creating links and sending them to clients in HTTP 302 responses, JSON documents or HTML pages.
|
||||
|
||||
TIP: If you are using Tomcat and terminating SSL at the proxy, configprop:server.tomcat.redirect-context-root[] should be set to `false`.
|
||||
This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed.
|
||||
If your web server of choice supports the set of HTTP headers you need, setting `server.forward-headers-strategy` to `NATIVE` is a good choice:
|
||||
|
||||
|===
|
||||
| Server | Support | Also see
|
||||
|
||||
| Tomcat
|
||||
| `"X-Forwarded-*"`
|
||||
| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.tomcat[]
|
||||
|
||||
| Jetty
|
||||
| `"X-Forwarded-*"`
|
||||
|
|
||||
|
||||
| Reactor Netty
|
||||
| `"X-Forwarded-*"`
|
||||
|
|
||||
|
||||
|===
|
||||
|
||||
If this is not enough, Spring Framework provides a {url-spring-framework-docs}/web/webmvc/filters.html#filters-forwarded-headers[ForwardedHeaderFilter] for Spring MVC apps and a {url-spring-framework-docs}/web/webflux/reactive-spring.html#webflux-forwarded-headers[ForwardedHeaderTransformer] for WebFlux apps.
|
||||
You can use them in your application by setting configprop:server.forward-headers-strategy[] to `FRAMEWORK` and select the appropriate variant with configprop:spring.mvc.forwarded-headers.header-format[] or configprop:spring.webflux.forwarded-headers.header-format[].
|
||||
|
||||
NOTE: If your application runs javadoc:org.springframework.boot.cloud.CloudPlatform#enum-constant-summary[in a supported Cloud Platform], the configprop:server.forward-headers-strategy[] property defaults to `NATIVE`.
|
||||
In all other instances, it defaults to `NONE`.
|
||||
@@ -544,6 +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`.
|
||||
This allows the `X-Forwarded-Proto` header to be honored before any redirects are performed.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
|
||||
-9
@@ -25,7 +25,6 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.boot.autoconfigure.condition.SearchStrategy;
|
||||
@@ -36,8 +35,6 @@ import org.springframework.boot.tomcat.TomcatProtocolHandlerCustomizer;
|
||||
import org.springframework.boot.tomcat.autoconfigure.TomcatServerProperties;
|
||||
import org.springframework.boot.tomcat.autoconfigure.TomcatWebServerConfiguration;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer;
|
||||
import org.springframework.boot.web.server.autoconfigure.servlet.ServletWebServerConfiguration;
|
||||
import org.springframework.boot.web.server.servlet.ServletWebServerFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
@@ -82,10 +79,4 @@ public final class TomcatServletWebServerAutoConfiguration {
|
||||
return new TomcatServletWebServerFactoryCustomizer(tomcatProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
|
||||
ForwardedHeaderFilterCustomizer tomcatForwardedHeaderFilterCustomizer(ServerProperties serverProperties) {
|
||||
return (filter) -> filter.setRelativeRedirects(this.tomcatProperties.isUseRelativeRedirects());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-27
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.boot.tomcat.autoconfigure.servlet;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import org.apache.catalina.Context;
|
||||
import org.apache.catalina.connector.Connector;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -27,10 +26,8 @@ import org.springframework.boot.tomcat.TomcatProtocolHandlerCustomizer;
|
||||
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
|
||||
import org.springframework.boot.web.server.autoconfigure.servlet.AbstractServletWebServerAutoConfigurationTests;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -120,30 +117,6 @@ class TomcatServletWebServerAutoConfigurationTests extends AbstractServletWebSer
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenUsingFrameworkForwardHeadersStrategyAndRelativeRedirectsAreEnabledThenFilterIsConfiguredToUseRelativeRedirects() {
|
||||
this.serverRunner
|
||||
.withPropertyValues("server.forward-headers-strategy=framework",
|
||||
"server.tomcat.use-relative-redirects=true", "server.port=0")
|
||||
.run((context) -> {
|
||||
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
|
||||
assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class);
|
||||
assertThat(filter).extracting("relativeRedirects").isEqualTo(true);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenUsingFrameworkForwardHeadersStrategyAndNotUsingRelativeRedirectsThenFilterIsNotConfiguredToUseRelativeRedirects() {
|
||||
this.serverRunner
|
||||
.withPropertyValues("server.forward-headers-strategy=framework",
|
||||
"server.tomcat.use-relative-redirects=false", "server.port=0")
|
||||
.run((context) -> {
|
||||
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
|
||||
assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class);
|
||||
assertThat(filter).extracting("relativeRedirects").isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class TomcatConnectorCustomizerConfiguration {
|
||||
|
||||
|
||||
-11
@@ -25,8 +25,6 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostProcessor;
|
||||
@@ -37,7 +35,6 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
|
||||
/**
|
||||
* {@link Configuration Configuration} for a reactive web server.
|
||||
@@ -57,14 +54,6 @@ public class ReactiveWebServerConfiguration {
|
||||
return new ReactiveWebServerFactoryCustomizer(serverProperties, sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
|
||||
@SuppressWarnings("removal") // gh-51030
|
||||
ForwardedHeaderTransformer forwardedHeaderTransformer() {
|
||||
return new ForwardedHeaderTransformer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
|
||||
* {@link ImportBeanDefinitionRegistrar} for early registration.
|
||||
|
||||
+3
@@ -23,7 +23,10 @@ import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @since 4.0.0
|
||||
* @deprecated since 4.2.0 for removal in 4.4.0 in favor of
|
||||
* {@code org.springframework.boot.webmvc.autoconfigure.ForwardedHeaderFilterCustomizer}.
|
||||
*/
|
||||
@Deprecated(since = "4.2.0", forRemoval = true)
|
||||
public interface ForwardedHeaderFilterCustomizer {
|
||||
|
||||
/**
|
||||
|
||||
-20
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.boot.web.server.autoconfigure.servlet;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
@@ -26,8 +25,6 @@ import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.ssl.SslBundles;
|
||||
import org.springframework.boot.web.error.ErrorPageRegistrarBeanPostProcessor;
|
||||
@@ -35,15 +32,12 @@ import org.springframework.boot.web.server.WebServerFactoryCustomizerBeanPostPro
|
||||
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.web.server.servlet.CookieSameSiteSupplier;
|
||||
import org.springframework.boot.web.server.servlet.WebListenerRegistrar;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
/**
|
||||
* {@link Configuration Configuration} for a servlet web server.
|
||||
@@ -69,20 +63,6 @@ public class ServletWebServerConfiguration {
|
||||
cookieSameSiteSuppliers.orderedStream().toList(), sslBundles.getIfAvailable());
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
|
||||
@ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class)
|
||||
@SuppressWarnings("removal") // gh-51030
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter(
|
||||
ObjectProvider<ForwardedHeaderFilterCustomizer> customizerProvider) {
|
||||
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
|
||||
customizerProvider.ifAvailable((customizer) -> customizer.customize(filter));
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter);
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
|
||||
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
return registration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a {@link WebServerFactoryCustomizerBeanPostProcessor}. Registered via
|
||||
* {@link ImportBeanDefinitionRegistrar} for early registration.
|
||||
|
||||
-34
@@ -32,7 +32,6 @@ import org.springframework.context.ApplicationContextException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
@@ -112,28 +111,6 @@ public abstract class AbstractReactiveWebServerAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerShouldBeConfigured() {
|
||||
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerWhenStrategyNotFilterShouldNotBeConfigured() {
|
||||
this.mockServerRunner.withUserConfiguration(HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=native", "server.port=0")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerWhenAlreadyRegisteredShouldBackOff() {
|
||||
this.mockServerRunner
|
||||
.withUserConfiguration(ForwardedHeaderTransformerConfiguration.class, HttpHandlerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework", "server.port=0")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class HttpHandlerConfiguration {
|
||||
|
||||
@@ -174,15 +151,4 @@ public abstract class AbstractReactiveWebServerAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderTransformerConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("removal") // gh-51030
|
||||
ForwardedHeaderTransformer testForwardedHeaderTransformer() {
|
||||
return new ForwardedHeaderTransformer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-36
@@ -54,7 +54,6 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.RestClient.RequestHeadersSpec;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -115,29 +114,6 @@ public abstract class AbstractServletWebServerAutoConfigurationTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterShouldBeConfigured() {
|
||||
this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> {
|
||||
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
|
||||
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
|
||||
assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class);
|
||||
assertThat(filter).extracting("relativeRedirects").isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterWhenStrategyNotFilterShouldNotBeConfigured() {
|
||||
this.mockServerRunner.withPropertyValues("server.forward-headers-strategy=native")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterWhenFilterAlreadyRegisteredShouldBackOff() {
|
||||
this.mockServerRunner.withUserConfiguration(ForwardedHeaderFilterConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework")
|
||||
.run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void cookieSameSiteSuppliersAreApplied() {
|
||||
this.mockServerRunner.withUserConfiguration(CookieSameSiteSupplierConfiguration.class).run((context) -> {
|
||||
@@ -208,18 +184,6 @@ public abstract class AbstractServletWebServerAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("removal") // gh-51030
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() {
|
||||
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
|
||||
return new FilterRegistrationBean<>(filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CookieSameSiteSupplierConfiguration {
|
||||
|
||||
|
||||
+15
@@ -36,6 +36,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
@@ -57,6 +58,8 @@ import org.springframework.boot.web.server.autoconfigure.ServerProperties;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversion;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Apiversion.Use;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Format;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.Forwardedheaders;
|
||||
import org.springframework.boot.webflux.autoconfigure.WebFluxProperties.HeaderFormat;
|
||||
import org.springframework.boot.webflux.filter.OrderedHiddenHttpMethodFilter;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.EmbeddedValueResolverAware;
|
||||
@@ -100,6 +103,7 @@ import org.springframework.web.reactive.result.method.annotation.RequestMappingH
|
||||
import org.springframework.web.reactive.result.method.annotation.ResponseEntityExceptionHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
|
||||
import org.springframework.web.server.i18n.FixedLocaleContextResolver;
|
||||
@@ -413,6 +417,17 @@ public final class WebFluxAutoConfiguration {
|
||||
return webSessionManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
|
||||
ForwardedHeaderTransformer forwardedHeaderTransformer() {
|
||||
Forwardedheaders properties = this.webFluxProperties.getForwardedHeaders();
|
||||
ForwardedHeaderTransformer transformer = new ForwardedHeaderTransformer(
|
||||
properties.getHeaderFormat() == HeaderFormat.STANDARD);
|
||||
transformer.setUseForwardedPrefix(properties.isUseForwardedPrefix());
|
||||
return transformer;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ConditionalOnMissingBean(name = "webFluxApiVersionStrategy")
|
||||
public @Nullable ApiVersionStrategy webFluxApiVersionStrategy() {
|
||||
|
||||
+56
@@ -48,6 +48,8 @@ public class WebFluxProperties {
|
||||
|
||||
private final Apiversion apiversion = new Apiversion();
|
||||
|
||||
private final Forwardedheaders forwardedHeaders = new Forwardedheaders();
|
||||
|
||||
/**
|
||||
* Path pattern used for static resources.
|
||||
*/
|
||||
@@ -99,6 +101,10 @@ public class WebFluxProperties {
|
||||
return this.apiversion;
|
||||
}
|
||||
|
||||
public Forwardedheaders getForwardedHeaders() {
|
||||
return this.forwardedHeaders;
|
||||
}
|
||||
|
||||
public String getStaticPathPattern() {
|
||||
return this.staticPathPattern;
|
||||
}
|
||||
@@ -308,4 +314,54 @@ public class WebFluxProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwarded headers.
|
||||
*/
|
||||
public static class Forwardedheaders {
|
||||
|
||||
/**
|
||||
* Format of the forwarded headers to support.
|
||||
*/
|
||||
private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED;
|
||||
|
||||
/**
|
||||
* Whether to use the "X-Forwarded-Prefix" header to determine the context path.
|
||||
*/
|
||||
private boolean useForwardedPrefix;
|
||||
|
||||
public HeaderFormat getHeaderFormat() {
|
||||
return this.headerFormat;
|
||||
}
|
||||
|
||||
public void setHeaderFormat(HeaderFormat headerFormat) {
|
||||
this.headerFormat = headerFormat;
|
||||
}
|
||||
|
||||
public boolean isUseForwardedPrefix() {
|
||||
return this.useForwardedPrefix;
|
||||
}
|
||||
|
||||
public void setUseForwardedPrefix(boolean useForwardedPrefix) {
|
||||
this.useForwardedPrefix = useForwardedPrefix;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats of forwarded headers supported by {@link Forwardedheaders}.
|
||||
*/
|
||||
public enum HeaderFormat {
|
||||
|
||||
/**
|
||||
* Use the standard "Forwarded" header, as defined by RFC 7239.
|
||||
*/
|
||||
STANDARD,
|
||||
|
||||
/**
|
||||
* Use the non-standard "X-Forwarded-*" headers.
|
||||
*/
|
||||
X_FORWARDED
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+52
@@ -123,6 +123,7 @@ import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
|
||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import org.springframework.web.server.WebSession;
|
||||
import org.springframework.web.server.adapter.ForwardedHeaderTransformer;
|
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
|
||||
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
|
||||
import org.springframework.web.server.i18n.FixedLocaleContextResolver;
|
||||
@@ -474,6 +475,47 @@ class WebFluxAutoConfigurationTests {
|
||||
.run((context) -> assertThat(context).hasSingleBean(OrderedHiddenHttpMethodFilter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerIsNotConfiguredByDefault() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerIsNotConfiguredWhenStrategyIsNotFramework() {
|
||||
this.contextRunner.withPropertyValues("server.forward-headers-strategy=native")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerIsConfiguredWhenFrameworkStrategyIsUsed() {
|
||||
this.contextRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> {
|
||||
assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class);
|
||||
ForwardedHeaderTransformer transformer = context.getBean(ForwardedHeaderTransformer.class);
|
||||
assertThat(transformer).extracting("useStandardHeader").isEqualTo(false);
|
||||
assertThat(transformer).extracting("useForwardedPrefix").isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerAppliesConfiguredProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("server.forward-headers-strategy=framework",
|
||||
"spring.webflux.forwarded-headers.header-format=standard",
|
||||
"spring.webflux.forwarded-headers.use-forwarded-prefix=true")
|
||||
.run((context) -> {
|
||||
ForwardedHeaderTransformer transformer = context.getBean(ForwardedHeaderTransformer.class);
|
||||
assertThat(transformer).extracting("useStandardHeader").isEqualTo(true);
|
||||
assertThat(transformer).extracting("useForwardedPrefix").isEqualTo(true);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderTransformerBacksOffWhenBeanAlreadyRegistered() {
|
||||
this.contextRunner.withUserConfiguration(ForwardedHeaderTransformerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework")
|
||||
.run((context) -> assertThat(context).hasSingleBean(ForwardedHeaderTransformer.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void customRequestMappingHandlerMapping() {
|
||||
this.contextRunner.withUserConfiguration(CustomRequestMappingHandlerMapping.class).run((context) -> {
|
||||
@@ -1144,6 +1186,16 @@ class WebFluxAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderTransformerConfiguration {
|
||||
|
||||
@Bean
|
||||
ForwardedHeaderTransformer testForwardedHeaderTransformer() {
|
||||
return new ForwardedHeaderTransformer(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomRequestMappingHandlerAdapter {
|
||||
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2012-present 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
|
||||
*
|
||||
* https://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.boot.webmvc.autoconfigure;
|
||||
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
|
||||
/**
|
||||
* Customizer for the auto-configured {@link ForwardedHeaderFilter}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 4.2.0
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface ForwardedHeaderFilterCustomizer {
|
||||
|
||||
/**
|
||||
* Customize the given {@link ForwardedHeaderFilter}.
|
||||
* @param filter the filter to customize
|
||||
*/
|
||||
void customize(ForwardedHeaderFilter filter);
|
||||
|
||||
}
|
||||
+47
@@ -22,6 +22,7 @@ import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.DispatcherType;
|
||||
import jakarta.servlet.Servlet;
|
||||
import jakarta.servlet.ServletContext;
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -43,6 +44,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProp
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingFilterBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
|
||||
import org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration;
|
||||
@@ -62,10 +64,13 @@ import org.springframework.boot.servlet.filter.OrderedFormContentFilter;
|
||||
import org.springframework.boot.servlet.filter.OrderedHiddenHttpMethodFilter;
|
||||
import org.springframework.boot.servlet.filter.OrderedRequestContextFilter;
|
||||
import org.springframework.boot.validation.autoconfigure.ValidatorAdapter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.boot.web.servlet.ServletRegistrationBean;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Apiversion.Use;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Format;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.Forwardedheaders;
|
||||
import org.springframework.boot.webmvc.autoconfigure.WebMvcProperties.HeaderFormat;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.EmbeddedValueResolverAware;
|
||||
import org.springframework.context.ResourceLoaderAware;
|
||||
@@ -101,6 +106,7 @@ import org.springframework.web.context.ServletContextAware;
|
||||
import org.springframework.web.context.request.RequestContextListener;
|
||||
import org.springframework.web.context.support.ServletContextResource;
|
||||
import org.springframework.web.filter.FormContentFilter;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
import org.springframework.web.filter.HiddenHttpMethodFilter;
|
||||
import org.springframework.web.filter.RequestContextFilter;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
@@ -405,6 +411,47 @@ public final class WebMvcAutoConfiguration {
|
||||
return new OrderedRequestContextFilter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "server.forward-headers-strategy", havingValue = "framework")
|
||||
@ConditionalOnMissingFilterBean(ForwardedHeaderFilter.class)
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter(
|
||||
ObjectProvider<ForwardedHeaderFilterCustomizer> customizerProvider) {
|
||||
Forwardedheaders properties = this.mvcProperties.getForwardedHeaders();
|
||||
ForwardedHeaderFilter filter = new ForwardedHeaderFilter(
|
||||
properties.getHeaderFormat() == HeaderFormat.STANDARD);
|
||||
filter.setUseForwardedPrefix(properties.isUseForwardedPrefix());
|
||||
customizerProvider.ifAvailable((customizer) -> customizer.customize(filter));
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter);
|
||||
registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
|
||||
registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
|
||||
return registration;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts a deprecated {@code spring-boot-web-server}
|
||||
* {@link org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer}
|
||||
* bean to {@link ForwardedHeaderFilterCustomizer}. Defined as a separate nested
|
||||
* config, guarded by {@link ConditionalOnClass}, so that referencing the deprecated
|
||||
* type does not prevent reflection on other configuration classes when
|
||||
* {@code spring-boot-web-server} is not on the classpath (as is the case for
|
||||
* traditional WAR deployments).
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer.class)
|
||||
@SuppressWarnings("removal")
|
||||
static class DeprecatedForwardedHeaderFilterCustomizerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer.class)
|
||||
@ConditionalOnMissingBean
|
||||
@SuppressWarnings("removal")
|
||||
ForwardedHeaderFilterCustomizer deprecatedForwardedHeaderFilterCustomizerAdapter(
|
||||
org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer customizer) {
|
||||
return customizer::customize;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+56
@@ -104,6 +104,8 @@ public class WebMvcProperties {
|
||||
|
||||
private final Apiversion apiversion = new Apiversion();
|
||||
|
||||
private final Forwardedheaders forwardedHeaders = new Forwardedheaders();
|
||||
|
||||
public DefaultMessageCodesResolver.@Nullable Format getMessageCodesResolverFormat() {
|
||||
return this.messageCodesResolverFormat;
|
||||
}
|
||||
@@ -201,6 +203,10 @@ public class WebMvcProperties {
|
||||
return this.apiversion;
|
||||
}
|
||||
|
||||
public Forwardedheaders getForwardedHeaders() {
|
||||
return this.forwardedHeaders;
|
||||
}
|
||||
|
||||
public static class Async {
|
||||
|
||||
/**
|
||||
@@ -593,4 +599,54 @@ public class WebMvcProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwarded headers.
|
||||
*/
|
||||
public static class Forwardedheaders {
|
||||
|
||||
/**
|
||||
* Format of the forwarded headers to support.
|
||||
*/
|
||||
private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED;
|
||||
|
||||
/**
|
||||
* Whether to use the "X-Forwarded-Prefix" header to determine the context path.
|
||||
*/
|
||||
private boolean useForwardedPrefix;
|
||||
|
||||
public HeaderFormat getHeaderFormat() {
|
||||
return this.headerFormat;
|
||||
}
|
||||
|
||||
public void setHeaderFormat(HeaderFormat headerFormat) {
|
||||
this.headerFormat = headerFormat;
|
||||
}
|
||||
|
||||
public boolean isUseForwardedPrefix() {
|
||||
return this.useForwardedPrefix;
|
||||
}
|
||||
|
||||
public void setUseForwardedPrefix(boolean useForwardedPrefix) {
|
||||
this.useForwardedPrefix = useForwardedPrefix;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats of forwarded headers supported by {@link Forwardedheaders}.
|
||||
*/
|
||||
public enum HeaderFormat {
|
||||
|
||||
/**
|
||||
* Use the standard "Forwarded" header, as defined by RFC 7239.
|
||||
*/
|
||||
STANDARD,
|
||||
|
||||
/**
|
||||
* Use the non-standard "X-Forwarded-*" headers.
|
||||
*/
|
||||
X_FORWARDED
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+75
@@ -33,6 +33,7 @@ import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import jakarta.servlet.Filter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
@@ -114,6 +115,7 @@ import org.springframework.web.context.WebApplicationContext;
|
||||
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
import org.springframework.web.filter.FormContentFilter;
|
||||
import org.springframework.web.filter.ForwardedHeaderFilter;
|
||||
import org.springframework.web.filter.HiddenHttpMethodFilter;
|
||||
import org.springframework.web.filter.RequestContextFilter;
|
||||
import org.springframework.web.method.ControllerAdviceBean;
|
||||
@@ -661,6 +663,58 @@ class WebMvcAutoConfigurationTests {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(HiddenHttpMethodFilter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterIsNotConfiguredByDefault() {
|
||||
this.contextRunner.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterIsNotConfiguredWhenStrategyIsNotFramework() {
|
||||
this.contextRunner.withPropertyValues("server.forward-headers-strategy=native")
|
||||
.run((context) -> assertThat(context).doesNotHaveBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterIsConfiguredWhenFrameworkStrategyIsUsed() {
|
||||
this.contextRunner.withPropertyValues("server.forward-headers-strategy=framework").run((context) -> {
|
||||
assertThat(context).hasSingleBean(FilterRegistrationBean.class);
|
||||
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
|
||||
assertThat(filter).isInstanceOf(ForwardedHeaderFilter.class);
|
||||
assertThat(filter).extracting("useStandardHeader").isEqualTo(false);
|
||||
assertThat(filter).extracting("useForwardedPrefix").isEqualTo(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterAppliesConfiguredProperties() {
|
||||
this.contextRunner
|
||||
.withPropertyValues("server.forward-headers-strategy=framework",
|
||||
"spring.mvc.forwarded-headers.header-format=standard",
|
||||
"spring.mvc.forwarded-headers.use-forwarded-prefix=true")
|
||||
.run((context) -> {
|
||||
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
|
||||
assertThat(filter).extracting("useStandardHeader").isEqualTo(true);
|
||||
assertThat(filter).extracting("useForwardedPrefix").isEqualTo(true);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterBacksOffWhenFilterBeanAlreadyRegistered() {
|
||||
this.contextRunner.withUserConfiguration(ForwardedHeaderFilterConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework")
|
||||
.run((context) -> assertThat(context).hasSingleBean(FilterRegistrationBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeaderFilterCustomizerFromDeprecatedTypeIsApplied() {
|
||||
this.contextRunner.withUserConfiguration(ForwardedHeaderFilterCustomizerConfiguration.class)
|
||||
.withPropertyValues("server.forward-headers-strategy=framework")
|
||||
.run((context) -> {
|
||||
Filter filter = context.getBean(FilterRegistrationBean.class).getFilter();
|
||||
assertThat(filter).extracting("removeOnly").isEqualTo(true);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void customConfigurableWebBindingInitializer() {
|
||||
this.contextRunner.withUserConfiguration(CustomConfigurableWebBindingInitializer.class)
|
||||
@@ -1345,6 +1399,27 @@ class WebMvcAutoConfigurationTests {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderFilterConfiguration {
|
||||
|
||||
@Bean
|
||||
FilterRegistrationBean<ForwardedHeaderFilter> testForwardedHeaderFilter() {
|
||||
return new FilterRegistrationBean<>(new ForwardedHeaderFilter(false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class ForwardedHeaderFilterCustomizerConfiguration {
|
||||
|
||||
@Bean
|
||||
@SuppressWarnings("removal")
|
||||
org.springframework.boot.web.server.autoconfigure.servlet.ForwardedHeaderFilterCustomizer forwardedHeaderFilterCustomizer() {
|
||||
return (filter) -> filter.setRemoveOnly(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class CustomRequestMappingHandlerMapping {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user