0%

Spring Cloud Gateway性能优化

Spring Could Gateway基于Reactor项目构建,是一个业务网关框架

官方介绍:This project provides a libraries for building an API Gateway on top of Spring WebFlux or Spring WebMVC. Spring Cloud Gateway aims to provide a simple, yet effective way to route to APIs and provide cross cutting concerns to them such as: security, monitoring/metrics, and resiliency.

版本信息

本文的优化方案基于springcould 2021.0.9、springboot2.7.18,理论上适用于springboot2的所有版本

更多优化方案可参考
SpringCloud Gateway 在微服务架构下的最佳实践

Reactor-Netty相关优化

可以使用JVM的-D命令设置或在项目启动前通过System.setProperty()实现

NIO线程数调整

框架默认的IO线程数等于机器核心数,如果业务代码中没有网络IO的阻塞调用,不需要修改。但如果存在阻塞调用Redis、MySQL、API等场景,可以适当将线程数调大,以提升系统的性能表现

1
2
3
4
5
6
7
// The number of processors available to the Java virtual machine.
int processors = Runtime.getRuntime().availableProcessors();
/**
* IO_WORKER_COUNT的值最好是2的幂次方,Netty做了相应的优化
* {@link io.netty.util.concurrent.DefaultEventExecutorChooserFactory#isPowerOfTwo(int)}
*/
System.setProperty(ReactorNetty.IO_WORKER_COUNT, String.valueOf(processors * 2));

单独开一个线程作为selector

1
System.setProperty(ReactorNetty.IO_SELECT_COUNT, "1");

连接池获取策略调整

1
2
3
4
5
/*
* "lifo"可以更快地释放不需要的连接,但在流量波动频繁时会出现更明显的性能波动
* 需要配置"spring.cloud.gateway.httpclient.pool.eviction-interval"以关闭长时间未使用的连接
*/
System.setProperty(ReactorNetty.POOL_LEASING_STRATEGY, "lifo");

关闭access-log

如果网关系统接入了三方的监控系统,可以考虑关闭access log

1
System.setProperty(ReactorNetty.ACCESS_LOG_ENABLED, "false");

关闭Netty的内存泄漏检测

1
2
// 关闭Netty的内存泄漏检测,开发调试时可以打开
System.setProperty("io.netty.leakDetection.level", "disabled");

优先使用原生的事件订阅技术(默认开启)

1
2
// 优先选择原生的事件订阅技术,例如Linux的epoll、BSD的kqueue
System.setProperty(ReactorNetty.NATIVE, "true");

其他

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 框架层在对接Netty时存在逻辑缺陷,新分配的池化内存无法完全回收,导致堆外内存泄漏。
* <a href="https://zhuanlan.zhihu.com/p/647224221"/>
*/
System.setProperty("io.netty.allocator.type", "unpooled");
/**
* @see reactor.core.scheduler.Schedulers
*/
System.setProperty("reactor.schedulers.defaultPoolSize", String.valueOf(processors));
System.setProperty("reactor.schedulers.defaultBoundedElasticSize", String.valueOf(processors * 8));
// 默认长度100k
System.setProperty("reactor.schedulers.defaultBoundedElasticQueueSize", "4096");

签名校验优化

API网关通常会使用MD5等算法为请求进行签名校验,Gateway本身的线程数较少且固定,可以考虑使用FastThreadLocal缓存MessageDigest实例,不需要每次重新构建,执行速度相较于每次新建实例可提升10倍左右

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* MessageDigest非线程安全,每个线程单独绑定一个实例
*/
private static final FastThreadLocal<MessageDigest> MD5_HOLDER = new FastThreadLocal<MessageDigest>() {

@Override
protected MessageDigest initialValue() throws Exception {
return MessageDigest.getInstance("MD5");
}
};

private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};

/**
* 获取 MD5 MessageDigest
*/
private static MessageDigest getMd5MessageDigest() {
MessageDigest messageDigest = MD5_HOLDER.get();
messageDigest.reset();
return messageDigest;
}

public static String md5(String string) {
char[] digits = DIGITS;
byte[] bytes = string.getBytes(StandardCharsets.UTF_8);
bytes = getMd5MessageDigest().digest(bytes);
char[] chars = new char[32];
for (int i = 0, j = 0; i < bytes.length; i++) {
chars[j++] = digits[(bytes[i] & 0xF0) >>> 4];
chars[j++] = digits[bytes[i] & 0x0F];
}
return new String(chars);
}

连接池优化

如果业务中存在调用Redis、MySQL等场景,且使用了连接池技术(一般都会用到吧)

