Merge branch 'main' into pr/3751

This commit is contained in:
Ryan Baxter
2025-05-29 11:36:27 -04:00
51 changed files with 1548 additions and 301 deletions

View File

@@ -109,6 +109,7 @@
*** xref:spring-cloud-gateway-server-webmvc/filters/requestsize.adoc[]
*** xref:spring-cloud-gateway-server-webmvc/filters/setrequesthostheader.adoc[]
*** xref:spring-cloud-gateway-server-webmvc/filters/tokenrelay.adoc[]
** xref:spring-cloud-gateway-server-webmvc/httpheadersfilters.adoc[]
** xref:spring-cloud-gateway-server-webmvc/writing-custom-predicates-and-filters.adoc[]
** xref:spring-cloud-gateway-server-webmvc/working-with-servlets-and-filters.adoc[]

View File

@@ -5,11 +5,11 @@
[[forwarded-headers-filter]]
== Forwarded Headers Filter
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header.
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header. To activate this filter set the `spring.cloud.gateway.server.webflux.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
The `Forwarded by` header part can be enabled by setting the following property to true (defaults to false):
- `spring.cloud.gateway.forwarded.by.enabled=true`
- `spring.cloud.gateway.server.webflux.forwarded.by.enabled=true`
[[removehopbyhop-headers-filter]]
== RemoveHopByHop Headers Filter
@@ -25,25 +25,25 @@ The `RemoveHopByHop` Headers Filter removes headers from forwarded requests. The
* Transfer-Encoding
* Upgrade
To change this, set the `spring.cloud.gateway.filter.remove-hop-by-hop.headers` property to the list of header names to remove.
To change this, set the `spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.headers` property to the list of header names to remove.
[[xforwarded-headers-filter]]
== XForwarded Headers Filter
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers.
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers. To activate this filter set the `spring.cloud.gateway.server.webflux.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
Creating of individual headers can be controlled by the following boolean properties (defaults to true):
- `spring.cloud.gateway.x-forwarded.for-enabled`
- `spring.cloud.gateway.x-forwarded.host-enabled`
- `spring.cloud.gateway.x-forwarded.port-enabled`
- `spring.cloud.gateway.x-forwarded.proto-enabled`
- `spring.cloud.gateway.x-forwarded.prefix-enabled`
- `spring.cloud.gateway.server.webflux.x-forwarded.for-enabled`
- `spring.cloud.gateway.server.webflux.x-forwarded.host-enabled`
- `spring.cloud.gateway.server.webflux.x-forwarded.port-enabled`
- `spring.cloud.gateway.server.webflux.x-forwarded.proto-enabled`
- `spring.cloud.gateway.server.webflux.x-forwarded.prefix-enabled`
Appending multiple headers can be controlled by the following boolean properties (defaults to true):
- `spring.cloud.gateway.x-forwarded.for-append`
- `spring.cloud.gateway.x-forwarded.host-append`
- `spring.cloud.gateway.x-forwarded.port-append`
- `spring.cloud.gateway.x-forwarded.proto-append`
- `spring.cloud.gateway.x-forwarded.prefix-append`
- `spring.cloud.gateway.server.webflux.x-forwarded.for-append`
- `spring.cloud.gateway.server.webflux.x-forwarded.host-append`
- `spring.cloud.gateway.server.webflux.x-forwarded.port-append`
- `spring.cloud.gateway.server.webflux.x-forwarded.proto-append`
- `spring.cloud.gateway.server.webflux.x-forwarded.prefix-append`

View File

@@ -0,0 +1,45 @@
[[httpheadersfilters]]
= HttpHeadersFilters
HttpHeadersFilters are applied to the requests before sending them downstream, such as in the `NettyRoutingFilter`.
[[forwarded-headers-filter]]
== Forwarded Headers Filter
The `Forwarded` Headers Filter creates a `Forwarded` header to send to the downstream service. It adds the `Host` header, scheme and port of the current request to any existing `Forwarded` header. To activate this filter set the `spring.cloud.gateway.server.webmvc.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
[[removehopbyhop-headers-filter]]
== RemoveHopByHop Headers Filter
The `RemoveHopByHop` Headers Filter removes headers from forwarded requests. The default list of headers that is removed comes from the https://tools.ietf.org/html/draft-ietf-httpbis-p1-messaging-14#section-7.1.3[IETF].
.The default removed headers are:
* Connection
* Keep-Alive
* Proxy-Authenticate
* Proxy-Authorization
* TE
* Trailer
* Transfer-Encoding
* Upgrade
//To change this, set the `spring.cloud.gateway.filter.remove-hop-by-hop.headers` property to the list of header names to remove.
[[xforwarded-headers-filter]]
== XForwarded Headers Filter
The `XForwarded` Headers Filter creates various `X-Forwarded-*` headers to send to the downstream service. It uses the `Host` header, scheme, port and path of the current request to create the various headers. To activate this filter set the `spring.cloud.gateway.server.webmvc.trusted-proxies` property to a Java Regular Expression. This regular expression defines the proxies that are trusted when they appear in the `Forwarded` header.
Creating of individual headers can be controlled by the following boolean properties (defaults to true):
- `spring.cloud.gateway.server.webmvc.x-forwarded.for-enabled`
- `spring.cloud.gateway.server.webmvc.x-forwarded.host-enabled`
- `spring.cloud.gateway.server.webmvc.x-forwarded.port-enabled`
- `spring.cloud.gateway.server.webmvc.x-forwarded.proto-enabled`
- `spring.cloud.gateway.server.webmvc.x-forwarded.prefix-enabled`
Appending multiple headers can be controlled by the following boolean properties (defaults to true):
- `spring.cloud.gateway.server.webmvc.x-forwarded.for-append`
- `spring.cloud.gateway.server.webmvc.x-forwarded.host-append`
- `spring.cloud.gateway.server.webmvc.x-forwarded.port-append`
- `spring.cloud.gateway.server.webmvc.x-forwarded.proto-append`
- `spring.cloud.gateway.server.webmvc.x-forwarded.prefix-append`

View File

@@ -49,6 +49,71 @@ class SimpleGateway {
[[gateway-handlerfunctions]]
== Gateway MVC Handler Functions
Various `RouterFunctions.Builder` methods require a `HandlerFunction<ServerResponse>`. To create a route that is proxied by the MVC Gateway, `HandlerFunction` implementations are supplied in `org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions`. The most basic is the `http()` `HandlerFunction`. The function looks for a `URI` in the `org.springframework.cloud.gateway.server.mvc.common.MvcUtils.GATEWAY_REQUEST_URL_ATTR` request attribute. This allows for dynamic targets such as load balancing to set the `URI`.
WARNING: As of version 4.1.7, `HandlerFunctions.http(String)` and `HandlerFunctions.http(URI)` are now deprecated. Please use `HandlerFunctions.http()` in combination with the `BeforeFilterFunctions.uri()` filter instead. This fixes inconsistencies in dealing with the route url request attribute.
Various `RouterFunctions.Builder` methods require a `HandlerFunction<ServerResponse>`. To create a route that is proxied by the MVC Gateway, `HandlerFunction` implementations are supplied in `org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions`.
=== HTTP Handler Function
The most basic handler function is `http()` `HandlerFunction`. If a `URI` is supplied as a parameter, that is the `URI` used as the downstream target for sending the HTTP requests (as seen in the example above). If no parameter is passed, the function looks for a `URI` in the `org.springframework.cloud.gateway.server.mvc.common.MvcUtils.GATEWAY_REQUEST_URL_ATTR` request attribute. This allows for dynamic targets such as load balancing to set the `URI`.
WARNING: As of version 4.1.7, `HandlerFunctions.http(String)` and `HandlerFunctions.http(URI)` are now deprecated. Please use `HandlerFunctions.http()` in combination with the `BeforeFilterFunctions.uri()` filter instead. This fixes inconsistencies in dealing with the route url request attribute.
=== Spring Cloud Function Handler Function
By placing https://spring.io/projects/spring-cloud-function[Spring Cloud Function] on the classpath, Spring Cloud Gateway will automatically configure routes to invoke functions you define as beans. The bean names of the functions will be used as the path of the routes.
For example, given the following configuration:
[source,xml]
----
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
</dependency>
----
Once Spring Cloud Function dependency is provided the name of the Java function bean becomes the path you can use to route to functions.
For example, assume the following application:
[source,java]
----
@SpringBootApplication
public class DemoFunctionGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(DemoFunctionGatewayApplication.class, args);
}
@Bean
public Function<String, String> uppercase() {
return v -> v.toUpperCase();
}
@Bean
public Function<String, String> concat() {
return v -> v + v;
}
}
----
You can invoke the `concat` or `uppercase` functions by issuing a `GET` or `POST` request to `/concat` or `/uppercase`.
Making a `GET` request to ``http://localhost:8080/uppercase/hello` will invoke the `uppercase` function with the String `hello` and return `HELLO` in the `GET` response body.
Instead of passing the function parameter as a path parameter you can use a `POST` request. For example the following cURL command can issued to invoke the `concat` function:
[source,bash]
----
$ curl -d "hello"' -H "Content-Type: application/json" -X POST http://localhost:8080/concat
----
The response body will contain `hellohello`.
Spring Cloud Gateway also supports function composition by issuing a request to a path composed of function names separated by a comma. For example:
[source,bash]
----
$ curl -d "hello"' -H "Content-Type: application/json" -X POST http://localhost:8080/concat,uppercase
----
The response body will contain `HELLOHELLO`.

View File

