Add route predicate for "X-Forwarded-For" header
Fixes gh-783 Fixes gh-2384
This commit is contained in:
committed by
spencergibb
parent
d5a9a21a5e
commit
8b685b18ca
@@ -134,6 +134,7 @@ import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicat
|
||||
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.XForwardedRemoteAddrRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.route.CachingRouteLocator;
|
||||
import org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator;
|
||||
import org.springframework.cloud.gateway.route.CompositeRouteLocator;
|
||||
@@ -432,6 +433,12 @@ public class GatewayAutoConfiguration {
|
||||
return new RemoteAddrRoutePredicateFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnEnabledPredicate
|
||||
public XForwardedRemoteAddrRoutePredicateFactory xForwardedRemoteAddrRoutePredicateFactory() {
|
||||
return new XForwardedRemoteAddrRoutePredicateFactory();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@DependsOn("weightCalculatorWebFilter")
|
||||
@ConditionalOnEnabledPredicate
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.handler.predicate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.gateway.support.ipresolver.XForwardedRemoteAddressResolver;
|
||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
/*
|
||||
|
||||
This route predicate allows requests to be filtered based on the "X-Forwarded-For" HTTP header.
|
||||
|
||||
This can be used with reverse proxies such as load balancers or web application firewalls where
|
||||
the request should only be allowed if it comes from a trusted list of IP addresses used by those
|
||||
reverse proxies.
|
||||
|
||||
With this implementation, a separate "XForwardedRemoteAddr" route predicate is offered which can
|
||||
be configured with a list of allowed IP addresses and which by default has "maxTrustedIndex" set to 1.
|
||||
This value means we trust the last (right-most) value in the "X-Forwarded-For" header, which represents
|
||||
the last reverse proxy that was used when calling the gateway. That IP address is then checked against
|
||||
the list of allowed IP addresses and used to determine whether or not the request is allowed.
|
||||
|
||||
See https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#modifying-the-way-remote-addresses-are-resolved.
|
||||
|
||||
Note that this predicate implementation does not implement any core logic itself, it aggregates the
|
||||
"RemoteAddrRoutePredicateFactory" and "XForwardedRemoteAddressResolver" classes into a single predicate
|
||||
that can be enabled directly from application configuration without the need to specify anything in
|
||||
custom code.
|
||||
|
||||
Example usage in application.yml which trusts two reverse proxies (one using an IPv6 range):
|
||||
|
||||
...
|
||||
- predicates:
|
||||
- XForwardedRemoteAddr="20.103.252.85", "2a01:111:2050::/44"
|
||||
|
||||
*/
|
||||
|
||||
/**
|
||||
* @author Jelle Druyts
|
||||
*/
|
||||
public class XForwardedRemoteAddrRoutePredicateFactory
|
||||
extends AbstractRoutePredicateFactory<XForwardedRemoteAddrRoutePredicateFactory.Config> {
|
||||
|
||||
private static final Log log = LogFactory.getLog(XForwardedRemoteAddrRoutePredicateFactory.class);
|
||||
|
||||
public XForwardedRemoteAddrRoutePredicateFactory() {
|
||||
super(Config.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ShortcutType shortcutType() {
|
||||
return ShortcutType.GATHER_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> shortcutFieldOrder() {
|
||||
return Arrays.asList("sources");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Predicate<ServerWebExchange> apply(Config config) {
|
||||
if (log.isDebugEnabled()) {
|
||||
log.debug("Applying XForwardedRemoteAddr route predicate with maxTrustedIndex of "
|
||||
+ config.getMaxTrustedIndex() + " for " + config.getSources().size() + " source(s)");
|
||||
}
|
||||
|
||||
// Reuse the standard RemoteAddrRoutePredicateFactory but instead of using the
|
||||
// default RemoteAddressResolver to determine the client IP address, use an
|
||||
// XForwardedRemoteAddressResolver.
|
||||
RemoteAddrRoutePredicateFactory.Config wrappedConfig = new RemoteAddrRoutePredicateFactory.Config();
|
||||
wrappedConfig.setSources(config.getSources());
|
||||
wrappedConfig
|
||||
.setRemoteAddressResolver(XForwardedRemoteAddressResolver.maxTrustedIndex(config.getMaxTrustedIndex()));
|
||||
RemoteAddrRoutePredicateFactory remoteAddrRoutePredicateFactory = new RemoteAddrRoutePredicateFactory();
|
||||
Predicate<ServerWebExchange> wrappedPredicate = remoteAddrRoutePredicateFactory.apply(wrappedConfig);
|
||||
|
||||
return exchange -> {
|
||||
Boolean isAllowed = wrappedPredicate.test(exchange);
|
||||
|
||||
if (log.isDebugEnabled()) {
|
||||
ServerHttpRequest request = exchange.getRequest();
|
||||
log.debug("Request for \"" + request.getURI() + "\" from client \""
|
||||
+ request.getRemoteAddress().getAddress().getHostAddress() + "\" with \""
|
||||
+ XForwardedRemoteAddressResolver.X_FORWARDED_FOR + "\" header value of \""
|
||||
+ request.getHeaders().get(XForwardedRemoteAddressResolver.X_FORWARDED_FOR) + "\" is "
|
||||
+ (isAllowed ? "ALLOWED" : "NOT ALLOWED"));
|
||||
}
|
||||
|
||||
return isAllowed;
|
||||
};
|
||||
}
|
||||
|
||||
public static class Config {
|
||||
|
||||
// Trust the last (right-most) value in the "X-Forwarded-For" header by default,
|
||||
// which represents the last reverse proxy that was used when calling the gateway.
|
||||
private int maxTrustedIndex = 1;
|
||||
|
||||
private List<String> sources = new ArrayList<>();
|
||||
|
||||
public int getMaxTrustedIndex() {
|
||||
return this.maxTrustedIndex;
|
||||
}
|
||||
|
||||
public Config setMaxTrustedIndex(int maxTrustedIndex) {
|
||||
this.maxTrustedIndex = maxTrustedIndex;
|
||||
return this;
|
||||
}
|
||||
|
||||
public List<String> getSources() {
|
||||
return this.sources;
|
||||
}
|
||||
|
||||
public Config setSources(List<String> sources) {
|
||||
this.sources = sources;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Config setSources(String... sources) {
|
||||
this.sources = Arrays.asList(sources);
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFa
|
||||
import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.handler.predicate.XForwardedRemoteAddrRoutePredicateFactory;
|
||||
import org.springframework.cloud.gateway.route.Route;
|
||||
import org.springframework.cloud.gateway.support.ipresolver.RemoteAddressResolver;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -262,6 +263,22 @@ public class PredicateSpec extends UriSpec {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A predicate which checks the remote address of the request based off of the
|
||||
* {@code X-Forwarded-For} header. Useful if Spring Cloud Gateway site behind a proxy
|
||||
* layer. See
|
||||
* {@link org.springframework.cloud.gateway.support.ipresolver.XForwardedRemoteAddressResolver}
|
||||
* for more information.
|
||||
* @param addrs the remote address to verify. Should use CIDR-notation (IPv4 or IPv6)
|
||||
* strings.
|
||||
* @return a {@link BooleanSpec} to be used to add logical operators
|
||||
*/
|
||||
public BooleanSpec xForwardedRemoteAddr(String... addrs) {
|
||||
return asyncPredicate(getBean(XForwardedRemoteAddrRoutePredicateFactory.class).applyAsync(c -> {
|
||||
c.setSources(addrs);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* A predicate which will select a route based on its assigned weight.
|
||||
* @param group the group the route belongs to
|
||||
|
||||
@@ -73,13 +73,13 @@ public final class NameUtils {
|
||||
StringBuffer stringBuffer = new StringBuffer();
|
||||
while (matcher.find()) {
|
||||
if (stringBuffer.length() != 0) {
|
||||
matcher.appendReplacement(stringBuffer, "-" + matcher.group(1).toLowerCase());
|
||||
matcher.appendReplacement(stringBuffer, "-" + matcher.group(1));
|
||||
}
|
||||
else {
|
||||
matcher.appendReplacement(stringBuffer, matcher.group(1).toLowerCase());
|
||||
matcher.appendReplacement(stringBuffer, matcher.group(1));
|
||||
}
|
||||
}
|
||||
return stringBuffer.toString();
|
||||
return stringBuffer.toString().toLowerCase();
|
||||
}
|
||||
|
||||
private static String removeGarbage(String s) {
|
||||
|
||||
@@ -324,6 +324,12 @@
|
||||
"description": "Enables the remote-addr predicate.",
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.predicate.xforwarded-remote-addr.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables the xforwarded-remote-addr predicate.",
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.gateway.predicate.weight.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
|
||||
@@ -85,6 +85,7 @@ public class DisableBuiltInPredicatesTests {
|
||||
"spring.cloud.gateway.predicate.query.enabled=false",
|
||||
"spring.cloud.gateway.predicate.read-body.enabled=false",
|
||||
"spring.cloud.gateway.predicate.remote-addr.enabled=false",
|
||||
"spring.cloud.gateway.predicate.xforwarded-remote-addr.enabled=false",
|
||||
"spring.cloud.gateway.predicate.weight.enabled=false",
|
||||
"spring.cloud.gateway.predicate.cloud-foundry-route-service.enabled=false" })
|
||||
@ActiveProfiles("disable-components")
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.handler.predicate;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.SpringBootConfiguration;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.gateway.route.RouteLocator;
|
||||
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
|
||||
import org.springframework.cloud.gateway.test.BaseWebClientTests;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.web.reactive.function.client.ClientResponse;
|
||||
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
import static org.springframework.cloud.gateway.test.TestUtils.assertStatus;
|
||||
|
||||
@SpringBootTest(webEnvironment = RANDOM_PORT)
|
||||
@DirtiesContext
|
||||
@ActiveProfiles({ "remote-address" })
|
||||
public class XForwardedRemoteAddrRoutePredicateFactoryTests extends BaseWebClientTests {
|
||||
|
||||
@Test
|
||||
public void xForwardedRemoteAddrWorks() {
|
||||
Mono<ClientResponse> result = webClient.get().uri("/xforwardfor").header("X-Forwarded-For", "12.34.56.78")
|
||||
.exchangeToMono(Mono::just);
|
||||
|
||||
StepVerifier.create(result).consumeNextWith(response -> assertStatus(response, HttpStatus.OK)).expectComplete()
|
||||
.verify(Duration.ofSeconds(20));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void xForwardedRemoteAddrWorksUsingRightMostValueByDefault() {
|
||||
Mono<ClientResponse> result = webClient.get().uri("/xforwardfor")
|
||||
.header("X-Forwarded-For", "99.99.99.99, 12.34.56.78").exchangeToMono(Mono::just);
|
||||
|
||||
StepVerifier.create(result).consumeNextWith(response -> assertStatus(response, HttpStatus.OK)).expectComplete()
|
||||
.verify(Duration.ofSeconds(20));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void xForwardedRemoteAddrRejects() {
|
||||
Mono<ClientResponse> result = webClient.get().uri("/xforwardfor").header("X-Forwarded-For", "99.99.99.99")
|
||||
.exchangeToMono(Mono::just);
|
||||
|
||||
StepVerifier.create(result).consumeNextWith(response -> assertStatus(response, HttpStatus.NOT_FOUND))
|
||||
.expectComplete().verify(Duration.ofSeconds(20));
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@SpringBootConfiguration
|
||||
@Import(DefaultTestConfig.class)
|
||||
public static class TestConfig {
|
||||
|
||||
@Value("${test.uri}")
|
||||
String uri;
|
||||
|
||||
@Bean
|
||||
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
|
||||
return builder.routes().route("x_forwarded_for_test", r -> r.path("/xforwardfor").and()
|
||||
.xForwardedRemoteAddr("12.34.56.78").filters(f -> f.setStatus(200)).uri(uri)).build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user