连接池的最小活跃数量可以设置为{NIO线程数} + 1,最大数量可设置为{NIO线程数} * 2作为冗余

使用GlobalFilter和自定义断言

框架默认情况下使用遍历路由的方式进行断言,时间复杂度为O(n),随着路由数量提升,性能下降较为明显

在业务场景允许的情况下(很难做到不允许吧),建议仅使用全局过滤器(GlobalFilter)替代普通的过滤器(Filter),后面会提到为什么要这么做

1
2
3
4
5
6
7
8
9
RouteDefinition routeDefinition = new RouteDefinition();
routeDefinition.setId("xxx");
/**
* 自定义路由规则,不使用gateway的RoutePredicateFactory
* @see CustomizedRoutePredicateHandlerMapping#lookupRoute(ServerWebExchange)
*/
routeDefinition.setPredicates(Collections.emptyList());
// 全都走GlobalFilter,这里暂时用不到
routeDefinition.setFilters(Collections.emptyList());

使用MAP缓存路由信息

自定义实现RouteLocator接口,并替换自动配置中声明的名称为‘cachedCompositeRouteLocator’的bean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
/**
* 增强了spring cloud gateway自带的CachingRouteLocator,可以通过routeId直接获取对应的route
*
* @see org.springframework.cloud.gateway.route.CachingRouteLocator
* @see org.springframework.cloud.gateway.config.GatewayAutoConfiguration#cachedCompositeRouteLocator(List)
* @see org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#routeLocator
*/
@Slf4j
@Primary
@Component("cachedCompositeRouteLocator")
public class EnhancedCachingRouteLocator implements Ordered, RouteLocator, ApplicationListener<RefreshRoutesEvent> {

private final RouteLocator routeLocator;

private final ApplicationEventPublisher applicationEventPublisher;

/**
* [RouteId, Route] 路由缓存,可直接使用RouteId来获取Route
*/
private Map<String, Route> cache = Collections.emptyMap();

private Flux<Route> routes;

public EnhancedCachingRouteLocator(RouteLocator routeLocator, ApplicationEventPublisher applicationEventPublisher) {
this.routeLocator = routeLocator;
this.applicationEventPublisher = applicationEventPublisher;
this.routes = this.refresh();
}

public Route getRoute(String routeId) {
Map<String, Route> cache = this.cache;
return cache.get(routeId);
}

@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}

@Override
public Flux<Route> getRoutes() {
return routes;
}

@Override
public void onApplicationEvent(RefreshRoutesEvent refreshRoutesEvent) {
FunctionUtil.runnable(
() -> this.refresh()
.collect(Collectors.toList())
.subscribe(
list -> Flux.fromIterable(list)
.materialize()
.collect(Collectors.toList())
.subscribe(
signals -> applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this)),
this::handleRefreshError
),
this::handleRefreshError
),
this::handleRefreshError
);
}

/**
* Clears the routes cache.
*
* @return routes flux
*/
private Flux<Route> refresh() {
Flux<Route> routes = routeLocator.getRoutes().sort(AnnotationAwareOrderComparator.INSTANCE);
Map<String, Route> cache = new HashMap<>(128, 1.0F);
routes.subscribe(route -> cache.put(route.getId(), route));
this.routes = routes;
this.cache = Collections.unmodifiableMap(cache);
return routes;
}

private void handleRefreshError(Throwable throwable) {
log.error("Refresh routes error", throwable);
applicationEventPublisher.publishEvent(new RefreshRoutesResultEvent(this, throwable));
}
}

自定义路由断言

