diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index 5987b3c4..08bf68a1 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -842,6 +842,15 @@ For `Request`, you need to implement and define `LoadBalancerFeignRequestTransfo If multiple transformers are defined, they are applied in the order in which beans are defined. Alternatively, you can use `LoadBalancerFeignRequestTransformer.DEFAULT_ORDER` to specify the order. +=== X-Forwarded Headers Support + +`X-Forwarded-Host` and `X-Forwarded-Proto` support can be enabled by setting following flag: + +[source,properties] +---- +spring.cloud.loadbalancer.x-forwarded.enabled=true +---- + == Configuration properties To see the list of all Spring Cloud OpenFeign related configuration properties please check link:appendix.html[the Appendix page]. diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java index dfd819ce..142f6ca3 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/FeignLoadBalancerAutoConfiguration.java @@ -23,6 +23,7 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; @@ -30,6 +31,7 @@ import org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientA import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.cloud.openfeign.FeignAutoConfiguration; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -39,6 +41,7 @@ import org.springframework.context.annotation.Import; * * @author Olga Maciaszek-Sharma * @author Nguyen Ky Thanh + * @author changjin wei(魏昌进) * @since 2.2.0 */ @ConditionalOnClass(Feign.class) @@ -54,4 +57,11 @@ import org.springframework.context.annotation.Import; HttpClient5FeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class }) public class FeignLoadBalancerAutoConfiguration { + @Bean + @ConditionalOnBean(LoadBalancerClientFactory.class) + @ConditionalOnMissingBean(XForwardedHeadersTransformer.class) + public XForwardedHeadersTransformer xForwarderHeadersFeignTransformer(LoadBalancerClientFactory factory) { + return new XForwardedHeadersTransformer(factory); + } + } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/XForwardedHeadersTransformer.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/XForwardedHeadersTransformer.java new file mode 100644 index 00000000..e7533cbc --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/loadbalancer/XForwardedHeadersTransformer.java @@ -0,0 +1,63 @@ +/* + * Copyright 2013-2022 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.openfeign.loadbalancer; + +import java.net.URI; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import feign.Request; + +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; + +/** + * To add X-Forwarded-Host and X-Forwarded-Proto Headers. + * + * @author changjin wei(魏昌进) + */ +public class XForwardedHeadersTransformer implements LoadBalancerFeignRequestTransformer { + + private final ReactiveLoadBalancer.Factory factory; + + public XForwardedHeadersTransformer(ReactiveLoadBalancer.Factory factory) { + this.factory = factory; + } + + @Override + public Request transformRequest(Request request, ServiceInstance instance) { + if (instance == null) { + return request; + } + LoadBalancerProperties.XForwarded xForwarded = factory.getProperties(instance.getServiceId()).getXForwarded(); + if (xForwarded.isEnabled()) { + Map> headers = new HashMap<>(request.headers()); + URI uri = URI.create(request.url()); + String xForwardedHost = uri.getHost(); + String xForwardedProto = uri.getScheme(); + headers.put("X-Forwarded-Host", Collections.singleton(xForwardedHost)); + headers.put("X-Forwarded-Proto", Collections.singleton(xForwardedProto)); + request = Request.create(request.httpMethod(), request.url(), headers, request.body(), request.charset(), + request.requestTemplate()); + } + return request; + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/XForwardedHeadersTransformerTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/XForwardedHeadersTransformerTests.java new file mode 100644 index 00000000..71093d98 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/loadbalancer/XForwardedHeadersTransformerTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2013-2022 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.openfeign.loadbalancer; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import feign.Request; +import org.junit.jupiter.api.Test; + +import org.springframework.cloud.client.DefaultServiceInstance; +import org.springframework.cloud.client.ServiceInstance; +import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for + * {@link XForwardedHeadersTransformer}. + * + * @author changjin wei(魏昌进) + */ +class XForwardedHeadersTransformerTests { + + private final LoadBalancerClientFactory loadBalancerClientFactory = mock(LoadBalancerClientFactory.class); + + private final LoadBalancerProperties loadBalancerProperties = new LoadBalancerProperties(); + + private final ServiceInstance serviceInstance = new DefaultServiceInstance("test1", "test", "test.org", 8080, + false); + + private final Request request = testRequest(); + + private Request testRequest() { + return testRequest("spring.io"); + } + + private Request testRequest(String host) { + return Request.create(Request.HttpMethod.GET, "https://" + host + "/path", testHeaders(), "hello".getBytes(), + StandardCharsets.UTF_8, null); + } + + private Map> testHeaders() { + Map> feignHeaders = new HashMap<>(); + feignHeaders.put(HttpHeaders.CONTENT_TYPE, Collections.singletonList(MediaType.APPLICATION_JSON_VALUE)); + return feignHeaders; + + } + + @Test + void shouldAppendXForwardedHeadersIfEnabled() { + loadBalancerProperties.getXForwarded().setEnabled(true); + when(loadBalancerClientFactory.getProperties("test")).thenReturn(loadBalancerProperties); + XForwardedHeadersTransformer transformer = new XForwardedHeadersTransformer(loadBalancerClientFactory); + + Request newRequest = transformer.transformRequest(request, serviceInstance); + + assertThat(newRequest.headers()).containsKey("X-Forwarded-Host"); + assertThat(newRequest.headers()).containsEntry("X-Forwarded-Host", Collections.singleton("spring.io")); + assertThat(newRequest.headers()).containsKey("X-Forwarded-Proto"); + assertThat(newRequest.headers()).containsEntry("X-Forwarded-Proto", Collections.singleton("https")); + + } + + @Test + void shouldNotAppendXForwardedHeadersIfDefault() { + when(loadBalancerClientFactory.getProperties("test")).thenReturn(loadBalancerProperties); + XForwardedHeadersTransformer transformer = new XForwardedHeadersTransformer(loadBalancerClientFactory); + + Request newRequest = transformer.transformRequest(request, serviceInstance); + + assertThat(newRequest.headers()).doesNotContainKey("X-Forwarded-Host"); + assertThat(newRequest.headers()).doesNotContainKey("X-Forwarded-Proto"); + } + +}