@@ -1,9 +1,9 @@
|===
|Name | Default | Description
|spring.cloud.gateway | |
|spring.cloud.gateway | |
|spring.cloud.gateway.default-filters | | List of filter definitions that are applied to every route.
|spring.cloud.gateway.discovery.locator | |
|spring.cloud.gateway.discovery.locator | |
|spring.cloud.gateway.discovery.locator.enabled | `+++false+++` | Flag that enables DiscoveryClient gateway integration.
|spring.cloud.gateway.discovery.locator.filters | |
|spring.cloud.gateway.discovery.locator.include-expression | `+++true+++` | SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true.
@@ -21,10 +21,10 @@
|spring.cloud.gateway.filter.fallback-headers.enabled | `+++true+++` | Enables the fallback-headers filter.
|spring.cloud.gateway.filter.hystrix.enabled | `+++true+++` | Enables the hystrix filter.
|spring.cloud.gateway.filter.json-to-grpc.enabled | `+++true+++` | Enables the JSON to gRPC filter.
|spring.cloud.gateway.filter.local-response-cache | |
|spring.cloud.gateway.filter.local-response-cache | |
|spring.cloud.gateway.filter.local-response-cache.enabled | `+++false+++` | Enables the local-response-cache filter.
|spring.cloud.gateway.filter.local-response-cache.request | |
|spring.cloud.gateway.filter.local-response-cache.request.no-cache-strategy | `+++skip-update-cache-entry+++` |
|spring.cloud.gateway.filter.local-response-cache.request | |
|spring.cloud.gateway.filter.local-response-cache.request.no-cache-strategy | `+++skip-update-cache-entry+++` |
|spring.cloud.gateway.filter.local-response-cache.size | | Maximum size of the cache to evict entries for this route (in KB, MB and GB).
|spring.cloud.gateway.filter.local-response-cache.time-to-live | `+++5m+++` | Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours).
|spring.cloud.gateway.filter.map-request-header.enabled | `+++true+++` | Enables the map-request-header filter.
@@ -33,16 +33,16 @@
|spring.cloud.gateway.filter.prefix-path.enabled | `+++true+++` | Enables the prefix-path filter.
|spring.cloud.gateway.filter.preserve-host-header.enabled | `+++true+++` | Enables the preserve-host-header filter.
|spring.cloud.gateway.filter.redirect-to.enabled | `+++true+++` | Enables the redirect-to filter.
|spring.cloud.gateway.filter.remove-hop-by-hop | |
|spring.cloud.gateway.filter.remove-hop-by-hop.headers | |
|spring.cloud.gateway.filter.remove-hop-by-hop | |
|spring.cloud.gateway.filter.remove-hop-by-hop.headers | |
|spring.cloud.gateway.filter.remove-hop-by-hop.order | `+++0+++` |
|spring.cloud.gateway.filter.remove-request-header.enabled | `+++true+++` | Enables the remove-request-header filter.
|spring.cloud.gateway.filter.remove-request-parameter.enabled | `+++true+++` | Enables the remove-request-parameter filter.
|spring.cloud.gateway.filter.remove-response-header.enabled | `+++true+++` | Enables the remove-response-header filter.
|spring.cloud.gateway.filter.request-header-size.enabled | `+++true+++` | Enables the request-header-size filter.
|spring.cloud.gateway.filter.request-header-to-request-uri.enabled | `+++true+++` | Enables the request-header-to-request-uri filter.
|spring.cloud.gateway.filter.request-rate-limiter | |
|spring.cloud.gateway.filter.request-rate-limiter.default-key-resolver | |
|spring.cloud.gateway.filter.request-rate-limiter | |
|spring.cloud.gateway.filter.request-rate-limiter.default-key-resolver | |
|spring.cloud.gateway.filter.request-rate-limiter.default-rate-limiter | |
|spring.cloud.gateway.filter.request-rate-limiter.enabled | `+++true+++` | Enables the request-rate-limiter filter.
|spring.cloud.gateway.filter.request-size.enabled | `+++true+++` | Enables the request-size filter.
@@ -53,18 +53,18 @@
|spring.cloud.gateway.filter.rewrite-request-parameter.enabled | `+++true+++` | Enables the rewrite-request-parameter filter.
|spring.cloud.gateway.filter.rewrite-response-header.enabled | `+++true+++` | Enables the rewrite-response-header filter.
|spring.cloud.gateway.filter.save-session.enabled | `+++true+++` | Enables the save-session filter.
|spring.cloud.gateway.filter.secure-headers | |
|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|spring.cloud.gateway.filter.secure-headers | |
|spring.cloud.gateway.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|spring.cloud.gateway.filter.secure-headers.content-type-options | `+++nosniff+++` |
|spring.cloud.gateway.filter.secure-headers.default-headers | |
|spring.cloud.gateway.filter.secure-headers.disable | |
|spring.cloud.gateway.filter.secure-headers.disabled-headers | |
|spring.cloud.gateway.filter.secure-headers.download-options | `+++noopen+++` |
|spring.cloud.gateway.filter.secure-headers.default-headers | |
|spring.cloud.gateway.filter.secure-headers.disable | |
|spring.cloud.gateway.filter.secure-headers.disabled-headers | |
|spring.cloud.gateway.filter.secure-headers.download-options | `+++noopen+++` |
|spring.cloud.gateway.filter.secure-headers.enabled | `+++true+++` | Enables the secure-headers filter.
|spring.cloud.gateway.filter.secure-headers.enabled-headers | |
|spring.cloud.gateway.filter.secure-headers.frame-options | `+++DENY+++` |
|spring.cloud.gateway.filter.secure-headers.permissions-policy | `+++accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()+++` |
|spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|spring.cloud.gateway.filter.secure-headers.enabled-headers | |
|spring.cloud.gateway.filter.secure-headers.frame-options | `+++DENY+++` |
|spring.cloud.gateway.filter.secure-headers.permissions-policy | `+++accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()+++` |
|spring.cloud.gateway.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|spring.cloud.gateway.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
|spring.cloud.gateway.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
|spring.cloud.gateway.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
@@ -87,16 +87,16 @@
|spring.cloud.gateway.global-filter.remove-cached-body.enabled | `+++true+++` | Enables the remove-cached-body global filter.
|spring.cloud.gateway.global-filter.route-to-request-url.enabled | `+++true+++` | Enables the route-to-request-url global filter.
|spring.cloud.gateway.global-filter.websocket-routing.enabled | `+++true+++` | Enables the websocket-routing global filter.
|spring.cloud.gateway.globalcors | |
|spring.cloud.gateway.globalcors | |
|spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping | `+++false+++` | If global CORS config should be added to the URL handler.
|spring.cloud.gateway.globalcors.cors-configurations | |
|spring.cloud.gateway.handler-mapping.order | `+++1+++` | The order of RoutePredicateHandlerMapping.
|spring.cloud.gateway.httpclient | |
|spring.cloud.gateway.httpclient | |
|spring.cloud.gateway.httpclient.compression | `+++false+++` | Enables compression for Netty HttpClient.
|spring.cloud.gateway.httpclient.connect-timeout | | The connect timeout in millis, the default is 30s.
|spring.cloud.gateway.httpclient.max-header-size | | The max response header size.
|spring.cloud.gateway.httpclient.max-initial-line-length | | The max initial line length.
|spring.cloud.gateway.httpclient.pool | |
|spring.cloud.gateway.httpclient.pool | |
|spring.cloud.gateway.httpclient.pool.acquire-timeout | | Only for type FIXED, the maximum time in millis to wait for acquiring.
|spring.cloud.gateway.httpclient.pool.eviction-interval | `+++0+++` | Perform regular eviction checks in the background at a specified interval. Disabled by default ({@link Duration#ZERO})
|spring.cloud.gateway.httpclient.pool.leasing-strategy | `+++fifo+++` | Configures the leasing strategy for the pool (fifo or lifo), defaults to FIFO which is Netty's default.
@@ -106,7 +106,7 @@
|spring.cloud.gateway.httpclient.pool.metrics | `+++false+++` | Enables channel pools metrics to be collected and registered in Micrometer. Disabled by default.
|spring.cloud.gateway.httpclient.pool.name | `+++proxy+++` | The channel pool map name, defaults to proxy.
|spring.cloud.gateway.httpclient.pool.type | `+++elastic+++` | Type of pool for HttpClient to use (elastic, fixed or disabled).
|spring.cloud.gateway.httpclient.proxy | |
|spring.cloud.gateway.httpclient.proxy | |
|spring.cloud.gateway.httpclient.proxy.host | | Hostname for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.httpclient.proxy.non-proxy-hosts-pattern | | Regular expression (Java) for a configured list of hosts. that should be reached directly, bypassing the proxy
|spring.cloud.gateway.httpclient.proxy.password | | Password for proxy configuration of Netty HttpClient.
@@ -114,7 +114,7 @@
|spring.cloud.gateway.httpclient.proxy.type | `+++http+++` | proxyType for proxy configuration of Netty HttpClient (http, socks4 or socks5).
|spring.cloud.gateway.httpclient.proxy.username | | Username for proxy configuration of Netty HttpClient.
|spring.cloud.gateway.httpclient.response-timeout | | The response timeout.
|spring.cloud.gateway.httpclient.ssl | |
|spring.cloud.gateway.httpclient.ssl | |
|spring.cloud.gateway.httpclient.ssl.close-notify-flush-timeout | `+++3000ms+++` | SSL close_notify flush timeout. Default to 3000 ms.
|spring.cloud.gateway.httpclient.ssl.close-notify-read-timeout | `+++0+++` | SSL close_notify read timeout. Default to 0 ms.
|spring.cloud.gateway.httpclient.ssl.handshake-timeout | `+++10000ms+++` | SSL handshake timeout. Default to 10000 ms
@@ -126,14 +126,14 @@
|spring.cloud.gateway.httpclient.ssl.ssl-bundle | | The name of the SSL bundle to use.
|spring.cloud.gateway.httpclient.ssl.trusted-x509-certificates | | Trusted certificates for verifying the remote endpoint's certificate.
|spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager | `+++false+++` | Installs the netty InsecureTrustManagerFactory. This is insecure and not suitable for production.
|spring.cloud.gateway.httpclient.websocket | |
|spring.cloud.gateway.httpclient.websocket | |
|spring.cloud.gateway.httpclient.websocket.max-frame-payload-length | | Max frame payload length.
|spring.cloud.gateway.httpclient.websocket.proxy-ping | `+++true+++` | Proxy ping frames to downstream services, defaults to true.
|spring.cloud.gateway.httpclient.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpClient.
|spring.cloud.gateway.httpserver.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpServer.
|spring.cloud.gateway.loadbalancer | |
|spring.cloud.gateway.loadbalancer.use404 | `+++false+++` |
|spring.cloud.gateway.metrics | |
|spring.cloud.gateway.loadbalancer | |
|spring.cloud.gateway.loadbalancer.use404 | `+++false+++` |
|spring.cloud.gateway.metrics | |
|spring.cloud.gateway.metrics.enabled | `+++false+++` | Enables the collection of metrics data.
|spring.cloud.gateway.metrics.prefix | `+++spring.cloud.gateway+++` | The prefix of all metrics emitted by gateway.
|spring.cloud.gateway.metrics.tags | | Tags map that added to metrics.
@@ -152,6 +152,7 @@
|spring.cloud.gateway.mvc.streaming-buffer-size | `+++16384+++` | Buffer size for streaming media mime-types.
|spring.cloud.gateway.mvc.streaming-media-types | | Mime-types that are streaming.
|spring.cloud.gateway.mvc.transfer-encoding-normalization-request-headers-filter.enabled | `+++true+++` | Enables the transfer-encoding-normalization-request-headers-filter.
|spring.cloud.gateway.mvc.trusted-proxies | | Regular expression defining proxies that are trusted when they appear in a Forwarded of X-Forwarded header.
|spring.cloud.gateway.mvc.weight-calculator-filter.enabled | `+++true+++` | Enables the weight-calculator-filter.
|spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.mvc.x-forwarded-request-headers-filter.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
@@ -181,7 +182,7 @@
|spring.cloud.gateway.predicate.remote-addr.enabled | `+++true+++` | Enables the remote-addr predicate.
|spring.cloud.gateway.predicate.weight.enabled | `+++true+++` | Enables the weight predicate.
|spring.cloud.gateway.predicate.xforwarded-remote-addr.enabled | `+++true+++` | Enables the xforwarded-remote-addr predicate.
|spring.cloud.gateway.redis-rate-limiter | |
|spring.cloud.gateway.redis-rate-limiter | |
|spring.cloud.gateway.redis-rate-limiter.burst-capacity-header | `+++X-RateLimit-Burst-Capacity+++` | The name of the header that returns the burst capacity configuration.
|spring.cloud.gateway.redis-rate-limiter.config | |
|spring.cloud.gateway.redis-rate-limiter.include-headers | `+++true+++` | Whether or not to include headers containing rate limiter information, defaults to true.
@@ -195,10 +196,10 @@
|spring.cloud.gateway.routes | | List of Routes.
|spring.cloud.gateway.server.webflux.default-filters | | List of filter definitions that are applied to every route.
|spring.cloud.gateway.server.webflux.discovery.locator.enabled | `+++false+++` | Flag that enables DiscoveryClient gateway integration.
|spring.cloud.gateway.server.webflux.discovery.locator.filters | |
|spring.cloud.gateway.server.webflux.discovery.locator.filters | |
|spring.cloud.gateway.server.webflux.discovery.locator.include-expression | `+++true+++` | SpEL expression that will evaluate whether to include a service in gateway integration or not, defaults to: true.
|spring.cloud.gateway.server.webflux.discovery.locator.lower-case-service-id | `+++false+++` | Option to lower case serviceId in predicates and filters, defaults to false. Useful with eureka when it automatically uppercases serviceId. so MYSERIVCE, would match /myservice/**
|spring.cloud.gateway.server.webflux.discovery.locator.predicates | |
|spring.cloud.gateway.server.webflux.discovery.locator.predicates | |
|spring.cloud.gateway.server.webflux.discovery.locator.route-id-prefix | | The prefix for the routeId, defaults to discoveryClient.getClass().getSimpleName() + "_". Service Id will be appended to create the routeId.
|spring.cloud.gateway.server.webflux.discovery.locator.url-expression | `+++'lb://'+serviceId+++` | SpEL expression that create the uri for each route, defaults to: 'lb://'+serviceId.
|spring.cloud.gateway.server.webflux.enabled | `+++true+++` | Enables gateway functionality.
@@ -212,7 +213,7 @@
|spring.cloud.gateway.server.webflux.filter.hystrix.enabled | `+++true+++` | Enables the hystrix filter.
|spring.cloud.gateway.server.webflux.filter.json-to-grpc.enabled | `+++true+++` | Enables the JSON to gRPC filter.
|spring.cloud.gateway.server.webflux.filter.local-response-cache.enabled | `+++false+++` | Enables the local-response-cache filter.
|spring.cloud.gateway.server.webflux.filter.local-response-cache.request.no-cache-strategy | `+++skip-update-cache-entry+++` |
|spring.cloud.gateway.server.webflux.filter.local-response-cache.request.no-cache-strategy | `+++skip-update-cache-entry+++` |
|spring.cloud.gateway.server.webflux.filter.local-response-cache.size | | Maximum size of the cache to evict entries for this route (in KB, MB and GB).
|spring.cloud.gateway.server.webflux.filter.local-response-cache.time-to-live | `+++5m+++` | Time to expire a cache entry (expressed in s for seconds, m for minutes, and h for hours).
|spring.cloud.gateway.server.webflux.filter.map-request-header.enabled | `+++true+++` | Enables the map-request-header filter.
@@ -221,15 +222,15 @@
|spring.cloud.gateway.server.webflux.filter.prefix-path.enabled | `+++true+++` | Enables the prefix-path filter.
|spring.cloud.gateway.server.webflux.filter.preserve-host-header.enabled | `+++true+++` | Enables the preserve-host-header filter.
|spring.cloud.gateway.server.webflux.filter.redirect-to.enabled | `+++true+++` | Enables the redirect-to filter.
|spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.headers | |
|spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.order | `+++0+++` |
|spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.headers | |
|spring.cloud.gateway.server.webflux.filter.remove-hop-by-hop.order | `+++0+++` |
|spring.cloud.gateway.server.webflux.filter.remove-request-header.enabled | `+++true+++` | Enables the remove-request-header filter.
|spring.cloud.gateway.server.webflux.filter.remove-request-parameter.enabled | `+++true+++` | Enables the remove-request-parameter filter.
|spring.cloud.gateway.server.webflux.filter.remove-response-header.enabled | `+++true+++` | Enables the remove-response-header filter.
|spring.cloud.gateway.server.webflux.filter.request-header-size.enabled | `+++true+++` | Enables the request-header-size filter.
|spring.cloud.gateway.server.webflux.filter.request-header-to-request-uri.enabled | `+++true+++` | Enables the request-header-to-request-uri filter.
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.default-key-resolver | |
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.default-rate-limiter | |
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.default-key-resolver | |
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.default-rate-limiter | |
|spring.cloud.gateway.server.webflux.filter.request-rate-limiter.enabled | `+++true+++` | Enables the request-rate-limiter filter.
|spring.cloud.gateway.server.webflux.filter.request-size.enabled | `+++true+++` | Enables the request-size filter.
|spring.cloud.gateway.server.webflux.filter.retry.enabled | `+++true+++` | Enables the retry filter.
@@ -239,20 +240,20 @@
|spring.cloud.gateway.server.webflux.filter.rewrite-request-parameter.enabled | `+++true+++` | Enables the rewrite-request-parameter filter.
|spring.cloud.gateway.server.webflux.filter.rewrite-response-header.enabled | `+++true+++` | Enables the rewrite-response-header filter.
|spring.cloud.gateway.server.webflux.filter.save-session.enabled | `+++true+++` | Enables the save-session filter.
|spring.cloud.gateway.server.webflux.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.content-type-options | `+++nosniff+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.default-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.disable | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.disabled-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.download-options | `+++noopen+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.content-security-policy | `+++default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.content-type-options | `+++nosniff+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.default-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.disable | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.disabled-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.download-options | `+++noopen+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.enabled | `+++true+++` | Enables the secure-headers filter.
|spring.cloud.gateway.server.webflux.filter.secure-headers.enabled-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.frame-options | `+++DENY+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.permissions-policy | `+++accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.enabled-headers | |
|spring.cloud.gateway.server.webflux.filter.secure-headers.frame-options | `+++DENY+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.permissions-policy | `+++accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.permitted-cross-domain-policies | `+++none+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.referrer-policy | `+++no-referrer+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.strict-transport-security | `+++max-age=631138519+++` |
|spring.cloud.gateway.server.webflux.filter.secure-headers.xss-protection-header | `+++1 ; mode=block+++` |
|spring.cloud.gateway.server.webflux.filter.set-path.enabled | `+++true+++` | Enables the set-path filter.
|spring.cloud.gateway.server.webflux.filter.set-request-header.enabled | `+++true+++` | Enables the set-request-header filter.
|spring.cloud.gateway.server.webflux.filter.set-request-host-header.enabled | `+++true+++` | Enables the set-request-host-header filter.
@@ -273,7 +274,7 @@
|spring.cloud.gateway.server.webflux.global-filter.route-to-request-url.enabled | `+++true+++` | Enables the route-to-request-url global filter.
|spring.cloud.gateway.server.webflux.global-filter.websocket-routing.enabled | `+++true+++` | Enables the websocket-routing global filter.
|spring.cloud.gateway.server.webflux.globalcors.add-to-simple-url-handler-mapping | `+++false+++` | If global CORS config should be added to the URL handler.
|spring.cloud.gateway.server.webflux.globalcors.cors-configurations | |
|spring.cloud.gateway.server.webflux.globalcors.cors-configurations | |
|spring.cloud.gateway.server.webflux.handler-mapping.order | `+++1+++` | The order of RoutePredicateHandlerMapping.
|spring.cloud.gateway.server.webflux.httpclient.compression | `+++false+++` | Enables compression for Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpclient.connect-timeout | | The connect timeout in millis, the default is 30s.
@@ -310,7 +311,7 @@
|spring.cloud.gateway.server.webflux.httpclient.websocket.proxy-ping | `+++true+++` | Proxy ping frames to downstream services, defaults to true.
|spring.cloud.gateway.server.webflux.httpclient.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpClient.
|spring.cloud.gateway.server.webflux.httpserver.wiretap | `+++false+++` | Enables wiretap debugging for Netty HttpServer.
|spring.cloud.gateway.server.webflux.loadbalancer.use404 | `+++false+++` |
|spring.cloud.gateway.server.webflux.loadbalancer.use404 | `+++false+++` |
|spring.cloud.gateway.server.webflux.metrics.enabled | `+++false+++` | Enables the collection of metrics data.
|spring.cloud.gateway.server.webflux.metrics.prefix | `+++spring.cloud.gateway+++` | The prefix of all metrics emitted by gateway.
|spring.cloud.gateway.server.webflux.metrics.tags | | Tags map that added to metrics.
@@ -331,7 +332,7 @@
|spring.cloud.gateway.server.webflux.predicate.weight.enabled | `+++true+++` | Enables the weight predicate.
|spring.cloud.gateway.server.webflux.predicate.xforwarded-remote-addr.enabled | `+++true+++` | Enables the xforwarded-remote-addr predicate.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.burst-capacity-header | `+++X-RateLimit-Burst-Capacity+++` | The name of the header that returns the burst capacity configuration.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.config | |
|spring.cloud.gateway.server.webflux.redis-rate-limiter.config | |
|spring.cloud.gateway.server.webflux.redis-rate-limiter.include-headers | `+++true+++` | Whether or not to include headers containing rate limiter information, defaults to true.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.remaining-header | `+++X-RateLimit-Remaining+++` | The name of the header that returns number of remaining requests during the current second.
|spring.cloud.gateway.server.webflux.redis-rate-limiter.replenish-rate-header | `+++X-RateLimit-Replenish-Rate+++` | The name of the header that returns the replenish rate configuration.
@@ -342,7 +343,7 @@
|spring.cloud.gateway.server.webflux.route-refresh-listener.enabled | `+++true+++` | If RouteRefreshListener should be turned on.
|spring.cloud.gateway.server.webflux.routes | | List of Routes.
|spring.cloud.gateway.server.webflux.set-status.original-status-header-name | | The name of the header which contains http code of the proxied request.
|spring.cloud.gateway.server.webflux.streaming-media-types | |
|spring.cloud.gateway.server.webflux.streaming-media-types | |
|spring.cloud.gateway.server.webflux.x-forwarded.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
|spring.cloud.gateway.server.webflux.x-forwarded.for-enabled | `+++true+++` | If X-Forwarded-For is enabled.
@@ -383,10 +384,10 @@
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.prefix-enabled | `+++true+++` | If X-Forwarded-Prefix is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.proto-append | `+++true+++` | If appending X-Forwarded-Proto as a list is enabled.
|spring.cloud.gateway.server.webmvc.x-forwarded-request-headers-filter.proto-enabled | `+++true+++` | If X-Forwarded-Proto is enabled.
|spring.cloud.gateway.set-status | |
|spring.cloud.gateway.set-status | |
|spring.cloud.gateway.set-status.original-status-header-name | | The name of the header which contains http code of the proxied request.
|spring.cloud.gateway.streaming-media-types | |
|spring.cloud.gateway.x-forwarded | |
|spring.cloud.gateway.trusted-proxies | | Regular expression defining proxies that are trusted when they appear in a Forwarded or X-Forwarded header.
|spring.cloud.gateway.x-forwarded.enabled | `+++true+++` | If the XForwardedHeadersFilter is enabled.
|spring.cloud.gateway.x-forwarded.for-append | `+++true+++` | If appending X-Forwarded-For as a list is enabled.
|spring.cloud.gateway.x-forwarded.for-enabled | `+++true+++` | If X-Forwarded-For is enabled.

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
</parent>
<artifactId>spring-cloud-gateway-docs</artifactId>
<packaging>jar</packaging>

12
pom.xml
View File

@@ -6,7 +6,7 @@
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Spring Cloud Gateway</name>
@@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath/>
</parent>
<scm>
@@ -55,10 +55,10 @@
<blockhound.version>1.0.8.RELEASE</blockhound.version>
<java.version>17</java.version>
<junit-pioneer.version>2.3.0</junit-pioneer.version>
<spring-cloud-circuitbreaker.version>3.3.0-SNAPSHOT</spring-cloud-circuitbreaker.version>
<spring-cloud-commons.version>4.3.0-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-function.version>4.3.0-SNAPSHOT</spring-cloud-function.version>
<spring-cloud-stream.version>4.3.0-SNAPSHOT</spring-cloud-stream.version>
<spring-cloud-circuitbreaker.version>3.3.1-SNAPSHOT</spring-cloud-circuitbreaker.version>
<spring-cloud-commons.version>4.3.1-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-function.version>4.3.1-SNAPSHOT</spring-cloud-function.version>
<spring-cloud-stream.version>4.3.1-SNAPSHOT</spring-cloud-stream.version>
</properties>
<dependencyManagement>

View File

@@ -6,12 +6,12 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-gateway-dependencies</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<packaging>pom</packaging>
<name>spring-cloud-gateway-dependencies</name>

View File

@@ -18,7 +18,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>

View File

@@ -16,7 +16,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>

View File

@@ -16,7 +16,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>

View File

@@ -16,7 +16,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-integration-tests</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>

View File

@@ -16,7 +16,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-proxyexchange-webflux</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-proxyexchange-webmvc</artifactId>

View File

@@ -16,7 +16,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>

View File

@@ -22,7 +22,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-server-mvc</artifactId>

View File

@@ -32,9 +32,9 @@ import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.http.client.HttpRedirects;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.cloud.gateway.server.mvc.common.ArgumentSupplierBeanPostProcessor;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcAotRuntimeHintsRegistrar;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcPropertiesBeanDefinitionRegistrar;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcRuntimeHintsProcessor;
import org.springframework.cloud.gateway.server.mvc.config.RouterFunctionHolderFactory;
import org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.filter.FilterBeanFactoryDiscoverer;
@@ -47,6 +47,7 @@ import org.springframework.cloud.gateway.server.mvc.filter.RemoveHopByHopRequest
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHopByHopResponseHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.RemoveHttp2StatusResponseHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.TransferEncodingNormalizationRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.TrustedProxies;
import org.springframework.cloud.gateway.server.mvc.filter.WeightCalculatorFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter;
import org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilterProperties;
@@ -59,8 +60,8 @@ import org.springframework.cloud.gateway.server.mvc.predicate.PredicateBeanFacto
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscoverer;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
@@ -80,7 +81,6 @@ import org.springframework.web.client.RestClient;
PredicateAutoConfiguration.class })
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".enabled", matchIfMissing = true)
@Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class)
@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class)
public class GatewayServerMvcAutoConfiguration {
@Bean
@@ -122,10 +122,9 @@ public class GatewayServerMvcAutoConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = GatewayMvcProperties.PREFIX, name = "forwarded-request-headers-filter.enabled",
matchIfMissing = true)
public ForwardedRequestHeadersFilter forwardedRequestHeadersFilter() {
return new ForwardedRequestHeadersFilter();
@Conditional(TrustedProxies.ForwardedTrustedProxiesCondition.class)
public ForwardedRequestHeadersFilter forwardedRequestHeadersFilter(GatewayMvcProperties properties) {
return new ForwardedRequestHeadersFilter(properties.getTrustedProxies());
}
@Bean
@@ -199,9 +198,10 @@ public class GatewayServerMvcAutoConfiguration {
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = XForwardedRequestHeadersFilterProperties.PREFIX, name = ".enabled",
matchIfMissing = true)
public XForwardedRequestHeadersFilter xForwardedRequestHeadersFilter(
XForwardedRequestHeadersFilterProperties props) {
return new XForwardedRequestHeadersFilter(props);
@Conditional(TrustedProxies.XForwardedTrustedProxiesCondition.class)
public XForwardedRequestHeadersFilter xForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props,
GatewayMvcProperties gatewayMvcProperties) {
return new XForwardedRequestHeadersFilter(props, gatewayMvcProperties.getTrustedProxies());
}
@Bean
@@ -209,6 +209,11 @@ public class GatewayServerMvcAutoConfiguration {
return new XForwardedRequestHeadersFilterProperties();
}
@Bean
static GatewayMvcRuntimeHintsProcessor gatewayMvcRuntimeHintsProcessor() {
return new GatewayMvcRuntimeHintsProcessor();
}
static class GatewayHttpClientEnvironmentPostProcessor implements EnvironmentPostProcessor {
static final boolean APACHE = ClassUtils.isPresent("org.apache.hc.client5.http.impl.classic.HttpClients", null);

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2013-2024 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.cloud.gateway.server.mvc.config;
import java.util.Arrays;
import java.util.Set;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.BodyFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions;
import org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates;
import org.springframework.lang.NonNull;
import org.springframework.util.ClassUtils;
/**
* AOT runtime hints registrar on the gateway server mvc.
*
* @author Jürgen Wißkirchen
*/
public class GatewayMvcAotRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
// TODO: fix AOT HINTS
private static final Set<Class<?>> FUNCTION_PROVIDERS = Set.of(HandlerFunctions.class,
FilterAutoConfiguration.LoadBalancerHandlerConfiguration.class, FilterFunctions.class,
BeforeFilterFunctions.class, AfterFilterFunctions.class, TokenRelayFilterFunctions.class,
BodyFilterFunctions.class, CircuitBreakerFilterFunctions.class, GatewayRouterFunctions.class,
LoadBalancerFilterFunctions.class, GatewayRequestPredicates.class, Bucket4jFilterFunctions.class);
private static final Set<Class<?>> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class,
RouteProperties.class);
@Override
public void registerHints(@NonNull RuntimeHints hints, ClassLoader classLoader) {
final ReflectionHints reflectionHints = hints.reflection();
FUNCTION_PROVIDERS.forEach(clazz -> addHintsForClass(reflectionHints, clazz, classLoader));
PROPERTIES.forEach(clazz -> reflectionHints.registerType(clazz, MemberCategory.PUBLIC_FIELDS,
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
}
/**
* Add hints for the given class. Since we need to register mostly static methods, the
* annotation way with @Reflective does not work here.
* @param reflectionHints the reflection hints
* @param clazz the class to add hints for
* @param classLoader the class loader
*/
private void addHintsForClass(ReflectionHints reflectionHints, Class<?> clazz, ClassLoader classLoader) {
if (!ClassUtils.isPresent(clazz.getName(), classLoader)) {
return; // safety net
}
Arrays.stream(clazz.getMethods())
.forEach(method -> reflectionHints.registerMethod(method, ExecutableMode.INVOKE));
}
}

View File

@@ -66,6 +66,12 @@ public class GatewayMvcProperties {
*/
private int streamingBufferSize = 16384;
/**
* Regular expression defining proxies that are trusted when they appear in a
* Forwarded of X-Forwarded header.
*/
private String trustedProxies;
public List<RouteProperties> getRoutes() {
return routes;
}
@@ -102,6 +108,14 @@ public class GatewayMvcProperties {
this.streamingBufferSize = streamingBufferSize;
}
public String getTrustedProxies() {
return trustedProxies;
}
public void setTrustedProxies(String trustedProxies) {
this.trustedProxies = trustedProxies;
}
@Override
public String toString() {
return new ToStringCreator(this).append("httpClient", httpClient)
@@ -109,6 +123,7 @@ public class GatewayMvcProperties {
.append("routesMap", routesMap)
.append("streamingMediaTypes", streamingMediaTypes)
.append("streamingBufferSize", streamingBufferSize)
.append("trustedProxies", trustedProxies)
.toString();
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright 2013-2025 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.cloud.gateway.server.mvc.config;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.FilterAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
/**
* A {@link BeanFactoryInitializationAotProcessor} responsible for registering reflection
* hints for Gateway MVC beans.
*
* @author Jürgen Wißkirchen
* @author Olga Maciaszek-Sharma
* @since 4.3.0
*/
public class GatewayMvcRuntimeHintsProcessor implements BeanFactoryInitializationAotProcessor {
private static final Log LOG = LogFactory.getLog(GatewayMvcRuntimeHintsProcessor.class);
private static final String GATEWAY_MVC_FILTER_PACKAGE_NAME = "org.springframework.cloud.gateway.server.mvc.filter";
private static final String GATEWAY_MVC_PREDICATE_PACKAGE_NAME = "org.springframework.cloud.gateway.server.mvc.predicate";
private static final Map<String, Set<String>> beansConditionalOnClasses = Map.of(
"io.github.bucket4j.BucketConfiguration",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions"),
"org.springframework.cloud.client.circuitbreaker.CircuitBreaker",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions"),
"org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions"),
"org.springframework.retry.support.RetryTemplate",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions"),
"org.springframework.security.oauth2.client.OAuth2AuthorizedClient",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions"));
private static final Set<Class<?>> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class,
RouteProperties.class);
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
return (generationContext, beanFactoryInitializationCode) -> {
ReflectionHints hints = generationContext.getRuntimeHints().reflection();
Set<Class<?>> typesToRegister = Stream
.of(getTypesToRegister(GATEWAY_MVC_FILTER_PACKAGE_NAME),
getTypesToRegister(GATEWAY_MVC_PREDICATE_PACKAGE_NAME), PROPERTIES,
new HashSet<>(Collections.singletonList(FilterFunctions.class)))
.flatMap(Set::stream)
.collect(Collectors.toSet());
typesToRegister.forEach(clazz -> hints.registerType(TypeReference.of(clazz),
hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)));
};
}
private static Set<Class<?>> getTypesToRegister(String packageName) {
Set<Class<?>> classesToAdd = new HashSet<>();
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(Object.class));
provider.addExcludeFilter(new AssignableTypeFilter(FilterAutoConfiguration.class));
provider.addExcludeFilter(new AssignableTypeFilter(PredicateAutoConfiguration.class));
Set<BeanDefinition> components = provider.findCandidateComponents(packageName);
for (BeanDefinition component : components) {
Class<?> clazz;
try {
clazz = Class.forName(component.getBeanClassName());
if (shouldRegisterClass(clazz)) {
classesToAdd.add(clazz);
}
}
catch (NoClassDefFoundError | ClassNotFoundException exception) {
if (LOG.isDebugEnabled()) {
LOG.debug(exception);
}
}
}
return classesToAdd;
}
private static boolean shouldRegisterClass(Class<?> clazz) {
Set<String> conditionClasses = beansConditionalOnClasses.getOrDefault(clazz.getName(), Collections.emptySet());
for (String conditionClass : conditionClasses) {
try {
GatewayMvcRuntimeHintsProcessor.class.getClassLoader().loadClass(conditionClass);
}
catch (ClassNotFoundException e) {
return false;
}
}
return true;
}
}

View File

@@ -24,7 +24,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
@@ -34,11 +39,26 @@ import org.springframework.web.servlet.function.ServerRequest;
public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestHttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(ForwardedRequestHeadersFilter.class);
/**
* Forwarded header.
*/
public static final String FORWARDED_HEADER = "Forwarded";
private final TrustedProxies trustedProxies;
@Deprecated
public ForwardedRequestHeadersFilter() {
trustedProxies = s -> true;
log.warn(GatewayMvcProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to Forwarded header.");
}
public ForwardedRequestHeadersFilter(String trustedProxiesRegex) {
trustedProxies = TrustedProxies.from(trustedProxiesRegex);
}
/* for testing */
static List<Forwarded> parse(List<String> values) {
ArrayList<Forwarded> forwardeds = new ArrayList<>();
@@ -46,8 +66,11 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
return forwardeds;
}
for (String value : values) {
Forwarded forwarded = parse(value);
forwardeds.add(forwarded);
String[] forwardedValues = StringUtils.tokenizeToStringArray(value, ",");
for (String forwardedValue : forwardedValues) {
Forwarded forwarded = parse(forwardedValue);
forwardeds.add(forwarded);
}
}
return forwardeds;
}
@@ -89,6 +112,13 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
@Override
public HttpHeaders apply(HttpHeaders input, ServerRequest request) {
if (request.servletRequest().getRemoteAddr() != null
&& !trustedProxies.isTrusted(request.servletRequest().getRemoteAddr())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.servletRequest().getRemoteHost()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -102,7 +132,10 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
List<Forwarded> forwardeds = parse(original.get(FORWARDED_HEADER));
for (Forwarded f : forwardeds) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
// only add if "for" value matches trustedProxies
if (trustedProxies.isTrusted(f.get("for"))) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
}
}
// TODO: add new forwarded
@@ -124,6 +157,10 @@ public class ForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestH
forValue = "[" + forValue + "]";
}
}
if (!trustedProxies.isTrusted(forValue)) {
// don't add for value
return;
}
int port = remoteAddress.getPort();
if (port >= 0) {
forValue = forValue + ":" + port;

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2013-2025 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.cloud.gateway.server.mvc.filter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@FunctionalInterface
public interface TrustedProxies {
boolean isTrusted(String host);
static TrustedProxies from(@NonNull String trustedProxies) {
Assert.hasText(trustedProxies, "trustedProxies must not be empty");
Pattern pattern = Pattern.compile(trustedProxies);
return value -> pattern.matcher(value).matches();
}
class ForwardedTrustedProxiesCondition extends AllNestedConditions {
public ForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".forwarded-request-headers-filter.enabled",
matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists(GatewayMvcProperties.PREFIX + ".trusted-proxies")
static class OnTrustedProxiesNotEmpty {
}
}
class XForwardedTrustedProxiesCondition extends AllNestedConditions {
public XForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = XForwardedRequestHeadersFilterProperties.PREFIX + ".enabled",
matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists(GatewayMvcProperties.PREFIX + ".trusted-proxies")
static class OnTrustedProxiesNotEmpty {
}
}
class OnPropertyExistsCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
try {
String value = metadata.getAnnotations().get(ConditionalOnPropertyExists.class).getString("value");
String property = context.getEnvironment().getProperty(value);
if (!StringUtils.hasText(property)) {
return ConditionOutcome.noMatch(value + " property is not set or is empty.");
}
return ConditionOutcome.match(value + " property is not empty.");
}
catch (NoSuchElementException e) {
return ConditionOutcome.noMatch("Missing required property 'value' of @ConditionalOnPropertyExists");
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyExistsCondition.class)
@interface ConditionalOnPropertyExists {
/**
* @return the property
*/
String value();
}
}

View File

@@ -16,21 +16,29 @@
package org.springframework.cloud.gateway.server.mvc.filter;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.server.mvc.common.MvcUtils;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.function.ServerRequest;
public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.RequestHttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(XForwardedRequestHeadersFilter.class);
/** Default http port. */
public static final int HTTP_PORT = 80;
@@ -60,8 +68,25 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
private final XForwardedRequestHeadersFilterProperties properties;
private final TrustedProxies trustedProxies;
@Deprecated
public XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties properties) {
this.properties = properties;
this(properties, s -> true);
log.warn(GatewayMvcProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to X-Forwarded header.");
}
public XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props, String trustedProxies) {
this(props, TrustedProxies.from(trustedProxies));
}
private XForwardedRequestHeadersFilter(XForwardedRequestHeadersFilterProperties props,
TrustedProxies trustedProxies) {
Assert.notNull(props, "XForwardedRequestHeadersFilterProperties must not be null");
Assert.notNull(trustedProxies, "trustedProxies must not be null");
this.properties = props;
this.trustedProxies = trustedProxies;
}
@Override
@@ -71,6 +96,13 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
@Override
public HttpHeaders apply(HttpHeaders input, ServerRequest request) {
if (request.servletRequest().getRemoteAddr() != null
&& !trustedProxies.isTrusted(request.servletRequest().getRemoteAddr())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.servletRequest().getRemoteHost()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -78,10 +110,12 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
updated.addAll(entry.getKey(), entry.getValue());
}
InetSocketAddress remoteAddress = request.remoteAddress().orElse(null);
if (properties.isForEnabled() && remoteAddress != null && remoteAddress.getAddress() != null) {
String remoteAddr = remoteAddress.getAddress().getHostAddress();
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, properties.isForAppend());
if (properties.isForEnabled()) {
String remoteAddr = null;
if (request.servletRequest().getRemoteAddr() != null) {
remoteAddr = request.servletRequest().getRemoteAddr();
}
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, properties.isForAppend(), trustedProxies::isTrusted);
}
String proto = request.uri().getScheme();
@@ -156,17 +190,22 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
}
private void write(HttpHeaders headers, String name, String value, boolean append) {
if (value == null) {
return;
}
write(headers, name, value, append, s -> true);
}
private void write(HttpHeaders headers, String name, String value, boolean append, Predicate<String> shouldWrite) {
if (append) {
headers.add(name, value);
if (value != null) {
headers.add(name, value);
}
// these headers should be treated as a single comma separated header
List<String> values = headers.get(name);
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
if (headers.containsKey(name)) {
List<String> values = headers.get(name).stream().filter(shouldWrite).toList();
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
}
}
else {
else if (value != null && shouldWrite.test(value)) {
headers.set(name, value);
}
}
@@ -175,11 +214,6 @@ public class XForwardedRequestHeadersFilter implements HttpHeadersFilter.Request
return HTTPS_SCHEME.equals(scheme) ? HTTPS_PORT : HTTP_PORT;
}
private boolean hasHeader(ServerRequest request, String name) {
HttpHeaders headers = request.headers().asHttpHeaders();
return headers.containsKey(name) && StringUtils.hasLength(headers.getFirst(name));
}
private String toHostHeader(ServerRequest request) {
int port = request.uri().getPort();
String host = request.uri().getHost();

View File

@@ -365,6 +365,18 @@
"defaultValue": "jdk",
"deprecated": true,
"deprecation": {}
},
{
"name": "spring.cloud.gateway.mvc.trusted-proxies",
"type": "java.lang.String",
"description": "Regular expression defining proxies that are trusted when they appear in a Forwarded or X-Forwarded header.",
"sourceType": "org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webmvc.trusted-proxies",
"since": "4.3.0"
}
}
]
}