与上面自定义的EnhancedCachingRouteLocator相配合,可以将断言的时间复杂度从O(n)降低到O(1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
/**
* 修改spring cloud gateway基于断言工厂的路由处理器,替换为更高效的实现。
* <p>
* 这里重写RoutePredicateHandlerMapping的lookupRoute方法,
* 可以将大部分情况下(带有"x-app"请求头的请求)路由匹配的时间复杂度从O(n)降低到O(1),最差情况也可以降低到O(3)
* <p>
* 框架自带的断言工厂,见 {@link org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory}
*
* @see org.springframework.cloud.gateway.handler.RoutePredicateHandlerMapping#lookupRoute(ServerWebExchange)
* @see org.springframework.cloud.gateway.config.GatewayAutoConfiguration#routePredicateHandlerMapping(FilteringWebHandler, RouteLocator, GlobalCorsProperties, Environment)
*/
@Component
public class CustomizedRoutePredicateHandlerMapping extends RoutePredicateHandlerMapping {

private final EnhancedCachingRouteLocator enhancedCachingRouteLocator;

public CustomizedRoutePredicateHandlerMapping(FilteringWebHandler filteringWebHandler,
RouteLocator routeLocator,
GlobalCorsProperties globalCorsProperties,
Environment environment) {
super(filteringWebHandler, routeLocator, globalCorsProperties, environment);
if (routeLocator instanceof EnhancedCachingRouteLocator) {
this.enhancedCachingRouteLocator = (EnhancedCachingRouteLocator) routeLocator;
} else {
throw new IllegalArgumentException("RouteLocator type not matched to " + EnhancedCachingRouteLocator.class.getName());
}
}

@Override
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
String routeId = this.lookupRouteId(exchange.getRequest());
if (StringUtils.isEmpty(routeId)) {
return Mono.empty();
}
// 直接根据routeId找到对应的route
Route route = enhancedCachingRouteLocator.getRoute(routeId);
if (route == null) {
return Mono.error(new InterruptRequestException(HttpStatus.NOT_FOUND, "路由编码错误"));
}
ExchangeAttribute.REQUEST_TARGET.set(exchange, route.getId());
return Mono.just(route);
}

/**
* 分别根据请求头、查询参数、请求路径来匹配对应的系统
*
* @param request request
* @return routeId
*/
private String lookupRouteId(ServerHttpRequest request) {
// Step.1 请求头指定route
String routeId = request.getHeaders().getFirst("x-app");
if (StringUtils.isNotEmpty(routeId)) {
return routeId;
}
// Step.2 查询参数指定route
routeId = this.lookupRouteIdFromQuery(request);
if (StringUtils.isNotEmpty(routeId)) {
return routeId;
}
// Step.3 请求地址指定route
routeId = this.lookupRouteIdFromPath(request.getPath().value());
if (StringUtils.isNotEmpty(routeId)) {
return routeId;
}
// Step.4 兜底逻辑,没有找到路由则默认为default
return "default";
}

/**
* "xyz=123&x-app=abc123&key=value" -> "abc123"
* 由于只需要获取"x-app"查询参数即可,不需要URLDecode,出于性能考虑,不使用框架自带的
* {@link org.springframework.http.server.reactive.ServerHttpRequest#getQueryParams()}
*/
private String lookupRouteIdFromQuery(ServerHttpRequest request) {
String query = request.getURI().getRawQuery();
if (StringUtils.isEmpty(query)) {
return null;
}
int i = query.indexOf("x-app=");
if (i < 0) {
return null;
}
query = query.substring(i + "x-app=".length());
i = query.indexOf('&');
// 一般来说系统编码不会出现特殊符号,不需要URLDecode
return i > 0 ? query.substring(0, i) : query;
}

/**
* "/gateway/abc123/**" -> "abc123"
*/
private String lookupRouteIdFromPath(String requestPath) {
int i = "/gateway/".length();
requestPath = requestPath.substring(i);
i = requestPath.indexOf('/');
if (i < 0) {
return null;
}
return requestPath.substring(0, i);
}
}

Filter排序优化

如果每个路由单独配置了Filter,框架在每次请求时,都会整合路由的Filter和GlobalFIilter并进行一次较为复杂的排序操作,性能损失较为严重

以下是框架的源代码,从源码中的TODO标签来看,官方已经意识到了这里的问题,但是目前还没有解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
* TODO: relocate @EventListener(RefreshRoutesEvent.class) void handleRefresh() {
* this.combinedFiltersForRoute.clear();
*/
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);
List<GatewayFilter> gatewayFilters = route.getFilters();

List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);
combined.addAll(gatewayFilters);
// TODO: needed or cached?
AnnotationAwareOrderComparator.sort(combined);

if (logger.isDebugEnabled()) {
logger.debug("Sorted gatewayFilterFactories: " + combined);
}

return new DefaultGatewayFilterChain(combined).filter(exchange);
}

使用时若只使用全局过滤器(GlobalFilter),就可以对GlobalFilter进行预排序,减少处理请求时因为排序带来的性能损失

实现方案:使用类加载器的就近加载机制,在项目下新建同限定名的FilteringWebHandler替换框架的默认实现

