Spring Cloud Gateway 2.X 跨域时出现重复Origin的BUG
版本
Spring Cloud :Hoxton.SR12
Spring Cloud Gateway : 3.1.0
问题描述
在 SpringCloud 项目中,前后端分离目前很常见,在调试时,会遇到两种情况的跨域:
前端页面通过不同域名或IP访问微服务的后台
例如前端人员会在本地起HttpServer 直连后台开发本地起的服务,此时,如果不加任何配置,前端页面的请求会被浏览器跨域限制拦截,所以,业务服务常常会添加如下代码设置全局跨域:
@Bean public CorsFilter corsFilter() { logger.debug("CORS限制打开"); CorsConfiguration config = new CorsConfiguration(); # 仅在开发环境设置为* config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", config); return new CorsFilter(configSource); }
前端页面通过不同域名或IP访问SpringCloud Gateway
例如前端人员在本地起HttpServer直连服务器的Gateway进行调试。此时,同样会遇到跨域。需要在Gateway的配置文件中增加:
spring: cloud: gateway: globalcors: cors-configurations: # 仅在开发环境设置为* '[/**]': allowedOrigins: "*" allowedHeaders: "*" allowedMethods: "*"
那么,此时直连微服务和网关的跨域问题都解决了,是不是很完美?
No~ 问题来了,****前端仍然会报错:“不允许有多个’Access-Control-Allow-Origin’ CORS头”。
Access to XMLHttpRequest at 'http://192.168.2.137:8088/api/two' from origin 'http://localhost:3200' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header contains multiple values '*, http://localhost:3200', but only one is allowed.
仔细查看返回的响应头,里面包含了两份Access-Control-Allow-Origin头。我们用客户端版的PostMan做一个模拟,在请求里设置头:Origin : * ,发现问题了:Access-Control-Allow-Origin 两个头重复了两次,其中浏览器对后者有唯一性限制!
解决的方案有两种:
利用DedupeResponseHeader配置
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOrigins: "*" allowedHeaders: "*" allowedMethods: "*" default-filters: #解决cors重复跨域问题 - DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
DedupeResponseHeader加上以后会启用DedupeResponseHeaderGatewayFilterFactory 在其中,dedupe方法可以按照给定策略处理值。
private void dedupe(HttpHeaders headers, String name, Strategy strategy) { List<String> values = headers.get(name); if (values == null || values.size() <= 1) { return; } switch (strategy) { // 只保留第一个 case RETAIN_FIRST: headers.set(name, values.get(0)); break; // 保留最后一个 case RETAIN_LAST: headers.set(name, values.get(values.size() - 1)); break; // 去除值相同的 case RETAIN_UNIQUE: headers.put(name, values.stream().distinct().collect(Collectors.toList())); break; default: break; } }
如果请求中设置的Origin的值与我们自己设置的是同一个,例如生产环境设置的都是自己的域名xxx.com或者开发测试环境设置的都是*(浏览器中是无法设置Origin的值,设置了也不起作用,浏览器默认是当前访问地址),那么可以选用RETAIN_UNIQUE策略,去重后返回到前端。
如果请求中设置的Oringin的值与我们自己设置的不是同一个,RETAIN_UNIQUE策略就无法生效,比如 ”*“ 和 ”xxx.com“是两个不一样的Origin,最终还是会返回两个Access-Control-Allow-Origin 的头。此时,看代码里,response的header里,先加入的是我们自己配置的Access-Control-Allow-Origin的值,所以,我们可以将策略设置为RETAIN_FIRST ,只保留我们自己设置的。
大多数情况下,我们想要返回的是我们自己设置的规则,所以直接使用RETAIN_FIRST 即可。实际上,DedupeResponseHeader 可以针对所有头,做重复的处理。
手动写一个 CorsResponseHeaderFilter 的 GlobalFilter 去修改Response中的头
@Component public class CorsResponseHeaderFilter implements GlobalFilter, Ordered { private static final Logger logger = LoggerFactory.getLogger(CorsResponseHeaderFilter.class); private static final String ANY = "*"; @Override public int getOrder() { // 指定此过滤器位于NettyWriteResponseFilter之后 // 即待处理完响应体后接着处理响应头 return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1; } @Override @SuppressWarnings("serial") public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return chain.filter(exchange).then(Mono.fromRunnable(() -> { exchange.getResponse().getHeaders().entrySet().stream() .filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1)) .filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) || kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS) || kv.getKey().equals(HttpHeaders.VARY))) .forEach(kv -> { // Vary只需要去重即可 if(kv.getKey().equals(HttpHeaders.VARY)) kv.setValue(kv.getValue().stream().distinct().collect(Collectors.toList())); else{ List<String> value = new ArrayList<>(); if(kv.getValue().contains(ANY)){ //如果包含*,则取* value.add(ANY); kv.setValue(value); }else{ value.add(kv.getValue().get(0)); // 否则默认取第一个 kv.setValue(value); } } }); })); } }
此处有两个地方要注意:
-
根据下图可以看到,在取得返回值后,Filter的Order 值越大,越先处理Response,而真正将Response返回到前端的,是 NettyWriteResponseFilter, 我们要想在它之前修改Response,则Order 的值必须比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。
修改后置filter时,网上有些博客使用的是 Mono.defer去做的,这种做法,会从此filter开始,重新执行一遍它后面的其他filter,一般我们会添加一些认证或鉴权的 GlobalFilter ,就需要在这些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判断是否重复执行,否则可能会执行二次重复操作,所以建议使用fromRunnable 避免这种情况。
参考:
https://github.com/spring-cloud/spring-cloud-gateway/issues/728
https://blog.csdn.net/weixin_45727359/article/details/123343390