mirror of
https://github.com/spring-projects/spring-boot.git
synced 2026-09-17 12:09:16 +00:00
Refine Forwarded headers configuration for Jetty servers
This commit introduces new properties for configuring the HTTP headers that should be used for forward support. The new `server.jetty.forwarded-headers.header-format` property allows "standard" or "x_forwarded" to select "Forwarded" or "X-Forwarded-*" headers. This aligns the behavior of Jetty server with the "FRAMEWORK" strategy. Closes gh-51148
This commit is contained in:
@@ -517,8 +517,8 @@ If your web server of choice supports the set of HTTP headers you need, setting
|
||||
| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.tomcat[]
|
||||
|
||||
| Jetty
|
||||
| `"X-Forwarded-*"`
|
||||
|
|
||||
| `"X-Forwarded-*"`, `"Forwarded"`
|
||||
| xref:how-to:webserver.adoc#howto.webserver.use-behind-a-proxy-server.jetty[]
|
||||
|
||||
| Reactor Netty
|
||||
| `"X-Forwarded-*"`
|
||||
@@ -569,6 +569,24 @@ You can take complete control of the configuration of Tomcat's javadoc:org.apach
|
||||
|
||||
|
||||
|
||||
[[howto.webserver.use-behind-a-proxy-server.jetty]]
|
||||
=== Customize Jetty's Proxy Configuration
|
||||
|
||||
If you use Jetty, you can additionally configure the type of the headers used to carry "`forwarded`" information.
|
||||
By default, the `"X-Forwarded-*"` format is used, but you can opt into the standard RFC variant:
|
||||
|
||||
[configprops,yaml]
|
||||
----
|
||||
server:
|
||||
jetty:
|
||||
forwarded-headers:
|
||||
header-format: "standard"
|
||||
----
|
||||
|
||||
For more specific options, you can switch off the forwarded configuration (using `server.forward-headers-strategy=NONE`) and directly use Jetty's javadoc:org.eclipse.jetty.server.ForwardedRequestCustomizer[] to change the HTTP configuration.
|
||||
|
||||
|
||||
|
||||
[[howto.webserver.enable-multiple-connectors]]
|
||||
== Enable Multiple Connectors
|
||||
|
||||
|
||||
+9
@@ -56,6 +56,15 @@ public interface ConfigurableJettyWebServerFactory extends ConfigurableWebServer
|
||||
*/
|
||||
void setUseForwardHeaders(boolean useForwardHeaders);
|
||||
|
||||
/**
|
||||
* Set if the RFC forwarded header should be processed.
|
||||
* <p>
|
||||
* {@link #setUseForwardHeaders(boolean)} will take precedence if enabled.
|
||||
* @param useRfcForwardHeader if forwarded header should be used
|
||||
* @since 4.2.0
|
||||
*/
|
||||
void setUseRfcForwardHeader(boolean useRfcForwardHeader);
|
||||
|
||||
/**
|
||||
* Add {@link JettyServerCustomizer}s that will be applied to the {@link Server}
|
||||
* before it is started.
|
||||
|
||||
+27
@@ -30,9 +30,36 @@ import org.eclipse.jetty.server.Server;
|
||||
*/
|
||||
public class ForwardHeadersCustomizer implements JettyServerCustomizer {
|
||||
|
||||
private final boolean useXForwarded;
|
||||
|
||||
/**
|
||||
* Create a new customizer instance.
|
||||
* @param useXForwarded "X-Forwarded-*" headers are used if {@code true}, otherwise
|
||||
* standard "Forwarded" are used instead.
|
||||
* @since 4.2.0
|
||||
*/
|
||||
public ForwardHeadersCustomizer(boolean useXForwarded) {
|
||||
this.useXForwarded = useXForwarded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new customizer instance with "X-Forwarded-*" support.
|
||||
*/
|
||||
public ForwardHeadersCustomizer() {
|
||||
this(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customize(Server server) {
|
||||
ForwardedRequestCustomizer customizer = new ForwardedRequestCustomizer();
|
||||
if (this.useXForwarded) {
|
||||
// disable "Forwarded" support
|
||||
customizer.setForwardedHeader(null);
|
||||
}
|
||||
else {
|
||||
// disable "X-Forwarded-*" support
|
||||
customizer.setForwardedOnly(true);
|
||||
}
|
||||
for (Connector connector : server.getConnectors()) {
|
||||
for (ConnectionFactory connectionFactory : connector.getConnectionFactories()) {
|
||||
if (connectionFactory instanceof HttpConfiguration.ConnectionFactory jettyConnectionFactory) {
|
||||
|
||||
+11
@@ -64,6 +64,8 @@ public class JettyWebServerFactory extends AbstractConfigurableWebServerFactory
|
||||
|
||||
private boolean useForwardHeaders;
|
||||
|
||||
private boolean useRfcForwardHeader;
|
||||
|
||||
private Set<JettyServerCustomizer> jettyServerCustomizers = new LinkedHashSet<>();
|
||||
|
||||
private int maxConnections = -1;
|
||||
@@ -180,6 +182,15 @@ public class JettyWebServerFactory extends AbstractConfigurableWebServerFactory
|
||||
this.useForwardHeaders = useForwardHeaders;
|
||||
}
|
||||
|
||||
public boolean isUseRfcForwardHeader() {
|
||||
return this.useRfcForwardHeader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUseRfcForwardHeader(boolean useRfcForwardHeader) {
|
||||
this.useRfcForwardHeader = useRfcForwardHeader;
|
||||
}
|
||||
|
||||
protected AbstractConnector createConnector(InetSocketAddress address, Server server) {
|
||||
return this.createConnector(address, server, null, null, null);
|
||||
}
|
||||
|
||||
+46
@@ -86,6 +86,11 @@ public class JettyServerProperties {
|
||||
*/
|
||||
private final Accesslog accesslog = new Accesslog();
|
||||
|
||||
/**
|
||||
* Forwarded headers configuration.
|
||||
*/
|
||||
private final Forwardedheaders forwardedHeaders = new Forwardedheaders();
|
||||
|
||||
/**
|
||||
* Thread related configuration.
|
||||
*/
|
||||
@@ -95,6 +100,10 @@ public class JettyServerProperties {
|
||||
return this.accesslog;
|
||||
}
|
||||
|
||||
public Forwardedheaders getForwardedHeaders() {
|
||||
return this.forwardedHeaders;
|
||||
}
|
||||
|
||||
public Threads getThreads() {
|
||||
return this.threads;
|
||||
}
|
||||
@@ -268,6 +277,43 @@ public class JettyServerProperties {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwarded headers.
|
||||
*/
|
||||
public static class Forwardedheaders {
|
||||
|
||||
/**
|
||||
* Format of the forwarded headers to support.
|
||||
*/
|
||||
private HeaderFormat headerFormat = HeaderFormat.X_FORWARDED;
|
||||
|
||||
public HeaderFormat getHeaderFormat() {
|
||||
return this.headerFormat;
|
||||
}
|
||||
|
||||
public void setHeaderFormat(HeaderFormat headerFormat) {
|
||||
this.headerFormat = headerFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Jetty thread properties.
|
||||
*/
|
||||
|
||||
+10
-1
@@ -79,7 +79,9 @@ public class JettyWebServerFactoryCustomizer
|
||||
|
||||
@Override
|
||||
public void customize(ConfigurableJettyWebServerFactory factory) {
|
||||
factory.setUseForwardHeaders(getOrDeduceUseForwardHeaders());
|
||||
if (getOrDeduceUseForwardHeaders()) {
|
||||
configureForwardedSupport(factory);
|
||||
}
|
||||
JettyServerProperties.Threads threadProperties = this.jettyProperties.getThreads();
|
||||
factory.setThreadPool(JettyThreadPool.create(this.jettyProperties.getThreads()));
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
@@ -121,6 +123,13 @@ public class JettyWebServerFactoryCustomizer
|
||||
return this.serverProperties.getForwardHeadersStrategy().equals(ServerProperties.ForwardHeadersStrategy.NATIVE);
|
||||
}
|
||||
|
||||
private void configureForwardedSupport(ConfigurableJettyWebServerFactory factory) {
|
||||
switch (this.jettyProperties.getForwardedHeaders().getHeaderFormat()) {
|
||||
case X_FORWARDED -> factory.setUseForwardHeaders(true);
|
||||
case STANDARD -> factory.setUseRfcForwardHeader(true);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> Consumer<T> customizeHttpConfigurations(ConfigurableJettyWebServerFactory factory,
|
||||
BiConsumer<HttpConfiguration, T> action) {
|
||||
return customizeConnectionFactories(factory, HttpConfiguration.ConnectionFactory.class,
|
||||
|
||||
+4
-1
@@ -118,7 +118,10 @@ public class JettyReactiveWebServerFactory extends JettyWebServerFactory
|
||||
customizer.customize(server);
|
||||
}
|
||||
if (this.isUseForwardHeaders()) {
|
||||
new ForwardHeadersCustomizer().customize(server);
|
||||
new ForwardHeadersCustomizer(true).customize(server);
|
||||
}
|
||||
else if (this.isUseRfcForwardHeader()) {
|
||||
new ForwardHeadersCustomizer(false).customize(server);
|
||||
}
|
||||
if (getShutdown() == Shutdown.GRACEFUL) {
|
||||
GracefulHandler gracefulHandler = new GracefulHandler();
|
||||
|
||||
+4
-1
@@ -177,7 +177,10 @@ public class JettyServletWebServerFactory extends JettyWebServerFactory
|
||||
customizer.customize(server);
|
||||
}
|
||||
if (this.isUseForwardHeaders()) {
|
||||
new ForwardHeadersCustomizer().customize(server);
|
||||
new ForwardHeadersCustomizer(true).customize(server);
|
||||
}
|
||||
else if (this.isUseRfcForwardHeader()) {
|
||||
new ForwardHeadersCustomizer(false).customize(server);
|
||||
}
|
||||
if (getShutdown() == Shutdown.GRACEFUL) {
|
||||
GracefulHandler gracefulHandler = new GracefulHandler();
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* 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.jetty;
|
||||
|
||||
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
|
||||
import org.eclipse.jetty.server.HttpConfiguration;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link ForwardHeadersCustomizer}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
class ForwardHeadersCustomizerTests {
|
||||
|
||||
@Test
|
||||
void defaultConstructorUsesXForwardedHeaders() {
|
||||
ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer());
|
||||
assertThat(customizer.getForwardedHeader()).isNull();
|
||||
assertThat(customizer.getForwardedForHeader()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void useXForwardedTrueEnablesXForwardedAndDisablesForwarded() {
|
||||
ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer(true));
|
||||
assertThat(customizer.getForwardedHeader()).isNull();
|
||||
assertThat(customizer.getForwardedForHeader()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void useXForwardedFalseEnablesForwardedAndDisablesXForwarded() {
|
||||
ForwardedRequestCustomizer customizer = customize(new ForwardHeadersCustomizer(false));
|
||||
assertThat(customizer.getForwardedHeader()).isNotNull();
|
||||
assertThat(customizer.getForwardedForHeader()).isNull();
|
||||
}
|
||||
|
||||
private ForwardedRequestCustomizer customize(ForwardHeadersCustomizer customizer) {
|
||||
Server server = new Server(0);
|
||||
customizer.customize(server);
|
||||
HttpConfiguration httpConfiguration = ((HttpConfiguration.ConnectionFactory) server.getConnectors()[0]
|
||||
.getConnectionFactories()
|
||||
.stream()
|
||||
.filter(HttpConfiguration.ConnectionFactory.class::isInstance)
|
||||
.findFirst()
|
||||
.orElseThrow()).getHttpConfiguration();
|
||||
return httpConfiguration.getCustomizer(ForwardedRequestCustomizer.class);
|
||||
}
|
||||
|
||||
}
|
||||
+13
@@ -100,6 +100,19 @@ class JettyServerPropertiesTests {
|
||||
assertThat(this.properties.getAccesslog().getIgnorePaths()).containsExactly("/a/path", "/b/path");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeadersDefaultToXForwarded() {
|
||||
assertThat(this.properties.getForwardedHeaders().getHeaderFormat())
|
||||
.isEqualTo(JettyServerProperties.Forwardedheaders.HeaderFormat.X_FORWARDED);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCustomizeJettyForwardedHeadersFormat() {
|
||||
bind("server.jetty.forwarded-headers.header-format", "standard");
|
||||
assertThat(this.properties.getForwardedHeaders().getHeaderFormat())
|
||||
.isEqualTo(JettyServerProperties.Forwardedheaders.HeaderFormat.STANDARD);
|
||||
}
|
||||
|
||||
@Test
|
||||
void jettyThreadPoolPropertyDefaultsShouldMatchServerDefault() {
|
||||
JettyServletWebServerFactory jettyFactory = new JettyServletWebServerFactory(0);
|
||||
|
||||
+26
-2
@@ -54,8 +54,10 @@ import org.springframework.test.context.support.TestPropertySourceUtils;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
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 JettyWebServerFactoryCustomizer}.
|
||||
@@ -94,7 +96,8 @@ class JettyWebServerFactoryCustomizerTests {
|
||||
void defaultUseForwardHeaders() {
|
||||
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setUseForwardHeaders(false);
|
||||
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
|
||||
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -111,7 +114,28 @@ class JettyWebServerFactoryCustomizerTests {
|
||||
this.serverProperties.setForwardHeadersStrategy(ServerProperties.ForwardHeadersStrategy.NONE);
|
||||
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setUseForwardHeaders(false);
|
||||
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
|
||||
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeadersFormatXForwardedConfiguresUseForwardHeaders() {
|
||||
this.environment.setProperty("DYNO", "-");
|
||||
bind("server.jetty.forwarded-headers.header-format=x_forwarded");
|
||||
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setUseForwardHeaders(true);
|
||||
then(factory).should(never()).setUseRfcForwardHeader(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
void forwardedHeadersFormatStandardConfiguresUseRfcForwardHeader() {
|
||||
this.environment.setProperty("DYNO", "-");
|
||||
bind("server.jetty.forwarded-headers.header-format=standard");
|
||||
ConfigurableJettyWebServerFactory factory = mock(ConfigurableJettyWebServerFactory.class);
|
||||
this.customizer.customize(factory);
|
||||
then(factory).should().setUseRfcForwardHeader(true);
|
||||
then(factory).should(never()).setUseForwardHeaders(anyBoolean());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+17
@@ -43,6 +43,7 @@ import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
@@ -121,6 +122,22 @@ class JettyReactiveWebServerFactoryTests extends AbstractReactiveWebServerFactor
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useRfcForwardHeader() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
factory.setUseRfcForwardHeader(true);
|
||||
assertRfcForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
factory.setUseForwardHeaders(true);
|
||||
factory.setUseRfcForwardHeader(true);
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void whenServerIsShuttingDownGracefullyThenNewConnectionsCannotBeMade() {
|
||||
JettyReactiveWebServerFactory factory = getFactory();
|
||||
|
||||
+16
@@ -442,6 +442,22 @@ class JettyServletWebServerFactoryTests extends AbstractServletWebServerFactoryT
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useRfcForwardHeader() throws Exception {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setUseRfcForwardHeader(true);
|
||||
assertRfcForwardHeaderIsUsed(factory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void useForwardHeadersTakesPrecedenceOverUseRfcForwardHeader() throws Exception {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
factory.setUseForwardHeaders(true);
|
||||
factory.setUseRfcForwardHeader(true);
|
||||
assertForwardHeaderIsUsed(factory);
|
||||
assertThatExceptionOfType(AssertionError.class).isThrownBy(() -> assertRfcForwardHeaderIsUsed(factory));
|
||||
}
|
||||
|
||||
@Test
|
||||
void defaultThreadPool() {
|
||||
JettyServletWebServerFactory factory = getFactory();
|
||||
|
||||
+12
@@ -677,6 +677,18 @@ public abstract class AbstractReactiveWebServerFactoryTests {
|
||||
assertThat(body).isEqualTo("https");
|
||||
}
|
||||
|
||||
protected void assertRfcForwardHeaderIsUsed(ConfigurableReactiveWebServerFactory factory) {
|
||||
this.webServer = factory.getWebServer(new XForwardedHandler());
|
||||
this.webServer.start();
|
||||
String body = getWebClient(this.webServer.getPort()).build()
|
||||
.get()
|
||||
.header("Forwarded", "proto=https")
|
||||
.retrieve()
|
||||
.bodyToMono(String.class)
|
||||
.block(Duration.ofSeconds(30));
|
||||
assertThat(body).isEqualTo("https");
|
||||
}
|
||||
|
||||
private <T> T doWithRetry(Callable<T> action) throws Exception {
|
||||
Exception lastFailure = null;
|
||||
for (int i = 0; i < 10; i++) {
|
||||
|
||||
+8
@@ -1617,6 +1617,14 @@ public abstract class AbstractServletWebServerFactoryTests {
|
||||
.contains("remoteaddr=140.211.11.130");
|
||||
}
|
||||
|
||||
protected void assertRfcForwardHeaderIsUsed(ServletWebServerFactory factory)
|
||||
throws IOException, URISyntaxException {
|
||||
this.webServer = factory.getWebServer(new ServletRegistrationBean<>(new ExampleServlet(true, false), "/hello"));
|
||||
this.webServer.start();
|
||||
assertThat(getResponse(getLocalUrl("/hello"), "Forwarded:for=140.211.11.130"))
|
||||
.contains("remoteaddr=140.211.11.130");
|
||||
}
|
||||
|
||||
protected abstract ConfigurableServletWebServerFactory getFactory();
|
||||
|
||||
protected abstract org.apache.jasper.servlet.@Nullable JspServlet getJspServlet() throws Exception;
|
||||
|
||||
Reference in New Issue
Block a user