即:在项目根目录下新建一个名为org.springframework.cloud.gateway.handler的包,并新建一个与框架自带FilteringWebHandler同名的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* {@inheritDoc}
* 源码修改自gateway的FilteringWebHandler
* 因为没有路由过滤器,所以去除了每次组装FilterChain前的排序操作
* 基于DefaultFilterChain增强的FlexGatewayFilterChain可以更加灵活的跳转到指定的Filter
*
* @see org.springframework.cloud.gateway.config.GatewayAutoConfiguration#filteringWebHandler(List)
*/
@Slf4j
public class FilteringWebHandler implements WebHandler {

private final List<GatewayFilter> globalFilters;

public FilteringWebHandler(List<GlobalFilter> globalFilters) {
List<GatewayFilter> sortedGlobalFilters = globalFilters.stream()
.map(filter -> {
GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);
if (filter instanceof Ordered) {
return new OrderedGatewayFilter(gatewayFilter, ((Ordered) filter).getOrder());
}
return gatewayFilter;
})
.sorted(AnnotationAwareOrderComparator.INSTANCE)
.collect(Collectors.toList());
this.globalFilters = Collections.unmodifiableList(new ArrayList<>(sortedGlobalFilters));
}

/**
* 对比框架源码做了主要改动的地方,去除了合并全局过滤器和路由过滤器并排序的操作
*/
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
List<GatewayFilter> globalFilters = this.globalFilters;
if (log.isDebugEnabled()) {
log.debug("Sorted filters: {}", globalFilters);
}
return new FlexGatewayFilterChain(globalFilters).filter(exchange);
}

private static class GatewayFilterAdapter implements GatewayFilter {

private final GlobalFilter delegate;

private GatewayFilterAdapter(GlobalFilter delegate) {
this.delegate = delegate;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return delegate.filter(exchange, chain);
}

@Override
public String toString() {
return "GatewayFilterAdapter{" + "delegate=" + delegate + '}';
}
}
}

可以复制一份框架默认的GatewayFilterChain,也可自定义实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/**
* {@inheritDoc}
* 相较于框架自带的DefaultGatewayFilterChain更加灵活,可通过指定偏移量跳转到任何一个Filter
*/
public final class FlexGatewayFilterChain implements GatewayFilterChain {

private final List<GatewayFilter> filters;

private final int index;

FlexGatewayFilterChain(List<GatewayFilter> filters) {
this.filters = filters;
this.index = 0;
}

private FlexGatewayFilterChain(List<GatewayFilter> filters, int index) {
this.filters = filters;
this.index = index;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange) {
return this.filter(exchange, 1);
}

public Mono<Void> filter(ServerWebExchange exchange, int offset) {
return Mono.defer(
() -> {
int index = this.index;
List<GatewayFilter> filters = this.filters;
if (index < filters.size()) {
// FIXME 框架自带的DefaultFilterChain在这里是复制了一次,但是不进行异步/修改操作的话感觉没必要这么做
FlexGatewayFilterChain chain = new FlexGatewayFilterChain(filters, index + offset);
return filters.get(index).filter(exchange, chain);
} else {
return Mono.empty();
}
}
);
}
}

使用Native OpenSSL

若后端系统存在https调用,可引入性能更好的openSSL依赖

1
2
3
4
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-tcnative-boringssl-static</artifactId>
</dependency>

移除不必要的依赖

若系统通过agent等形式接入了三方的监控系统,可以关闭框架自带的监控,减少资源占用

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 用于暴露endpoint -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<!-- 去除内置的监控依赖 -->
<exclusion>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-core</artifactId>
</exclusion>
</exclusions>
</dependency>

异步打印日志

logback配置可以简单参考,线上环境关闭console日志输出

进阶性能调优方案可参考

分布式系统日志打印优化方案的探索与实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="false">
<property name="log_dir" value="logs"/>
<property name="log_file_name" value="gateway"/>
<property name="log_pattern" value="%d{yyy-MM-dd HH:mm:ss.SSS} %-5.5p [%-30.30t] %-45.45c{45} : %m%n"/>
<property name="log_charset" value="utf-8"/>

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log_pattern}</pattern>
<charset>${log_charset}</charset>
</encoder>
</appender>

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log_dir}/${log_file_name}.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log_dir}/${log_file_name}_%d{yyyyMMdd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log_pattern}</pattern>
<charset>${log_charset}</charset>
</encoder>
</appender>

<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<discardingThreshold>0</discardingThreshold>
<queueSize>1024</queueSize>
<appender-ref ref="file"/>
</appender>

<root level="info">
<!--
<appender-ref ref="console"/>
-->
<appender-ref ref="async"/>
</root>
</configuration>

调整RoutePredicateHandlerMapping位置

配置文件添加,将RoutePredicateHandlerMapping排在第一位

1
spring.cloud.gateway.handler-mapping.order=-10000