View File

@@ -23,10 +23,20 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter.Forwarded;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@@ -52,6 +62,31 @@ public class ForwardedRequestHeadersFilterTests {
return map;
}
@Test
public void trustedProxiesConditionMatches() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class, RestClientAutoConfiguration.class,
SslAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
GatewayServerMvcAutoConfiguration.class, FilterAutoConfiguration.class,
PredicateAutoConfiguration.class))
.withPropertyValues(GatewayMvcProperties.PREFIX + ".trusted-proxies=11\\.0\\.0\\..*")
.run(context -> {
assertThat(context).hasSingleBean(ForwardedRequestHeadersFilter.class);
});
}
@Test
public void trustedProxiesConditionDoesNotMatch() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class, RestClientAutoConfiguration.class,
SslAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
GatewayServerMvcAutoConfiguration.class, FilterAutoConfiguration.class,
PredicateAutoConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(ForwardedRequestHeadersFilter.class);
});
}
@Test
public void forwardedHeaderDoesNotExist() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
@@ -61,7 +96,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -81,28 +116,39 @@ public class ForwardedRequestHeadersFilterTests {
public void forwardedHeaderExists() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https; for=23.45.67.89")
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers.get(FORWARDED_HEADER)).hasSize(2);
assertThat(headers.get(FORWARDED_HEADER)).hasSize(3);
List<Forwarded> forwardeds = ForwardedRequestHeadersFilter.parse(headers.get(FORWARDED_HEADER));
assertThat(forwardeds).hasSize(2);
Forwarded addedForwardedHeader = forwardeds.get(0);
Forwarded existingForwardedHeader = forwardeds.get(1);
assertThat(existingForwardedHeader.getValues()).containsEntry("proto", "http")
.containsEntry("for", "\"10.0.0.1:80\"");
assertThat(addedForwardedHeader.getValues()).containsEntry("proto", "https")
.containsEntry("for", "23.45.67.89");
assertThat(forwardeds).hasSize(3);
Optional<Forwarded> added = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").contains("10.0.0.1:80"))
.findFirst();
assertThat(added).isPresent();
added.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "http").containsEntry("for", "\"10.0.0.1:80\"");
});
Optional<Forwarded> existing = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").equals("23.45.67.89"))
.findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("for", "23.45.67.89");
});
existing = forwardeds.stream().filter(forwarded -> forwarded.get("for").equals("12.34.56.78")).findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "https").containsEntry("for", "12.34.56.78");
});
}
@Test
@@ -113,7 +159,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -136,7 +182,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("2001:db8:cafe:0:0:0:0:17");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -158,7 +204,7 @@ public class ForwardedRequestHeadersFilterTests {
servletRequest.setRemoteHost("unresolvable-hostname");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter();
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter(".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
@@ -211,4 +257,62 @@ public class ForwardedRequestHeadersFilterTests {
}
}
@Test
public void forwardedHeadersNotTrusted() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter("11\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).doesNotContainKeys(FORWARDED_HEADER);
}
// verify that existing forwarded header is not forwarded if not trusted
@Test
public void untrustedForwardedForNotAppended() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=\"127.0.0.1:80\",for=10.0.0.11")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
filtered = forwardedHeaders.stream().filter(value -> value.contains("10.0.0.11")).findFirst();
assertThat(filtered).isNotEmpty();
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=127.0.0.1")
.buildRequest(null);
servletRequest.setRemoteAddr(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
ForwardedRequestHeadersFilter filter = new ForwardedRequestHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
}
}

