<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>

          Spring Cloud 輕松解決跨域,別再亂用了!

          共 20877字,需瀏覽 42分鐘

           ·

          2022-10-28 08:33

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

          ?

          今天是《Spring Cloud Alibaba 微服務(wù)實(shí)戰(zhàn)課》 1024 程序員節(jié)活動最后一天,別再錯過這次的特惠價了,點(diǎn)擊這個鏈接可了解、參與報(bào)名學(xué)習(xí)。

          ?

          問題

          在Spring Cloud項(xiàng)目中,前后端分離目前很常見,在調(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進(jìn)行調(diào)試。此時,同樣會遇到跨域。需要在Gateway的配置文件中增加:

          spring:
            cloud:
              gateway:
                globalcors:
                  cors-configurations:
                  # 僅在開發(fā)環(huán)境設(shè)置為*
                    '[/**]':
                      allowedOrigins: "*"
                      allowedHeaders: "*"
                      allowedMethods: "*"

          那么,此時直連微服務(wù)和網(wǎng)關(guān)的跨域問題都解決了,是不是很完美?No~ 問題來了,前端仍然會報(bào)錯:不允許有多個’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做一個模擬,在請求里設(shè)置頭:Origin : * ,查看返回結(jié)果的頭:

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

          發(fā)現(xiàn)問題了:

          VaryAccess-Control-Allow-Origin 兩個頭重復(fù)了兩次,其中瀏覽器對后者有唯一性限制!最新 Spring Cloud 面試題整理好了,大家可以在Java面試庫小程序在線刷題。

          分析

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

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

          那么,接下來看下 RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange) 方法,默認(rèn)提供者是其父類 AbstractHandlerMapping

          @Override
          public Mono<Object> getHandler(ServerWebExchange exchange) {
              return getHandlerInternal(exchange).map(handler -> {
                  if (logger.isDebugEnabled()) {
                      logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
                  }
                  ServerHttpRequest request = exchange.getRequest();
                  // 可以看到是在這一行就進(jìn)行CORS判斷,兩個條件:
                  // 1. 是否配置了CORS,如果不配的話,默認(rèn)是返回false
                  // 2. 或者當(dāng)前請求是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一樣,實(shí)現(xiàn)一個CorsWebFilter的Bean,靠寫代碼提供 CorsConfiguration ,而不是修改Gateway的配置文件。其實(shí)本質(zhì),都是將配置交給corsProcessor去處理,殊途同歸。但靠配置解決永遠(yuǎn)比hard code來的優(yōu)雅。

          該方法把Gateway里定義的所有的 GlobalFilter 加載進(jìn)來,作為handler返回,但在返回前,先進(jìn)行CORS校驗(yàn),獲取配置后,交給corsProcessor去處理,即DefaultCorsProcessor

          看下DefaultCorsProcessorprocess方法:

          @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) {
                  // 第一次進(jìn)來時,肯定是空,所以加了一次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);
          }

          // 在這個類里進(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<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 中,根據(jù)我們在appliation.yml 中的配置,給Response添加了 VaryAccess-Control-Allow-Origin 的頭。

          再接下來就是進(jìn)入各個GlobalFilter進(jìn)行處理了,其中NettyRoutingFilter 是負(fù)責(zé)實(shí)際將請求轉(zhuǎn)發(fā)給后臺微服務(wù),并獲取Response的,重點(diǎn)看下代碼中filter的處理結(jié)果的部分:

          其中以下幾種header會被過濾掉的:

          很明顯,在圖里的第3步中,如果后臺服務(wù)返回的header里有 VaryAccess-Control-Allow-Origin ,這時由于是putAll,沒有做任何去重就加進(jìn)去了,必然會重復(fù),看看DEBUG結(jié)果驗(yàn)證一下:

          驗(yàn)證了前面的發(fā)現(xiàn)。

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

          解決

          解決的方案有兩種:

          1. 利用 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方法可以按照給定策略處理。

          Spring Boot 基礎(chǔ)就不介紹了,推薦下這個實(shí)戰(zhàn)教程:https://github.com/javastacks/spring-boot-best-practice

          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;
            }
           }
          • 如果請求中設(shè)置的Origin的值與我們自己設(shè)置的是同一個,例如生產(chǎn)環(huán)境設(shè)置的都是自己的域名xxx.com或者開發(fā)測試環(huán)境設(shè)置的都是*(瀏覽器中是無法設(shè)置Origin的值,設(shè)置了也不起作用,瀏覽器默認(rèn)是當(dāng)前訪問地址),那么可以選用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 即可。實(shí)際上,DedupeResponseHeader 可以針對所有頭,做重復(fù)的處理。

          2. 手動寫一個 CorsResponseHeaderFilterGlobalFilter 去修改Response中的頭。

          Spring Boot 基礎(chǔ)就不介紹了,推薦下這個實(shí)戰(zhàn)教程:https://github.com/javastacks/spring-boot-best-practice

          @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<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)); // 否則默認(rèn)取第一個
                                          kv.setValue(value);
                                      }
                                  }
                              });
                  }));
              }
          }

          此處有兩個地方要注意:

          1)根據(jù)下圖可以看到,在取得返回值后,F(xiàn)ilter的Order 值越大,越先處理Response,而真正將Response返回到前端的,是 NettyWriteResponseFilter, 我們要想在它之前修改Response,則Order 的值必須比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。

          2)修改后置filter時,網(wǎng)上有些文字使用的是 Mono.defer去做的,這種做法,會從此filter開始,重新執(zhí)行一遍它后面的其他filter,一般我們會添加一些認(rèn)證或鑒權(quán)的 GlobalFilter ,就需要在這些filter里用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判斷是否重復(fù)執(zhí)行,否則可能會執(zhí)行二次重復(fù)操作,所以建議使用fromRunnable 避免這種情況。

          作者:EdisonXu - 徐焱飛 

          來源:http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html

          End


          Spring Boot 學(xué)習(xí)筆記,這個太全了!

          23 種設(shè)計(jì)模式實(shí)戰(zhàn)(很全)

          Nacos 2.1.1 正式發(fā)布,真心強(qiáng)!

          Spring Cloud Alibaba 最新重磅發(fā)布!

          Stream 中的 map、peek、foreach 方法的區(qū)別?

          Spring Cloud 微服務(wù)最新課程!

          瀏覽 58
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  免费看黄色大片 | 韩国一区在线 | 欧美巨大性爱视频 | 99999亚洲 | 免费操逼的视频 |