欢迎关注方志朋的博客,回复”666“获面试宝典

在 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 : * ,查看返回结果的头:

不能用Chrome插件版,由于浏览器的限制,插件版设置Origin的Header是无效的


发现问题了:Vary 和 Access-Control-Allow-Origin 两个头重复了两次,其中浏览器对后者有唯一性限制!

分析

Spring Cloud Gateway是基于SpringWebFlux的,所有web请求首先是交给DispatcherHandler进行处理的,将HTTP请求交给具体注册的handler去处理。

我们知道Spring Cloud Gateway进行请求转发,是在配置文件里配置路由信息,一般都是用url predicates模式,对应的就是RoutePredicateHandlerMapping 。所以,DispatcherHandler会把请求交给 RoutePredicateHandlerMapping.

图片

RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange) 方法,默认提供者是其父类 AbstractHandlerMapping :

@Overridepublic Mono<Object> getHandler(ServerWebExchange exchange) {return getHandlerInternal(exchange).map(handler -> {if (logger.isDebugEnabled()) {logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);}ServerHttpRequest request = exchange.getRequest();// 可以看到是在这一行就进行CORS判断,两个条件:// 1. 是否配置了CORS,如果不配的话,默认是返回false的// 2. 或者当前请求是OPTIONS请求,且头里包含ORIGIN和ACCESS_CONTROL_REQUEST_METHODif (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null);CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);config = (config != null ? config.combine(handlerConfig) : handlerConfig);//此处交给DefaultCorsProcessor去处理了if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {return REQUEST_HANDLED_HANDLER;}}return handler;});}

网上有些关于修改Gateway的CORS设定的方式,是跟前面SpringBoot一样,实现一个CorsWebFilter的Bean,靠写代码提供 CorsConfiguration ,而不是修改Gateway的配置文件。其实本质,都是将配置交给corsProcessor去处理,殊途同归。但靠配置解决永远比hard code来的优雅。

该方法把Gateway里定义的所有的 GlobalFilter 加载进来,作为handler返回,但在返回前,先进行CORS校验,获取配置后,交给corsProcessor去处理,即DefaultCorsProcessor类

看下DefaultCorsProcessor的process方法

@Override
public boolean process(@Nullable CorsConfiguration config, ServerWebExchange exchange) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();HttpHeaders responseHeaders = response.getHeaders();List<String> varyHeaders = responseHeaders.get(HttpHeaders.VARY);if (varyHeaders == null) {// 第一次进来时,肯定是空,所以加了一次VERY的头,包含ORIGIN, ACCESS_CONTROL_REQUEST_METHOD和ACCESS_CONTROL_REQUEST_HEADERSresponseHeaders.addAll(HttpHeaders.VARY, VARY_HEADERS);}else {for (String header : VARY_HEADERS) {if (!varyHeaders.contains(header)) {responseHeaders.add(HttpHeaders.VARY, header);}}}if (!CorsUtils.isCorsRequest(request)) {return true;}if (responseHeaders.getFirst(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN) != null) {logger.trace("Skip: response already contains \"Access-Control-Allow-Origin\"");return true;}boolean preFlightRequest = CorsUtils.isPreFlightRequest(request);if (config == null) {if (preFlightRequest) {rejectRequest(response);return false;}else {return true;}}return handleInternal(exchange, config, preFlightRequest);
}// 在这个类里进行实际的CORS校验和处理
protected boolean handleInternal(ServerWebExchange exchange,CorsConfiguration config, boolean preFlightRequest) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();HttpHeaders responseHeaders = response.getHeaders();String requestOrigin = request.getHeaders().getOrigin();String allowOrigin = checkOrigin(config, requestOrigin);if (allowOrigin == null) {logger.debug("Reject: '" + requestOrigin + "' origin is not allowed");rejectRequest(response);return false;}HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);List<HttpMethod> allowMethods = checkMethods(config, requestMethod);if (allowMethods == null) {logger.debug("Reject: HTTP '" + requestMethod + "' is not allowed");rejectRequest(response);return false;}List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);List<String> allowHeaders = checkHeaders(config, requestHeaders);if (preFlightRequest && allowHeaders == null) {logger.debug("Reject: headers '" + requestHeaders + "' are not allowed");rejectRequest(response);return false;}//此处添加了AccessControllAllowOrigin的头responseHeaders.setAccessControlAllowOrigin(allowOrigin);if (preFlightRequest) {responseHeaders.setAccessControlAllowMethods(allowMethods);}if (preFlightRequest && !allowHeaders.isEmpty()) {responseHeaders.setAccessControlAllowHeaders(allowHeaders);}if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());}if (Boolean.TRUE.equals(config.getAllowCredentials())) {responseHeaders.setAccessControlAllowCredentials(true);}if (preFlightRequest && config.getMaxAge() != null) {responseHeaders.setAccessControlMaxAge(config.getMaxAge());}return true;
}

