<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringCloudGateway CORS方案看這篇就夠了

          共 9406字,需瀏覽 19分鐘

           ·

          2022-03-10 08:49

          點擊關(guān)注公眾號,Java干貨及時送達

          轉(zhuǎn)自:Edison Xu

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

          問 題


          在 SpringCloud 項目中,前后端分離目前很常見,在調(diào)試時,會遇到兩種情況的跨域:

          | 前端頁面通過不同域名或IP訪問微服務(wù)的后臺

          例如前端人員會在本地起HttpServer 直連后臺開發(fā)本地起的服務(wù),此時,如果不加任何配置,前端頁面的請求會被瀏覽器跨域限制攔截,所以,業(yè)務(wù)服務(wù)常常會添加如下代碼設(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);
          }

          | 前端頁面通過不同域名或IP訪問SpringCloud Gateway

          例如前端人員在本地起HttpServer直連服務(wù)器的Gateway進行調(diào)試。此時,同樣會遇到跨域。需要在Gateway的配置文件中增加:
          spring:
          ??cloud:
          ????gateway:
          ??????globalcors:
          ????????cors-configurations:
          ????????#?僅在開發(fā)環(huán)境設(shè)置為*
          ??????????'[/**]':
          ????????????allowedOrigins:?"*"
          ????????????allowedHeaders:?"*"
          ????????????allowedMethods:?"*"
          那么,此時直連微服務(wù)和網(wǎng)關(guān)的跨域問題都解決了,是不是很完美?

          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.
          仔細查看返回的響應(yīng)頭,里面包含了兩份Access-Control-Allow-Origin頭。

          我們用客戶端版的PostMan做一個模擬,在請求里設(shè)置頭:Origin : *?,查看返回結(jié)果的頭:

          不能用Chrome插件版,由于瀏覽器的限制,插件版設(shè)置Origin的Header是無效的

          發(fā)現(xiàn)問題了:Vary 和 Access-Control-Allow-Origin 兩個頭重復(fù)了兩次,其中瀏覽器對后者有唯一性限制!

          分 析


          Spring Cloud Gateway是基于SpringWebFlux的,所有web請求首先是交給DispatcherHandler進行處理的,將HTTP請求交給具體注冊的handler去處理。

          我們知道Spring Cloud Gateway進行請求轉(zhuǎn)發(fā),是在配置文件里配置路由信息,一般都是用url predicates模式,對應(yīng)的就是RoutePredicateHandlerMapping 。所以,DispatcherHandler會把請求交給 RoutePredicateHandlerMapping.

          RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange)?方法,默認提供者是其父類 AbstractHandlerMapping :

          @Override
          ?public?Mono?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_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一樣,實現(xiàn)一個CorsWebFilter的Bean,靠寫代碼提供 CorsConfiguration ,而不是修改Gateway的配置文件。其實本質(zhì),都是將配置交給corsProcessor去處理,殊途同歸。但靠配置解決永遠比hard code來的優(yōu)雅。

          該方法把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?varyHeaders?=?responseHeaders.get(HttpHeaders.VARY);
          ????if?(varyHeaders?==?null)?{
          ????????//?第一次進來時,肯定是空,所以加了一次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);
          }

          //?在這個類里進行實際的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?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ù)我們在appliation.yml 中的配置,給Response添加了 Vary 和 Access-Control-Allow-Origin 的頭。

          再接下來就是進入各個GlobalFilter進行處理了,其中NettyRoutingFilter 是負責實際將請求轉(zhuǎn)發(fā)給后臺微服務(wù),并獲取Response的,重點看下代碼中filter的處理結(jié)果的部分:
          其中以下幾種header會被過濾掉的:
          很明顯,在圖里的第3步中,如果后臺服務(wù)返回的header里有 Vary 和 Access-Control-Allow-Origin ,這時由于是putAll,沒有做任何去重就加進去了,必然會重復(fù),看看DEBUG結(jié)果驗證一下:
          驗證了前面的發(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加上以后會啟用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)?{
          ??//?只保留第一個
          ??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;
          ??}
          ?}
          如果請求中設(shè)置的Origin的值與我們自己設(shè)置的是同一個,例如生產(chǎn)環(huán)境設(shè)置的都是自己的域名xxx.com或者開發(fā)測試環(huán)境設(shè)置的都是*(瀏覽器中是無法設(shè)置Origin的值,設(shè)置了也不起作用,瀏覽器默認是當前訪問地址),那么可以選用RETAIN_UNIQUE策略,去重后返回到前端。
          如果請求中設(shè)置的Oringin的值與我們自己設(shè)置的不是同一個,RETAIN_UNIQUE策略就無法生效,比如 ”*“ 和 ”xxx.com“是兩個不一樣的Origin,最終還是會返回兩個Access-Control-Allow-Origin 的頭。此時,看代碼里,response的header里,先加入的是我們自己配置的Access-Control-Allow-Origin的值,所以,我們可以將策略設(shè)置為RETAIN_FIRST ,只保留我們自己設(shè)置的。

          大多數(shù)情況下,我們想要返回的是我們自己設(shè)置的規(guī)則,所以直接使用RETAIN_FIRST 即可。實際上,DedupeResponseHeader 可以針對所有頭,做重復(fù)的處理。

          | 手動寫一個 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之后
          ????????//?即待處理完響應(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));?//?否則默認取第一個
          ????????????????????????????????kv.setValue(value);
          ????????????????????????????}
          ????????????????????????}
          ????????????????????});
          ????????}));
          ????}
          }
          此處有兩個地方要注意:
          1. 根據(jù)下圖可以看到,在取得返回值后,F(xiàn)ilter的Order 值越大,越先處理Response,而真正將Response返回到前端的,是 NettyWriteResponseFilter, 我們要想在它之前修改Response,則Order 的值必須比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。
          spring-cloud-gateway-fliter-order.png
          2. 修改后置filter時,網(wǎng)上有些博客使用的是 Mono.defer去做的,這種做法,會從此filter開始,重新執(zhí)行一遍它后面的其他filter,一般我們會添加一些認證或鑒權(quán)的 GlobalFilter ,就需要在這些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange)?方法去判斷是否重復(fù)執(zhí)行,否則可能會執(zhí)行二次重復(fù)操作,所以建議使用fromRunnable 避免這種情況。

          ????

          1發(fā)SQL

          2?Chrome

          3SpringBoot44Java

          4QQ

          5SpringBoot?發(fā)

          瀏覽 40
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                    <th id="afajh"><progress id="afajh"></progress></th>
                    五月天婷色 | 国产成人精品一级毛片 | 天天色天天日天天干 | 婷婷视频免费毛片在线观看 | 亲亲羞羞色色 |