View File

@@ -0,0 +1,165 @@
/*
* Copyright 2013-2023 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.cloud.gateway.server.mvc.filter;
import java.util.Collections;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.servlet.function.ServerRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER;
import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER;
/**
* @author Spencer Gibb
*/
public class XForwardedRequestHeadersFilterTests {
@Test
public void remoteAddressIsNull() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.header(HttpHeaders.HOST, "myhost")
.buildRequest(null);
servletRequest.setRemoteAddr(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), ".*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER)
.containsKeys(X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER, X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_HOST_HEADER)).isEqualTo("myhost");
assertThat(headers.getFirst(X_FORWARDED_PORT_HEADER)).isEqualTo("80");
assertThat(headers.getFirst(X_FORWARDED_PROTO_HEADER)).isEqualTo("http");
}
@Test
public void trustedProxiesConditionMatches() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class, RestClientAutoConfiguration.class,
SslAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
GatewayServerMvcAutoConfiguration.class, FilterAutoConfiguration.class,
PredicateAutoConfiguration.class))
.withPropertyValues(GatewayMvcProperties.PREFIX + ".trusted-proxies=11\\.0\\.0\\..*")
.run(context -> {
assertThat(context).hasSingleBean(XForwardedRequestHeadersFilter.class);
});
}
@Test
public void trustedProxiesConditionDoesNotMatch() {
new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebMvcAutoConfiguration.class, RestClientAutoConfiguration.class,
SslAutoConfiguration.class, ServletWebServerFactoryAutoConfiguration.class,
GatewayServerMvcAutoConfiguration.class, FilterAutoConfiguration.class,
PredicateAutoConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(XForwardedRequestHeadersFilter.class);
});
}
@Test
public void emptyTrustedProxiesFails() {
Assertions
.assertThatThrownBy(
() -> new XForwardedRequestHeadersFilter(new XForwardedRequestHeadersFilterProperties(), ""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void xForwardedHeadersNotTrusted() throws Exception {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), "11\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
}
// verify that existing forwarded header is not forwarded if not trusted
@Test
public void untrustedXForwardedForNotAppended() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.remoteAddress("10.0.0.1:80")
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.header(X_FORWARDED_FOR_HEADER, "10.0.0.10")
.buildRequest(null);
servletRequest.setRemoteHost("10.0.0.1");
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), "10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1")
.contains("10.0.0.1", "10.0.0.10");
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() {
MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost/get")
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.buildRequest(null);
servletRequest.setRemoteAddr(null);
ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList());
XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter(
new XForwardedRequestHeadersFilterProperties(), "10\\.0\\.0\\..*");
HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request);
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1");
}
}

