Spring Cloud Gateway CORS 方案看這篇就夠了
在 SpringCloud 項(xiàng)目中,前后端分離目前很常見,在調(diào)試時(shí),會(huì)遇到兩種情況的跨域:
前端頁(yè)面通過(guò)不同域名或IP訪問微服務(wù)的后臺(tái)
例如前端人員會(huì)在本地起HttpServer 直連后臺(tái)開發(fā)本地起的服務(wù),此時(shí),如果不加任何配置,前端頁(yè)面的請(qǐng)求會(huì)被瀏覽器跨域限制攔截,所以,業(yè)務(wù)服務(wù)常常會(huì)添加如下代碼設(shè)置全局跨域:
@Bean
public?CorsFilter?corsFilter()?{
????logger.debug("CORS限制打開");
????CorsConfiguration?config?=?new?CorsConfiguration();
????#?僅在開發(fā)環(huán)境設(shè)置為*
????config.addAllowedOrigin("*");
????config.addAllowedHeader("*");
????config.addAllowedMethod("*");
????config.setAllowCredentials(true);
????UrlBasedCorsConfigurationSource?configSource?=?new?UrlBasedCorsConfigurationSource();
????configSource.registerCorsConfiguration("/**",?config);
????return?new?CorsFilter(configSource);
}
前端頁(yè)面通過(guò)不同域名或IP訪問SpringCloud Gateway
例如前端人員在本地起HttpServer直連服務(wù)器的Gateway進(jìn)行調(diào)試。此時(shí),同樣會(huì)遇到跨域。需要在Gateway的配置文件中增加:
spring:
??cloud:
????gateway:
??????globalcors:
????????cors-configurations:
????????#?僅在開發(fā)環(huán)境設(shè)置為*
??????????'[/**]':
????????????allowedOrigins:?"*"
????????????allowedHeaders:?"*"
????????????allowedMethods:?"*"
那么,此時(shí)直連微服務(wù)和網(wǎng)關(guān)的跨域問題都解決了,是不是很完美?
No~ 問題來(lái)了,****前端仍然會(huì)報(bào)錯(cuò):“不允許有多個(gè)’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.
仔細(xì)查看返回的響應(yīng)頭,里面包含了兩份Access-Control-Allow-Origin頭。
我們用客戶端版的PostMan做一個(gè)模擬,在請(qǐng)求里設(shè)置頭:Origin : * ,查看返回結(jié)果的頭:
不能用Chrome插件版,由于瀏覽器的限制,插件版設(shè)置Origin的Header是無(wú)效的

發(fā)現(xiàn)問題了:Vary 和 Access-Control-Allow-Origin 兩個(gè)頭重復(fù)了兩次,其中瀏覽器對(duì)后者有唯一性限制!
分析
Spring Cloud Gateway是基于SpringWebFlux的,所有web請(qǐng)求首先是交給DispatcherHandler進(jìn)行處理的,將HTTP請(qǐng)求交給具體注冊(cè)的handler去處理。
我們知道Spring Cloud Gateway進(jìn)行請(qǐng)求轉(zhuǎn)發(fā),是在配置文件里配置路由信息,一般都是用url predicates模式,對(duì)應(yīng)的就是RoutePredicateHandlerMapping 。所以,DispatcherHandler會(huì)把請(qǐng)求交給 RoutePredicateHandlerMapping.

RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange) 方法,默認(rèn)提供者是其父類 AbstractHandlerMapping :
@Override
?public?Mono{
??return?getHandlerInternal(exchange).map(handler?->?{
???if?(logger.isDebugEnabled())?{
????logger.debug(exchange.getLogPrefix()?+?"Mapped?to?"?+?handler);
???}
???ServerHttpRequest?request?=?exchange.getRequest();
???//?可以看到是在這一行就進(jìn)行CORS判斷,兩個(gè)條件:
???//?1.?是否配置了CORS,如果不配的話,默認(rèn)是返回false的
???//?2.?或者當(dāng)前請(qǐng)求是OPTIONS請(qǐng)求,且頭里包含ORIGIN和ACCESS_CONTROL_REQUEST_METHOD
???if?(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;
??});
?}
網(wǎng)上有些關(guān)于修改Gateway的CORS設(shè)定的方式,是跟前面SpringBoot一樣,實(shí)現(xiàn)一個(gè)CorsWebFilter的Bean,靠寫代碼提供 CorsConfiguration ,而不是修改Gateway的配置文件。其實(shí)本質(zhì),都是將配置交給corsProcessor去處理,殊途同歸。但靠配置解決永遠(yuǎn)比hard code來(lái)的優(yōu)雅。
該方法把Gateway里定義的所有的 GlobalFilter 加載進(jìn)來(lái),作為handler返回,但在返回前,先進(jìn)行CORS校驗(yàn),獲取配置后,交給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?varyHeaders?=?responseHeaders.get(HttpHeaders.VARY);
????if?(varyHeaders?==?null)?{
????????//?第一次進(jìn)來(lái)時(shí),肯定是空,所以加了一次VERY的頭,包含ORIGIN,?ACCESS_CONTROL_REQUEST_METHOD和ACCESS_CONTROL_REQUEST_HEADERS
????????responseHeaders.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);
}
//?在這個(gè)類里進(jìn)行實(shí)際的CORS校驗(yàn)和處理
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?allowMethods?=?checkMethods(config,?requestMethod);
????if?(allowMethods?==?null)?{
????????logger.debug("Reject:?HTTP?'"?+?requestMethod?+?"'?is?not?allowed");
????????rejectRequest(response);
????????return?false;
????}
????List?requestHeaders?=?getHeadersToUse(request,?preFlightRequest);
????List?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 中,根據(jù)我們?cè)赼ppliation.yml 中的配置,給Response添加了 Vary 和 Access-Control-Allow-Origin 的頭。

再接下來(lái)就是進(jìn)入各個(gè)GlobalFilter進(jìn)行處理了,其中NettyRoutingFilter 是負(fù)責(zé)實(shí)際將請(qǐng)求轉(zhuǎn)發(fā)給后臺(tái)微服務(wù),并獲取Response的,重點(diǎn)看下代碼中filter的處理結(jié)果的部分:
其中以下幾種header會(huì)被過(guò)濾掉的:

很明顯,在圖里的第3步中,如果后臺(tái)服務(wù)返回的header里有 Vary 和 Access-Control-Allow-Origin ,這時(shí)由于是putAll,沒有做任何去重就加進(jìn)去了,必然會(huì)重復(fù),看看DEBUG結(jié)果驗(yàn)證一下:
驗(yàn)證了前面的發(fā)現(xiàn)。
解決的方案有兩種:
利用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加上以后會(huì)啟用DedupeResponseHeaderGatewayFilterFactory 在其中,dedupe方法可以按照給定策略處理值。
private?void?dedupe(HttpHeaders?headers,?String?name,?Strategy?strategy)?{
??List?values?=?headers.get(name);
??if?(values?==?null?||?values.size()?<=?1)?{
???return;
??}
??switch?(strategy)?{
??//?只保留第一個(gè)
??case?RETAIN_FIRST:
???headers.set(name,?values.get(0));
???break;
??//?保留最后一個(gè)????????
??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;
??}
?}
如果請(qǐng)求中設(shè)置的Origin的值與我們自己設(shè)置的是同一個(gè),例如生產(chǎn)環(huán)境設(shè)置的都是自己的域名xxx.com或者開發(fā)測(cè)試環(huán)境設(shè)置的都是*(瀏覽器中是無(wú)法設(shè)置Origin的值,設(shè)置了也不起作用,瀏覽器默認(rèn)是當(dāng)前訪問地址),那么可以選用RETAIN_UNIQUE策略,去重后返回到前端。
如果請(qǐng)求中設(shè)置的Oringin的值與我們自己設(shè)置的不是同一個(gè),RETAIN_UNIQUE策略就無(wú)法生效,比如 ”*“ 和 ”xxx.com“是兩個(gè)不一樣的Origin,最終還是會(huì)返回兩個(gè)Access-Control-Allow-Origin 的頭。此時(shí),看代碼里,response的header里,先加入的是我們自己配置的Access-Control-Allow-Origin的值,所以,我們可以將策略設(shè)置為RETAIN_FIRST ,只保留我們自己設(shè)置的。
大多數(shù)情況下,我們想要返回的是我們自己設(shè)置的規(guī)則,所以直接使用RETAIN_FIRST 即可。實(shí)際上,DedupeResponseHeader 可以針對(duì)所有頭,做重復(fù)的處理。
手動(dòng)寫一個(gè) 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()?{
????????//?指定此過(guò)濾器位于NettyWriteResponseFilter之后
????????//?即待處理完響應(yīng)體后接著處理響應(yīng)頭
????????return?NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER?+?1;
????}
????@Override
????@SuppressWarnings("serial")
????public?Mono?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?value?=?new?ArrayList<>();
????????????????????????????if(kv.getValue().contains(ANY)){??//如果包含*,則取*
????????????????????????????????value.add(ANY);
????????????????????????????????kv.setValue(value);
????????????????????????????}else{
????????????????????????????????value.add(kv.getValue().get(0));?//?否則默認(rèn)取第一個(gè)
????????????????????????????????kv.setValue(value);
????????????????????????????}
????????????????????????}
????????????????????});
????????}));
????}
}
此處有兩個(gè)地方要注意:
根據(jù)下圖可以看到,在取得返回值后,F(xiàn)ilter的Order 值越大,越先處理Response,而真正將Response返回到前端的,是 NettyWriteResponseFilter, 我們要想在它之前修改Response,則Order 的值必須比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。