可以看到,在DefaultCorsProcessor 中,根据我们在appliation.yml 中的配置,给Response添加了 Vary 和 Access-Control-Allow-Origin 的头。

图片

再接下来就是进入各个GlobalFilter进行处理了,其中NettyRoutingFilter 是负责实际将请求转发给后台微服务,并获取Response的,重点看下代码中filter的处理结果的部分:

图片

其中以下几种header会被过滤掉的:

图片

很明显,在图里的第3步中,如果后台服务返回的header里有 Vary 和 Access-Control-Allow-Origin ,这时由于是putAll,没有做任何去重就加进去了,必然会重复,看看DEBUG结果验证一下:

验证了前面的发现。

解决的方案有两种:

利用DedupeResponseHeader配置

spring:cloud:gateway:globalcors:cors-configurations:'[/**]':allowedOrigins: "*"allowedHeaders: "*"allowedMethods: "*"default-filters:- 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 = "*";@Overridepublic 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);}}});}));}
}

此处有两个地方要注意:

  1. 根据下图可以看到,在取得返回值后,Filter的Order 值越大,越先处理Response,而真正将Response返回到前端的,是 NettyWriteResponseFilter, 我们要想在它之前修改Response,则Order 的值必须比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。

图片
  1. 修改后置filter时,网上有些博客使用的是 Mono.defer去做的,这种做法,会从此filter开始,重新执行一遍它后面的其他filter,一般我们会添加一些认证或鉴权的 GlobalFilter ,就需要在这些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判断是否重复执行,否则可能会执行二次重复操作,所以建议使用fromRunnable 避免这种情况。

转自:Edison Xu

链接: http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html

热门内容:

  • 干掉visio,这个画图神器真的绝了!!!

  • 尽快卸载这两款恶意浏览器插件!已有近 50 万用户安装

  • UUID正在被NanoID取代?

  • 新来了个技术总监:谁再用 @Async 创建线程以后就不用来了!!

  • 最新 955 不加班的公司名单(2022版)

  • 我妈今年 70 岁,受不了Windows蓝屏,用了 21 年的 Linux!YYDS!

最近面试BAT,整理一份面试资料《Java面试BAT通关手册》,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。

明天见(。・ω・。)ノ♡