View File

@@ -22,7 +22,11 @@ import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
public class TestUtils {
public final class TestUtils {
private TestUtils() {
}
public static Map<String, Object> getMap(Map<String, Object> map, String mapKey) {
assertThat(map).isNotEmpty().containsKey(mapKey);

View File

@@ -6,4 +6,8 @@ logging:
org.springframework.retry: TRACE
spring:
mvc:
log-request-details: true
log-request-details: true
cloud:
gateway:
mvc:
trusted-proxies: .*

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-server-webflux</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-server-webmvc</artifactId>

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-gateway-server</artifactId>

View File

@@ -0,0 +1,137 @@
/*
* Copyright 2013-2024 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.
*/
/*
* Copyright (c) 2020-2023 VMware, Inc. or its affiliates, All Rights Reserved.
*
* 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.cloud.gateway.config;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import io.netty.handler.codec.http.HttpRequest;
import reactor.netty.http.server.ConnectionInfo;
import reactor.netty.transport.AddressUtils;
import static reactor.netty.http.server.ConnectionInfo.getDefaultHostPort;
/**
* Default implementation for handling {@code X-Forwarded}/{@code Forwarded} headers.
*
* @author Andrey Shlykov
* @since 0.9.12
*/
final class DefaultNettyHttpForwardedHeaderHandler implements BiFunction<ConnectionInfo, HttpRequest, ConnectionInfo> {
static final DefaultNettyHttpForwardedHeaderHandler INSTANCE = new DefaultNettyHttpForwardedHeaderHandler();
static final String FORWARDED_HEADER = "Forwarded";
static final String X_FORWARDED_IP_HEADER = "X-Forwarded-For";
static final String X_FORWARDED_HOST_HEADER = "X-Forwarded-Host";
static final String X_FORWARDED_PORT_HEADER = "X-Forwarded-Port";
static final String X_FORWARDED_PROTO_HEADER = "X-Forwarded-Proto";
static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?");
static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?");
static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("for=\"?([^;,\"]+)\"?");
/**
* Specifies whether the Http Server applies a strict {@code Forwarded} header
* validation. By default, it is enabled and strict validation is used.
* @since 1.0.8
* @deprecated The system property is used for backwards compatibility and will be
* removed in version 1.2.0.
*/
@Deprecated
static final String FORWARDED_HEADER_VALIDATION = "reactor.netty.http.server.forwarded.strictValidation";
static final boolean DEFAULT_FORWARDED_HEADER_VALIDATION = Boolean
.parseBoolean(System.getProperty(FORWARDED_HEADER_VALIDATION, "true"));
@Override
public ConnectionInfo apply(ConnectionInfo connectionInfo, HttpRequest request) {
String forwardedHeader = request.headers().get(FORWARDED_HEADER);
if (forwardedHeader != null) {
return parseForwardedInfo(connectionInfo, forwardedHeader);
}
return parseXForwardedInfo(connectionInfo, request);
}
private ConnectionInfo parseForwardedInfo(ConnectionInfo connectionInfo, String forwardedHeader) {
String forwarded = forwardedHeader.split(",", 2)[0];
Matcher protoMatcher = FORWARDED_PROTO_PATTERN.matcher(forwarded);
if (protoMatcher.find()) {
connectionInfo = connectionInfo.withScheme(protoMatcher.group(1).trim());
}
Matcher hostMatcher = FORWARDED_HOST_PATTERN.matcher(forwarded);
if (hostMatcher.find()) {
connectionInfo = connectionInfo.withHostAddress(AddressUtils.parseAddress(hostMatcher.group(1),
getDefaultHostPort(connectionInfo.getScheme()), DEFAULT_FORWARDED_HEADER_VALIDATION));
}
Matcher forMatcher = FORWARDED_FOR_PATTERN.matcher(forwarded);
if (forMatcher.find()) {
connectionInfo = connectionInfo.withRemoteAddress(AddressUtils.parseAddress(forMatcher.group(1).trim(),
connectionInfo.getRemoteAddress().getPort(), DEFAULT_FORWARDED_HEADER_VALIDATION));
}
return connectionInfo;
}
private ConnectionInfo parseXForwardedInfo(ConnectionInfo connectionInfo, HttpRequest request) {
String ipHeader = request.headers().get(X_FORWARDED_IP_HEADER);
if (ipHeader != null) {
connectionInfo = connectionInfo.withRemoteAddress(
AddressUtils.parseAddress(ipHeader.split(",", 2)[0], connectionInfo.getRemoteAddress().getPort()));
}
String protoHeader = request.headers().get(X_FORWARDED_PROTO_HEADER);
if (protoHeader != null) {
connectionInfo = connectionInfo.withScheme(protoHeader.split(",", 2)[0].trim());
}
String hostHeader = request.headers().get(X_FORWARDED_HOST_HEADER);
if (hostHeader != null) {
connectionInfo = connectionInfo
.withHostAddress(AddressUtils.parseAddress(hostHeader.split(",", 2)[0].trim(),
getDefaultHostPort(connectionInfo.getScheme()), DEFAULT_FORWARDED_HEADER_VALIDATION));
}
String portHeader = request.headers().get(X_FORWARDED_PORT_HEADER);
if (portHeader != null && !portHeader.isEmpty()) {
String portStr = portHeader.split(",", 2)[0].trim();
if (portStr.chars().allMatch(Character::isDigit)) {
int port = Integer.parseInt(portStr);
connectionInfo = connectionInfo.withHostAddress(
AddressUtils.createUnresolved(connectionInfo.getHostAddress().getHostString(), port),
connectionInfo.getHostName(), port);
}
else if (DEFAULT_FORWARDED_HEADER_VALIDATION) {
throw new IllegalArgumentException("Failed to parse a port from " + portHeader);
}
}
return connectionInfo;
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.cloud.gateway.config;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
@@ -62,6 +63,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.embedded.netty.NettyServerCustomizer;
import org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint;
import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint;
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledFilter;
@@ -127,6 +129,7 @@ import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilte
import org.springframework.cloud.gateway.filter.headers.HttpHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.RemoveHopByHopHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.TransferEncodingNormalizationHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.TrustedProxies;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.ratelimit.Bucket4jRateLimiter;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
@@ -319,11 +322,12 @@ public class GatewayAutoConfiguration {
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.forwarded.enabled", matchIfMissing = true)
public ForwardedHeadersFilter forwardedHeadersFilter(Environment env, ServerProperties serverProperties) {
@Conditional(TrustedProxies.ForwardedTrustedProxiesCondition.class)
public ForwardedHeadersFilter forwardedHeadersFilter(Environment env, ServerProperties serverProperties,
GatewayProperties properties) {
boolean forwardedByEnabled = env.getProperty("spring.cloud.gateway.server.webflux.forwarded.by.enabled",
Boolean.class, false);
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter();
ForwardedHeadersFilter forwardedHeadersFilter = new ForwardedHeadersFilter(properties.getTrustedProxies());
forwardedHeadersFilter.setForwardedByEnabled(forwardedByEnabled);
forwardedHeadersFilter.setServerPort(serverProperties.getPort());
return forwardedHeadersFilter;
@@ -337,9 +341,9 @@ public class GatewayAutoConfiguration {
}
@Bean
@ConditionalOnProperty(name = "spring.cloud.gateway.server.webflux.x-forwarded.enabled", matchIfMissing = true)
public XForwardedHeadersFilter xForwardedHeadersFilter() {
return new XForwardedHeadersFilter();
@Conditional(TrustedProxies.XForwardedTrustedProxiesCondition.class)
public XForwardedHeadersFilter xForwardedHeadersFilter(GatewayProperties properties) {
return new XForwardedHeadersFilter(properties.getTrustedProxies());
}
@Bean
@@ -790,6 +794,21 @@ public class GatewayAutoConfiguration {
};
}
@Bean
@TrustedProxies.ConditionalOnPropertyExists
public NettyServerCustomizer gatewayNettyServerCustomizer(GatewayProperties gatewayProperties) {
TrustedProxies trustedProxies = TrustedProxies.from(gatewayProperties.getTrustedProxies());
return httpServer -> httpServer.forwarded((connectionInfo, httpRequest) -> {
InetSocketAddress remoteAddress = connectionInfo.getRemoteAddress();
if (remoteAddress != null && trustedProxies.isTrusted(remoteAddress.getHostString())) {
// update remote address
return DefaultNettyHttpForwardedHeaderHandler.INSTANCE.apply(connectionInfo, httpRequest);
}
return connectionInfo;
});
}
@Bean
public HttpClientSslConfigurer httpClientSslConfigurer(ServerProperties serverProperties,
HttpClientProperties httpClientProperties, SslBundles bundles) {

View File

@@ -73,6 +73,12 @@ public class GatewayProperties {
*/
private boolean routeFilterCacheEnabled = false;
/**
* Regular expression defining proxies that are trusted when they appear in a
* Forwarded or X-Forwarded header.
*/
private String trustedProxies;
public boolean isRouteFilterCacheEnabled() {
return routeFilterCacheEnabled;
}
@@ -116,6 +122,14 @@ public class GatewayProperties {
this.failOnRouteDefinitionError = failOnRouteDefinitionError;
}
public String getTrustedProxies() {
return trustedProxies;
}
public void setTrustedProxies(String trustedProxies) {
this.trustedProxies = trustedProxies;
}
@Override
public String toString() {
return new ToStringCreator(this).append("routes", routes)
@@ -123,6 +137,7 @@ public class GatewayProperties {
.append("streamingMediaTypes", streamingMediaTypes)
.append("failOnRouteDefinitionError", failOnRouteDefinitionError)
.append("routeFilterCacheEnabled", routeFilterCacheEnabled)
.append("trustedProxies", trustedProxies)
.toString();
}

View File

@@ -29,7 +29,9 @@ import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.CollectionUtils;
@@ -44,6 +46,8 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(ForwardedHeadersFilter.class);
private Integer serverPort;
private final Log logger = LogFactory.getLog(getClass());
@@ -55,6 +59,19 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
*/
public static final String FORWARDED_HEADER = "Forwarded";
private final TrustedProxies trustedProxies;
@Deprecated
public ForwardedHeadersFilter() {
trustedProxies = s -> true;
log.warn(GatewayProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to Forwarded header.");
}
public ForwardedHeadersFilter(String trustedProxiesRegex) {
trustedProxies = TrustedProxies.from(trustedProxiesRegex);
}
/* for testing */
static List<Forwarded> parse(List<String> values) {
ArrayList<Forwarded> forwardeds = new ArrayList<>();
@@ -62,8 +79,11 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
return forwardeds;
}
for (String value : values) {
Forwarded forwarded = parse(value);
forwardeds.add(forwarded);
String[] forwardedValues = StringUtils.tokenizeToStringArray(value, ",");
for (String forwardedValue : forwardedValues) {
Forwarded forwarded = parse(forwardedValue);
forwardeds.add(forwarded);
}
}
return forwardeds;
}
@@ -114,6 +134,14 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
@Override
public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
if (request.getRemoteAddress() != null
&& !trustedProxies.isTrusted(request.getRemoteAddress().getHostString())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.getRemoteAddress()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -127,7 +155,10 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
List<Forwarded> forwardeds = parse(original.get(FORWARDED_HEADER));
for (Forwarded f : forwardeds) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
// only add if "for" value matches trustedProxies
if (trustedProxies.isTrusted(f.get("for"))) {
updated.add(FORWARDED_HEADER, f.toHeaderValue());
}
}
// TODO: add new forwarded
@@ -136,6 +167,7 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
Forwarded forwarded = new Forwarded().put("host", host).put("proto", uri.getScheme());
InetSocketAddress remoteAddress = request.getRemoteAddress();
// TODO: only add if "remoteAddress" value matches trustedProxies
if (remoteAddress != null) {
// If remoteAddress is unresolved, calling getHostAddress() would cause a
// NullPointerException.
@@ -150,11 +182,14 @@ public class ForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
forValue = "[" + forValue + "]";
}
}
int port = remoteAddress.getPort();
if (port >= 0) {
forValue = forValue + ":" + port;
if (trustedProxies.isTrusted(forValue)) {
// only add for value if trusted
int port = remoteAddress.getPort();
if (port >= 0) {
forValue = forValue + ":" + port;
}
forwarded.put("for", forValue);
}
forwarded.put("for", forValue);
}
if (forwardedByEnabled) {

View File

@@ -0,0 +1,117 @@
/*
* Copyright 2013-2025 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.cloud.gateway.filter.headers;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.NoSuchElementException;
import java.util.regex.Pattern;
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
import org.springframework.boot.autoconfigure.condition.ConditionOutcome;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.SpringBootCondition;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@FunctionalInterface
public interface TrustedProxies {
/**
* Property name.
*/
String PROPERTY = GatewayProperties.PREFIX + ".trusted-proxies";
boolean isTrusted(String host);
static TrustedProxies from(@NonNull String trustedProxies) {
Assert.hasText(trustedProxies, "trustedProxies must not be empty");
Pattern pattern = Pattern.compile(trustedProxies);
return value -> pattern.matcher(value).matches();
}
class ForwardedTrustedProxiesCondition extends AllNestedConditions {
public ForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".forwarded.enabled", matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists
static class OnTrustedProxiesNotEmpty {
}
}
class XForwardedTrustedProxiesCondition extends AllNestedConditions {
public XForwardedTrustedProxiesCondition() {
super(ConfigurationPhase.REGISTER_BEAN);
}
@ConditionalOnProperty(name = GatewayProperties.PREFIX + ".x-forwarded.enabled", matchIfMissing = true)
static class OnPropertyEnabled {
}
@ConditionalOnPropertyExists
static class OnTrustedProxiesNotEmpty {
}
}
class OnPropertyExistsCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
try {
String property = context.getEnvironment().getProperty(PROPERTY);
if (!StringUtils.hasText(property)) {
return ConditionOutcome.noMatch(PROPERTY + " property is not set or is empty.");
}
return ConditionOutcome.match(PROPERTY + " property is not empty.");
}
catch (NoSuchElementException e) {
return ConditionOutcome.noMatch("Missing required property 'value' of @ConditionalOnPropertyExists");
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyExistsCondition.class)
@interface ConditionalOnPropertyExists {
}
}

View File

@@ -20,9 +20,15 @@ import java.net.URI;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.core.Ordered;
import org.springframework.core.log.LogMessage;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.util.ObjectUtils;
@@ -35,6 +41,8 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
@ConfigurationProperties("spring.cloud.gateway.server.webflux.x-forwarded")
public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
private static final Log log = LogFactory.getLog(XForwardedHeadersFilter.class);
/** Default http port. */
public static final int HTTP_PORT = 80;
@@ -98,6 +106,19 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
/** If appending X-Forwarded-Prefix as a list is enabled. */
private boolean prefixAppend = true;
private final TrustedProxies trustedProxies;
@Deprecated
public XForwardedHeadersFilter() {
trustedProxies = s -> true;
log.warn(GatewayProperties.PREFIX
+ ".trusted-proxies is not set. Using deprecated Constructor. Untrusted hosts might be added to Forwarded header.");
}
public XForwardedHeadersFilter(String trustedProxiesRegex) {
trustedProxies = TrustedProxies.from(trustedProxiesRegex);
}
@Override
public int getOrder() {
return this.order;
@@ -197,8 +218,15 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
@Override
public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
if (request.getRemoteAddress() != null
&& !trustedProxies.isTrusted(request.getRemoteAddress().getHostString())) {
log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies,
request.getRemoteAddress()));
return input;
}
HttpHeaders original = input;
HttpHeaders updated = new HttpHeaders();
@@ -206,9 +234,13 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
updated.addAll(entry.getKey(), entry.getValue());
}
if (isForEnabled() && request.getRemoteAddress() != null && request.getRemoteAddress().getAddress() != null) {
String remoteAddr = request.getRemoteAddress().getAddress().getHostAddress();
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend());
if (isForEnabled()) {
String remoteAddr = null;
if (request.getRemoteAddress() != null && request.getRemoteAddress().getAddress() != null) {
remoteAddr = request.getRemoteAddress().getHostString();
}
// match xforwarded for against trusted proxies
write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend(), trustedProxies::isTrusted);
}
String proto = request.getURI().getScheme();
@@ -284,17 +316,22 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
}
private void write(HttpHeaders headers, String name, String value, boolean append) {
if (value == null) {
return;
}
write(headers, name, value, append, s -> true);
}
private void write(HttpHeaders headers, String name, String value, boolean append, Predicate<String> shouldWrite) {
if (append) {
headers.add(name, value);
if (value != null) {
headers.add(name, value);
}
// these headers should be treated as a single comma separated header
List<String> values = headers.get(name);
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
if (headers.containsKey(name)) {
List<String> values = headers.get(name).stream().filter(shouldWrite).toList();
String delimitedValue = StringUtils.collectionToCommaDelimitedString(values);
headers.set(name, delimitedValue);
}
}
else {
else if (value != null && shouldWrite.test(value)) {
headers.set(name, value);
}
}
@@ -303,11 +340,6 @@ public class XForwardedHeadersFilter implements HttpHeadersFilter, Ordered {
return HTTPS_SCHEME.equals(scheme) ? HTTPS_PORT : HTTP_PORT;
}
private boolean hasHeader(ServerHttpRequest request, String name) {
HttpHeaders headers = request.getHeaders();
return headers.containsKey(name) && StringUtils.hasLength(headers.getFirst(name));
}
private String toHostHeader(ServerHttpRequest request) {
int port = request.getURI().getPort();
String host = request.getURI().getHost();

View File

@@ -2432,6 +2432,18 @@
"replacement": "spring.cloud.gateway.server.webflux.x-forwarded.proto-enabled",
"since": "4.3.0"
}
},
{
"name": "spring.cloud.gateway.trusted-proxies",
"type": "java.lang.String",
"description": "Regular expression defining proxies that are trusted when they appear in a Forwarded or X-Forwarded header.",
"sourceType": "org.springframework.cloud.gateway.config.GatewayProperties",
"defaultValue": true,
"deprecated": true,
"deprecation": {
"replacement": "spring.cloud.gateway.server.webflux.trusted-proxies",
"since": "4.3.0"
}
}
]
}

View File

@@ -58,8 +58,10 @@ import org.springframework.cloud.gateway.actuate.GatewayLegacyControllerEndpoint
import org.springframework.cloud.gateway.config.GatewayAutoConfigurationTests.CustomHttpClientFactory.CustomSslConfigurer;
import org.springframework.cloud.gateway.config.HttpClientProperties.Pool.LeasingStrategy;
import org.springframework.cloud.gateway.filter.factory.TokenRelayGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.GRPCRequestHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.GRPCResponseHeadersFilter;
import org.springframework.cloud.gateway.filter.headers.XForwardedHeadersFilter;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.GatewayFilterSpec;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
@@ -206,8 +208,8 @@ public class GatewayAutoConfigurationTests {
"spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"spring.security.oauth2.client.registration[test].client-id=login-client")
.run(context -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class);
assertThat(context).hasSingleBean(TokenRelayGatewayFilterFactory.class);
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class)
.hasSingleBean(TokenRelayGatewayFilterFactory.class);
});
}
@@ -225,8 +227,8 @@ public class GatewayAutoConfigurationTests {
"spring.security.oauth2.client.registration[test].redirect-uri=http://localhost/redirect",
"spring.security.oauth2.client.registration[test].client-id=login-client")
.run(context -> {
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class);
assertThat(context).hasBean("myReactiveOAuth2AuthorizedClientManager");
assertThat(context).hasSingleBean(ReactiveOAuth2AuthorizedClientManager.class)
.hasBean("myReactiveOAuth2AuthorizedClientManager");
});
}
@@ -290,8 +292,8 @@ public class GatewayAutoConfigurationTests {
HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
.withPropertyValues("server.http2.enabled=true")
.run(context -> {
assertThat(context).hasSingleBean(GRPCRequestHeadersFilter.class);
assertThat(context).hasSingleBean(GRPCResponseHeadersFilter.class);
assertThat(context).hasSingleBean(GRPCRequestHeadersFilter.class)
.hasSingleBean(GRPCResponseHeadersFilter.class);
HttpClient httpClient = context.getBean(HttpClient.class);
assertThat(httpClient.configuration().protocols()).contains(HttpProtocol.HTTP11, HttpProtocol.H2);
});
@@ -305,8 +307,8 @@ public class GatewayAutoConfigurationTests {
HttpClientCustomizedConfig.class, ServerPropertiesConfig.class))
.withPropertyValues("server.http2.enabled=false")
.run(context -> {
assertThat(context).doesNotHaveBean(GRPCRequestHeadersFilter.class);
assertThat(context).doesNotHaveBean(GRPCResponseHeadersFilter.class);
assertThat(context).doesNotHaveBean(GRPCRequestHeadersFilter.class)
.doesNotHaveBean(GRPCResponseHeadersFilter.class);
});
}
@@ -337,6 +339,33 @@ public class GatewayAutoConfigurationTests {
});
}
@Test
public void forwardedHeaderFiltersNotEnabledByDefault() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
ServerPropertiesConfig.class))
.run(context -> {
assertThat(context).doesNotHaveBean(XForwardedHeadersFilter.class)
.doesNotHaveBean(ForwardedHeadersFilter.class);
});
}
@Test
public void forwardedHeaderFiltersEnabledWithProperties() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, MetricsAutoConfiguration.class,
SimpleMetricsExportAutoConfiguration.class, GatewayAutoConfiguration.class,
ServerPropertiesConfig.class))
.withPropertyValues("spring.cloud.gateway.server.webflux.forwarded.enabled=true",
"spring.cloud.gateway.server.webflux.x-forwarded.enabled=true",
"spring.cloud.gateway.server.webflux.trusted-proxies=.*")
.run(context -> {
assertThat(context).hasSingleBean(XForwardedHeadersFilter.class)
.hasSingleBean(ForwardedHeadersFilter.class);
});
}
@Configuration
@EnableConfigurationProperties(ServerProperties.class)
@AutoConfigureBefore(GatewayAutoConfiguration.class)

