Support passing selected ServiceInstance id downstream via cookie. (#2173)
This commit is contained in:
committed by
GitHub
parent
44a3e85ae5
commit
37cb0ea27b
@@ -20,10 +20,12 @@ import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
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.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
|
||||
import org.springframework.cloud.gateway.config.conditional.ConditionalOnEnabledGlobalFilter;
|
||||
import org.springframework.cloud.gateway.filter.LoadBalancerServiceInstanceCookieFilter;
|
||||
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
|
||||
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
@@ -52,4 +54,15 @@ public class GatewayReactiveLoadBalancerClientAutoConfiguration {
|
||||
return new ReactiveLoadBalancerClientFilter(clientFactory, properties, loadBalancerProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(ReactiveLoadBalancerClientFilter.class)
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.sticky-session.add-service-instance-cookie",
|
||||
havingValue = "true")
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnEnabledGlobalFilter
|
||||
public LoadBalancerServiceInstanceCookieFilter loadBalancerServiceInstanceCookieFilter(
|
||||
LoadBalancerProperties loadBalancerProperties) {
|
||||
return new LoadBalancerServiceInstanceCookieFilter(loadBalancerProperties);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2013-2021 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;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.Response;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR;
|
||||
|
||||
/**
|
||||
* A {@link GlobalFilter} that allows passing the {@code} instanceId) of the
|
||||
* {@link ServiceInstance} selected by the {@link ReactiveLoadBalancerClientFilter} in a
|
||||
* cookie.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 3.0.2
|
||||
*/
|
||||
public class LoadBalancerServiceInstanceCookieFilter implements GlobalFilter, Ordered {
|
||||
|
||||
private final LoadBalancerProperties loadBalancerProperties;
|
||||
|
||||
public LoadBalancerServiceInstanceCookieFilter(LoadBalancerProperties loadBalancerProperties) {
|
||||
this.loadBalancerProperties = loadBalancerProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
||||
Response<ServiceInstance> serviceInstanceResponse = exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR);
|
||||
if (serviceInstanceResponse == null || !serviceInstanceResponse.hasServer()) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
String instanceIdCookieName = loadBalancerProperties.getStickySession().getInstanceIdCookieName();
|
||||
if (!StringUtils.hasText(instanceIdCookieName)) {
|
||||
return chain.filter(exchange);
|
||||
}
|
||||
ServerWebExchange newExchange = exchange.mutate().request(exchange.getRequest().mutate().headers((headers) -> {
|
||||
List<String> cookieHeaders = new ArrayList<>(headers.getOrEmpty(HttpHeaders.COOKIE));
|
||||
String serviceInstanceCookie = new HttpCookie(instanceIdCookieName,
|
||||
serviceInstanceResponse.getServer().getInstanceId()).toString();
|
||||
cookieHeaders.add(serviceInstanceCookie);
|
||||
headers.put(HttpHeaders.COOKIE, cookieHeaders);
|
||||
}).build()).build();
|
||||
return chain.filter(newExchange);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return LOAD_BALANCER_CLIENT_FILTER_ORDER + 1;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -88,6 +88,7 @@ public class DisableBuiltInGlobalFiltersTests {
|
||||
"spring.cloud.gateway.global-filter.netty-routing.enabled=false",
|
||||
"spring.cloud.gateway.global-filter.reactive-load-balancer-client.enabled=false",
|
||||
"spring.cloud.gateway.global-filter.load-balancer-client.enabled=false",
|
||||
"spring.cloud.gateway.global-filter.load-balancer-service-instance-cookie.enabled=false",
|
||||
"spring.cloud.gateway.metrics.enabled=false" })
|
||||
@ActiveProfiles("disable-components")
|
||||
public static class DisableAllGlobalFiltersByProperty {
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright 2013-2021 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;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.http.HttpCookie;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
|
||||
import org.springframework.mock.web.server.MockServerWebExchange;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_LOADBALANCER_RESPONSE_ATTR;
|
||||
|
||||
/**
|
||||
* Tests for {@link LoadBalancerServiceInstanceCookieFilter}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class LoadBalancerServiceInstanceCookieFilterTests {
|
||||
|
||||
private final LoadBalancerProperties properties = new LoadBalancerProperties();
|
||||
|
||||
private final GatewayFilterChain chain = mock(GatewayFilterChain.class);
|
||||
|
||||
private final ServerWebExchange exchange = MockServerWebExchange
|
||||
.from(MockServerHttpRequest.get("http://localhost/get").build());
|
||||
|
||||
private final LoadBalancerServiceInstanceCookieFilter filter = new LoadBalancerServiceInstanceCookieFilter(
|
||||
properties);
|
||||
|
||||
@Test
|
||||
void shouldAddServiceInstanceCookieHeader() {
|
||||
exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR,
|
||||
new DefaultResponse(new DefaultServiceInstance("test-01", "test", "host", 8080, false)));
|
||||
|
||||
ServerWebExchange filteredExchange = testFilter(exchange);
|
||||
|
||||
assertThat(filteredExchange.getRequest().getHeaders().get(HttpHeaders.COOKIE)).hasSize(1);
|
||||
assertThat(filteredExchange.getRequest().getHeaders().get(HttpHeaders.COOKIE))
|
||||
.containsExactly("sc-lb-instance-id=test-01");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldAppendServiceInstanceCookieHeaderIfCookiesPresent() {
|
||||
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("http://localhost/get")
|
||||
.cookie(new HttpCookie("testCookieName", "testCookieValue")).build());
|
||||
exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR,
|
||||
new DefaultResponse(new DefaultServiceInstance("test-01", "test", "host", 8080, false)));
|
||||
|
||||
ServerWebExchange filteredExchange = testFilter(exchange);
|
||||
|
||||
assertThat(filteredExchange.getRequest().getHeaders().get(HttpHeaders.COOKIE))
|
||||
.containsExactly("testCookieName=testCookieValue", "sc-lb-instance-id=test-01");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContinueChainWhenNoServiceInstanceResponse() {
|
||||
ServerWebExchange filteredExchange = testFilter(exchange);
|
||||
|
||||
assertThat(filteredExchange.getRequest().getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContinueChainWhenNullServiceInstanceCookieName() {
|
||||
exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR,
|
||||
new DefaultResponse(new DefaultServiceInstance("test-01", "test", "host", 8080, false)));
|
||||
properties.getStickySession().setInstanceIdCookieName(null);
|
||||
|
||||
ServerWebExchange filteredExchange = testFilter(exchange);
|
||||
|
||||
assertThat(filteredExchange.getRequest().getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldContinueChainWhenEmptyServiceInstanceCookieName() {
|
||||
exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR,
|
||||
new DefaultResponse(new DefaultServiceInstance("test-01", "test", "host", 8080, false)));
|
||||
properties.getStickySession().setInstanceIdCookieName("");
|
||||
|
||||
ServerWebExchange filteredExchange = testFilter(exchange);
|
||||
|
||||
assertThat(filteredExchange.getRequest().getHeaders()).isEmpty();
|
||||
}
|
||||
|
||||
private ServerWebExchange testFilter(ServerWebExchange exchange) {
|
||||
ArgumentCaptor<ServerWebExchange> captor = ArgumentCaptor.forClass(ServerWebExchange.class);
|
||||
when(chain.filter(captor.capture())).thenReturn(Mono.empty());
|
||||
|
||||
filter.filter(exchange, chain).block();
|
||||
verify(chain).filter(any(ServerWebExchange.class));
|
||||
verifyNoMoreInteractions(chain);
|
||||
return captor.getValue();
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user