修改后置filter時(shí),網(wǎng)上有些博客使用的是 Mono.defer去做的,這種做法,會(huì)從此filter開始,重新執(zhí)行一遍它后面的其他filter,一般我們會(huì)添加一些認(rèn)證或鑒權(quán)的 GlobalFilter ,就需要在這些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判斷是否重復(fù)執(zhí)行,否則可能會(huì)執(zhí)行二次重復(fù)操作,所以建議使用fromRunnable 避免這種情況。
轉(zhuǎn)自:Edison Xu
鏈接: http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html
我們創(chuàng)建了一個(gè)高質(zhì)量的技術(shù)交流群,與優(yōu)秀的人在一起,自己也會(huì)優(yōu)秀起來(lái),趕緊點(diǎn)擊加群,享受一起成長(zhǎng)的快樂。另外,如果你最近想跳槽的話,年前我花了2周時(shí)間收集了一波大廠面經(jīng),節(jié)后準(zhǔn)備跳槽的可以點(diǎn)擊這里領(lǐng)取!
推薦閱讀
你好,我是程序猿DD,10年開發(fā)老司機(jī)、阿里云MVP、騰訊云TVP、出過(guò)書、創(chuàng)過(guò)業(yè)、國(guó)企4年互聯(lián)網(wǎng)6年。10年前畢業(yè)加入宇宙行,工資不高、也不算太忙,業(yè)余堅(jiān)持研究技術(shù)和做自己想做的東西。4年后離開國(guó)企,加入永輝互聯(lián)網(wǎng)板塊的創(chuàng)業(yè)團(tuán)隊(duì),從開發(fā)、到架構(gòu)、到合伙人。一路過(guò)來(lái),給我最深的感受就是一定要不斷學(xué)習(xí)并關(guān)注前沿。只要你能堅(jiān)持下來(lái),多思考、少抱怨、勤動(dòng)手,就很容易實(shí)現(xiàn)彎道超車!所以,不要問我現(xiàn)在干什么是否來(lái)得及。如果你看好一個(gè)事情,一定是堅(jiān)持了才能看到希望,而不是看到希望才去堅(jiān)持。相信我,只要堅(jiān)持下來(lái),你一定比現(xiàn)在更好!如果你還沒什么方向,可以先關(guān)注我,這里會(huì)經(jīng)常分享一些前沿資訊,幫你積累彎道超車的資本。