View File

@@ -66,7 +66,9 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
@SpringBootTest(webEnvironment = RANDOM_PORT,
properties = { "spring.cloud.gateway.server.webflux.httpclient.connect-timeout=500",
"spring.cloud.gateway.server.webflux.httpclient.response-timeout=2s",
"logging.level.org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory=TRACE" })
"logging.level.org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory=TRACE",
"spring.cloud.gateway.server.webflux.trusted-proxies=.*",
"spring.cloud.gateway.server.webflux.x-forwarded.enabled=true" })
@DirtiesContext
// default filter AddResponseHeader suppresses bug
// https://github.com/spring-cloud/spring-cloud-gateway/issues/1315,

View File

@@ -24,10 +24,17 @@ import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.filter.headers.ForwardedHeadersFilter.Forwarded;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
@@ -60,7 +67,7 @@ public class ForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -80,26 +87,37 @@ public class ForwardedHeadersFilterTests {
public void forwardedHeaderExists() throws UnknownHostException {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https; for=23.45.67.89")
.header(FORWARDED_HEADER, "for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers.get(FORWARDED_HEADER)).hasSize(2);
assertThat(headers.get(FORWARDED_HEADER)).hasSize(3);
List<Forwarded> forwardeds = ForwardedHeadersFilter.parse(headers.get(FORWARDED_HEADER));
assertThat(forwardeds).hasSize(2);
Forwarded addedForwardedHeader = forwardeds.get(0);
Forwarded existingForwardedHeader = forwardeds.get(1);
assertThat(existingForwardedHeader.getValues()).containsEntry("proto", "http")
.containsEntry("for", "\"10.0.0.1:80\"");
assertThat(addedForwardedHeader.getValues()).containsEntry("proto", "https")
.containsEntry("for", "23.45.67.89");
assertThat(forwardeds).hasSize(3);
Optional<Forwarded> added = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").contains("10.0.0.1:80"))
.findFirst();
assertThat(added).isPresent();
added.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "http").containsEntry("for", "\"10.0.0.1:80\"");
});
Optional<Forwarded> existing = forwardeds.stream()
.filter(forwarded -> forwarded.get("for").equals("23.45.67.89"))
.findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("for", "23.45.67.89");
});
existing = forwardeds.stream().filter(forwarded -> forwarded.get("for").equals("12.34.56.78")).findFirst();
assertThat(existing).isPresent();
existing.ifPresent(forwarded -> {
assertThat(forwarded.getValues()).containsEntry("proto", "https").containsEntry("for", "12.34.56.78");
});
}
@Test
@@ -108,7 +126,7 @@ public class ForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -129,7 +147,7 @@ public class ForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -149,7 +167,7 @@ public class ForwardedHeadersFilterTests {
.remoteAddress(InetSocketAddress.createUnresolved("unresolvable-hostname", 80))
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter(".*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -241,4 +259,83 @@ public class ForwardedHeadersFilterTests {
Assertions.assertThat(forwarded.getValues()).containsEntry("by", "216.103.69.111");
}
@Test
public void trustedProxiesConditionMatches() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.withPropertyValues("spring.cloud.gateway.server.webflux.trusted-proxies=11\\.0\\.0\\..*")
.run(context -> {
assertThat(context).hasSingleBean(ForwardedHeadersFilter.class);
});
}
@Test
public void trustedProxiesConditionDoesNotMatch() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(ForwardedHeadersFilter.class);
});
}
@Test
public void emptyTrustedProxiesFails() {
Assertions.assertThatThrownBy(() -> new ForwardedHeadersFilter(""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void forwardedHeadersNotTrusted() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter("11\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).doesNotContainKeys(FORWARDED_HEADER);
}
// verify that existing forwarded header is not forwarded if not trusted
@Test
public void untrustedForwardedForNotAppended() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=\"127.0.0.1:80\",for=10.0.0.11")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
filtered = forwardedHeaders.stream().filter(value -> value.contains("10.0.0.11")).findFirst();
assertThat(filtered).isNotEmpty();
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.HOST, "myhost")
.header(FORWARDED_HEADER, "proto=http;host=myhost;for=127.0.0.1")
.build();
ForwardedHeadersFilter filter = new ForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(FORWARDED_HEADER);
List<String> forwardedHeaders = headers.get(FORWARDED_HEADER);
Optional<String> filtered = forwardedHeaders.stream().filter(value -> value.contains("127.0.0.1")).findFirst();
assertThat(filtered).isEmpty();
}
}