Spring Cloud Gateway CORS 方案看这篇就够了相关推荐

  1. Spring Cloud Gateway之路由断言工厂篇

    1. 背景 最近,需要提升系统安全性,市面上有很多款网关服务的技术方案,最终选择了Spring Cloud Gateway. 2. Spring Cloud Gateway工作机制 官网配图: 客户端 ...

  2. spring cloud Netflix全套面试,这一篇就够了,3万字整理

    *spring cloud Netflix* 面试:springcloud https://blog.csdn.net/weixin_46577306/article/details/10690608 ...

  3. 前端跨域方案看这篇就够了

    文章目录 前言 跨域解决的方法 1.JSONP 2.CORS跨域资源共享 3.http proxy => webpack webpack-dev-server 4.nginx反向代理 5.pos ...

  4. 实战 Spring Cloud Gateway 之限流篇

    来源:https://www.aneasystone.com/archives/2020/08/spring-cloud-gateway-current-limiting.html 话说在 Sprin ...

  5. 【Spring Cloud Alibaba 实战 | 总结篇】Spring Cloud Gateway + Spring Security OAuth2 + JWT 实现微服务统一认证授权和鉴权

    一. 前言 hi,大家好~ 好久没更文了,期间主要致力于项目的功能升级和问题修复中,经过一年时间这里只贴出关键部分代码的打磨,[有来]终于迎来v2.0版本,相较于v1.x版本主要完善了OAuth2认证 ...

  6. 这可能是全网Spring Cloud Gateway限流最完整的方案了!

        作者:aneasystone     https://www.aneasystone.com/ 话说在 Spring Cloud Gateway 问世之前,Spring Cloud 的微服务世 ...

  7. spring cloud gateway之filter篇

    点击上方"方志朋",选择"置顶或者星标" 你的关注意义重大! 在上一篇文章详细的介绍了Gateway的Predict,Predict决定了请求由哪一个路由处理, ...

  8. Spring Cloud入门,看这篇就够了!

    点击▲关注 "中生代技术"   给公众号标星置顶 更多精彩 第一时间直达 概述 首先我给大家看一张图,如果大家对这张图有些地方不太理解的话,我希望你们看完我这篇文章会恍然大悟. 什 ...

  9. 【云原生微服务>SCG网关篇十二】Spring Cloud Gateway集成Sentinel API实现多种限流方式

    文章目录 一.前言 二.Gateway集成Sentinel API 0.集成Sentinel的核心概念 1)GatewayFlowRule 和 ApiDefinition 2)GatewayFlowR ...

最新文章

  1. C/S框架网介绍|.NET快速开发平台|Winform开发框架
  2. 什么东西都要用一句话总结出来:这是最重要的
  3. 免费网络学术资源获取
  4. Java多线程之CAS缺点
  5. CF960G-Bandit Blues【第一类斯特林数,分治,NTT】
  6. java gzipoutputstream_java – GZIPInputStream逐行读取
  7. 【C语言】第七章 模块化与函数 题解
  8. MySQL Workbench Failed to Connect to MySQL at 127.0.0.1:3306 with user root Bad handshake
  9. Linux—图解rsyslog及通过 Loganalyzer实现集中式日志管控
  10. Darwin Streaming Server 安装流程
  11. python的with用法
  12. 袋鼠云服务案例系列 | 从DB2到MySQL,某传统金融平台的互联网转型之路
  13. 浏览器插件 - Chrome 对 UserScript 的声明头(metadata)兼容性一览
  14. 人工智能是否将拥有人类意识?
  15. html5轮播怎么自动换图,如何使用JavaScript实现“无缝滚动 自动播放”轮播图效果...
  16. 大平台压榨亏损2000万怎么办?换流量变现策略才是王道!
  17. 关于求矩阵主对角线元素之和及副对角线元素之和的问题
  18. Html5 学前须知
  19. python基础练习题(一)
  20. 适用于Windows 10的所有Microsoft PowerToys的全部解释

热门文章

  1. 简单配置nginx反向代理,实现跨域请求
  2. 简介子窗口控件(api)
  3. 开发工具Drawscript
  4. C# 最快的逐一打印斐波那契结果数列的算法
  5. Linux 守护进程,编写(转载)
  6. 刻意练习:LeetCode实战 -- Task23. 不同的二叉搜索树 II
  7. 【计算机视觉】EmguCV学习笔记(4)分离颜色通道以及多通道图像混合
  8. 【UVA】10152 ShellSort (几只乌龟的故事)
  9. 25 年汽车技术老兵亲述,自动驾驶新驶向
  10. 5折票倒计时3天 | 超干货议程首度曝光!2019 中国大数据技术大会邀您共赴