CORS跨域是一个开发中经常会遇到的问题,在SpringBoot单体项目中我们只需要添加一个Filter即可解决,而在SpringCloud微服务中并非如此顺利,希望通过本文可以帮你一次解决~
问题
在Spring Cloud项目中,前后端分离目前很常见,在调试时,会遇到两种情况的跨域:
(1) 前端页面通过不同域名或IP访问微服务的后台,例如前端人员会在本地起HttpServer 直连后台开发本地起的服务,此时,如果不加任何配置,前端页面的请求会被浏览器跨域限制拦截,所以,业务服务常常会添加如下代码设置全局跨域:
(2) 前端页面通过不同域名或IP访问SpringCloud Gateway,例如前端人员在本地起HttpServer直连服务器的Gateway进行调试。此时,同样会遇到跨域。需要在Gateway的配置文件中增加:
那么,此时直连微服务和网关的跨域问题都解决了,是不是很完美?
No~ 问题来了,前端仍然会报错:“不允许有多个’Access-Control-Allow-Origin’ CORS头”。
仔细查看返回的响应头,里面包含了两份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 :
网上有些关于修改Gateway的CORS设定的方式,是跟前面SpringBoot一样,实现一个CorsWebFilter的Bean,靠写代码提供 CorsConfiguration ,而不是修改Gateway的配置文件。其实本质,都是将配置交给corsProcessor去处理,殊途同归。但靠配置解决永远比hard code来的优雅。
该方法把Gateway里定义的所有的 GlobalFilter 加载进来,作为handler返回,但在返回前,先进行CORS校验,获取配置后,交给corsProcessor去处理,即DefaultCorsProcessor类
看下DefaultCorsProcessor的process方法:
可以看到,在DefaultCorsProcessor 中,根据我们在appliation.yml 中的配置,给Response添加了 Vary 和 Access-Control-Allow-Origin 的头。
再接下来就是进入各个GlobalFilter进行处理了,其中NettyRoutingFilter 是负责实际将请求转发给后台微服务,并获取Response的,重点看下代码中filter的处理结果的部分:
其中以下几种header会被过滤掉的:
很明显,在图里的第3步中,如果后台服务返回的header里有 Vary 和 Access-Control-Allow-Origin ,这时由于是putAll,没有做任何去重就加进去了,必然会重复,看看DEBUG结果验证一下:
验证了前面的发现。
解决
解决的方案有两种:
(1) 利用 DedupeResponseHeader 配置:
DedupeResponseHeader 加上以后会启用DedupeResponseHeaderGatewayFilterFactory 在其中,dedupe方法可以按照给定策略处理值
- 如果请求中设置的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 可以针对所有头,做重复的处理。
(2) 手动写一个 CorsResponseHeaderFilter 的 GlobalFilter 去修改Response中的头。
此处有两个地方要注意:
(1) 根据下图可以看到,在取得返回值后,Filter的Order 值越大,越先处理Response,而真正将Response返回到前端的,是 NettyWriteResponseFilter, 我们要想在它之前修改Response,则Order 的值必须比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。
(2) 修改后置filter时,网上有些博客使用的是 Mono.defer去做的,这种做法,会从此filter开始,重新执行一遍它后面的其他filter,一般我们会添加一些认证或鉴权的 GlobalFilter ,就需要在这些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判断是否重复执行,否则可能会执行二次重复操作,所以建议使用fromRunnable 避免这种情况