View File

@@ -21,8 +21,16 @@ import java.net.InetSocketAddress;
import java.net.URI;
import java.util.LinkedHashSet;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.cloud.gateway.config.GatewayAutoConfiguration;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.http.HttpHeaders;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
@@ -43,17 +51,20 @@ import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.G
*/
public class XForwardedHeadersFilterTests {
public static final String ALLOW_ALL_REGEX = ".*";
@Test
public void remoteAddressIsNull() {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER, X_FORWARDED_PROTO_HEADER);
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER)
.containsKeys(X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER, X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_HOST_HEADER)).isEqualTo("localhost:8080");
assertThat(headers.getFirst(X_FORWARDED_PORT_HEADER)).isEqualTo("8080");
@@ -67,7 +78,7 @@ public class XForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -87,7 +98,7 @@ public class XForwardedHeadersFilterTests {
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -110,7 +121,7 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_PROTO_HEADER, "https")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -134,7 +145,7 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_PREFIX_HEADER, "/prefix")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setForAppend(false);
filter.setHostAppend(false);
filter.setPortAppend(false);
@@ -159,7 +170,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
@@ -184,7 +195,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
@@ -210,7 +221,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
@@ -232,7 +243,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
filter.setForEnabled(false);
@@ -258,7 +269,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setPrefixAppend(true);
filter.setPrefixEnabled(true);
filter.setForEnabled(false);
@@ -284,7 +295,7 @@ public class XForwardedHeadersFilterTests {
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
filter.setForEnabled(false);
filter.setHostEnabled(false);
filter.setPortEnabled(false);
@@ -303,7 +314,7 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_FOR_HEADER, "10.0.0.1")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
@@ -319,11 +330,92 @@ public class XForwardedHeadersFilterTests {
.header(X_FORWARDED_FOR_HEADER, "10.0.0.1")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX);
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).doesNotContainKeys(X_FORWARDED_PROTO_HEADER, X_FORWARDED_HOST_HEADER);
}
@Test
public void trustedProxiesConditionMatches() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.withPropertyValues(GatewayProperties.PREFIX + ".trusted-proxies=11\\.0\\.0\\..*")
.run(context -> {
assertThat(context).hasSingleBean(XForwardedHeadersFilter.class);
});
}
@Test
public void trustedProxiesConditionDoesNotMatch() {
new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebFluxAutoConfiguration.class, SslAutoConfiguration.class,
ReactiveWebServerFactoryAutoConfiguration.class, GatewayAutoConfiguration.class))
.run(context -> {
assertThat(context).doesNotHaveBean(XForwardedHeadersFilter.class);
});
}
@Test
public void emptyTrustedProxiesFails() {
Assertions.assertThatThrownBy(() -> new XForwardedHeadersFilter(""))
.isInstanceOf(IllegalArgumentException.class);
}
@Test
public void xForwardedHeadersNotTrusted() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter("11\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).doesNotContainKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
}
// : verify that existing x-forwarded-* headers are not forwarded
// if x-forwarded-for is not trusted
@Test
public void untrustedXForwardedForNotAppended() throws Exception {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80))
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.header(X_FORWARDED_FOR_HEADER, "10.0.0.10")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1")
.contains("10.0.0.1", "10.0.0.10");
}
@Test
public void remoteAdddressIsNullUnTrustedProxyNotAppended() {
MockServerHttpRequest request = MockServerHttpRequest.get("http://localhost:8080/get")
.header(HttpHeaders.HOST, "myhost")
.header(X_FORWARDED_FOR_HEADER, "127.0.0.1")
.build();
XForwardedHeadersFilter filter = new XForwardedHeadersFilter("10\\.0\\.0\\..*");
HttpHeaders headers = filter.filter(request.getHeaders(), MockServerWebExchange.from(request));
assertThat(headers).containsKeys(X_FORWARDED_FOR_HEADER, X_FORWARDED_HOST_HEADER, X_FORWARDED_PORT_HEADER,
X_FORWARDED_PROTO_HEADER);
assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1");
}
}

View File

@@ -63,7 +63,11 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
import static org.springframework.cloud.gateway.test.TestUtils.getMap;
@SpringBootTest(webEnvironment = RANDOM_PORT,
properties = "spring.cloud.gateway.server.webflux.forwarded.by.enabled=true")
properties = { "spring.cloud.gateway.server.webflux.forwarded.by.enabled=true",
"spring.cloud.gateway.server.webflux.forwarded.enabled=true",
"spring.cloud.gateway.server.webflux.x-forwarded.enabled=true",
"spring.cloud.gateway.server.webflux.trusted-proxies=.*",
"logging.level.org.springframework.cloud.gateway.filter.headers=TRACE" })
@DirtiesContext
@SuppressWarnings("unchecked")
@ExtendWith(OutputCaptureExtension.class)

View File

@@ -22,3 +22,4 @@ spring:
filters:
- SetPath=/httpbin/
- SetStatus=200
trusted-proxies: .*

View File

@@ -11,7 +11,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath>
</parent>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-starter-gateway-mvc</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-starter-gateway-server-webmvc</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway</artifactId>
<version>4.3.0-SNAPSHOT</version>
<version>4.3.1-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-starter-gateway</artifactId>