关闭一些不必要的组件

看实际项目需要,选择性关闭一部分不需要的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server.forward-headers-strategy=none
spring.cloud.gateway.metrics.enabled=false

spring.cloud.gateway.global-filter.adapt-cached-body.enabled=false
spring.cloud.gateway.global-filter.forward-path.enabled=false
spring.cloud.gateway.global-filter.forward-routing.enabled=false
spring.cloud.gateway.global-filter.load-balancer-client.enabled=false
spring.cloud.gateway.global-filter.netty-routing.enabled=true
spring.cloud.gateway.global-filter.netty-write-response.enabled=true
spring.cloud.gateway.global-filter.reactive-load-balancer-client.enabled=false
spring.cloud.gateway.global-filter.remove-cached-body.enabled=true
spring.cloud.gateway.global-filter.route-to-request-url.enabled=true
spring.cloud.gateway.global-filter.websocket-routing.enabled=false

spring.cloud.gateway.forwarded.enabled=false
spring.cloud.gateway.x-forwarded.enabled=false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
* BeanDefinition处理器,移除一些用不到的系统组件,
* 使用配置文件关闭的话要写很多行配置,这里统一处理,可以减少一点点🤏系统占用
*
* @see org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory
* @see org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor
*/
@Component
public class BeanDefinitionRegisterProcessor implements BeanDefinitionRegistryPostProcessor {

@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
// 移除RoutePredicateFactory
String routePredicateFactorySuffix = RoutePredicateFactory.class.getSimpleName();
// 移除GatewayFilterFactory
String gatewayFilterFactorySuffix = GatewayFilterFactory.class.getSimpleName();

for (String beanDefinitionName : beanDefinitionRegistry.getBeanDefinitionNames()) {
if (beanDefinitionName.endsWith(routePredicateFactorySuffix)) {
beanDefinitionRegistry.removeBeanDefinition(beanDefinitionName);
}
if (beanDefinitionName.endsWith(gatewayFilterFactorySuffix)) {
beanDefinitionRegistry.removeBeanDefinition(beanDefinitionName);
}
}
/**
* @see org.springframework.cloud.gateway.config.GatewayAutoConfiguration#readBodyPredicateFactory(org.springframework.http.codec.ServerCodecConfigurer)
*/
beanDefinitionRegistry.removeBeanDefinition("readBodyPredicateFactory");
/**
* 路由权重配置,依赖 {@link org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory}
* @see org.springframework.cloud.gateway.config.GatewayAutoConfiguration#weightCalculatorWebFilter(org.springframework.cloud.gateway.support.ConfigurationService, org.springframework.beans.factory.ObjectProvider)
*/
beanDefinitionRegistry.removeBeanDefinition("weightCalculatorWebFilter");
}

@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
}
}

跨域配置

1
2
3
4
5
6
spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping=true
spring.cloud.gateway.globalcors.cors-configurations.[/**].allow-credentials=true
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-methods=*
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-origin-patterns=https://*.domain.com,http://localhost:*,http://localhost
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowed-headers=*
spring.cloud.gateway.globalcors.cors-configurations.[/**].max-age=3600

后端服务重复进行了跨域处理,导致响应头重复的解决办法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 服务端http响应头过滤器,去除后端返回的CORS响应头,以网关的CORS配置为准
*
* @see org.springframework.web.cors.reactive.DefaultCorsProcessor
*/
@Component
public class CorsResponseHttpHeadersFilter implements Ordered, HttpHeadersFilter {

/**
* CORS
*/
private final List<String> corsResponseHeaders = Collections.unmodifiableList(Arrays.asList(
HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS,
HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS,
HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS,
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN,
HttpHeaders.ACCESS_CONTROL_MAX_AGE
));

/**
* Vary
*/
private final List<String> corsVaryHeaders = Collections.unmodifiableList(Arrays.asList(
HttpHeaders.ORIGIN,
HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD,
HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS
));

@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}

@Override
public boolean supports(Type type) {
return Type.RESPONSE == type;
}

@Override
public HttpHeaders filter(HttpHeaders headers, ServerWebExchange exchange) {
// 移除CORS headers
String accessControlAllowOrigin = headers.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN);
if (accessControlAllowOrigin != null) {
corsResponseHeaders.forEach(headers::remove);
}
// 移除vary
List<String> vary = headers.remove(HttpHeaders.VARY);
if (CollectionUtils.isNotEmpty(vary)) {
corsVaryHeaders.forEach(vary::remove);
headers.put(HttpHeaders.VARY, vary);
}
return headers;
}
}