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

来源:https://www.cnblogs.com/javastack/archive/2023/09/14/17701725.html
-Advertisement-
Play Games

問題 在Spring Cloud項目中,前後端分離目前很常見,在調試時,會遇到兩種情況的跨域: 前端頁面通過不同功能變數名稱或IP訪問微服務的後臺,例如前端人員會在本地起HttpServer 直連後臺開發本地起的服務,此時,如果不加任何配置,前端頁面的請求會被瀏覽器跨域限制攔截,所以,業務服務常常會添加如下 ...


問題

在Spring Cloud項目中,前後端分離目前很常見,在調試時,會遇到兩種情況的跨域:

前端頁面通過不同功能變數名稱或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: "*"

那麼,此時直連微服務和網關的跨域問題都解決了,是不是很完美?

Spring Cloud 教程推薦:https://www.javastack.cn/categories/Spring-Cloud/

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是無效的

發現問題了:

VaryAccess-Control-Allow-Origin 兩個頭重覆了兩次,其中瀏覽器對後者有唯一性限制!

分析

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

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

那麼,接下來看下 RoutePredicateHandlerMapping.getHandler(ServerWebExchange exchange) 方法,預設提供者是其父類 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();
        // 可以看到是在這一行就進行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;
    });
}

註:

網上有些關於修改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_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<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添加了 VaryAccess-Control-Allow-Origin 的頭。

再接下來就是進入各個GlobalFilter進行處理了,其中NettyRoutingFilter 是負責實際將請求轉發給後臺微服務,並獲取Response的,重點看下代碼中filter的處理結果的部分:

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

很明顯,在圖裡的第3步中,如果後臺服務返回的header里有 VaryAccess-Control-Allow-Origin ,這時由於是putAll,沒有做任何去重就加進去了,必然會重覆,看看DEBUG結果驗證一下:

驗證了前面的發現。

解決

解決的方案有兩種:

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方法可以按照給定策略處理值

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 可以針對所有頭,做重覆的處理。

2. 手動寫一個 CorsResponseHeaderFilterGlobalFilter 去修改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之後
        // 即待處理完響應體後接著處理響應頭
        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 大。

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

作者:EdisonXu - 徐焱飛
來源:http://edisonxu.com/2020/10/14/spring-cloud-gateway-cors.html

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2022最新版)

2.勁爆!Java 協程要來了。。。

3.Spring Boot 2.x 教程,太全了!

4.別再寫滿屏的爆爆爆炸類了,試試裝飾器模式,這才是優雅的方式!!

5.《Java開發手冊(嵩山版)》最新發佈,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 線程間共用數據的問題 多線程之間共用數據,最大的問題便是數據競爭導致的異常問題。多個線程操作同一塊資源,如果不做任何限制,那麼一定會發生錯誤。例如: 1 int g_nResource = 0; 2 void thread_entry() 3 { 4 for (int i = 0; i < 1000 ...
  • 原本你寫的程式是靜態鏈接的系統的vulkan-1.dll,如果系統不存在vulkan-1.dll,則會直接崩潰。 關於將ncnn靜態鏈接vulkan改成動態載入vulkan的形式,然後提供這兩個函數 bool ncnn::has_vulkan(); void ncnn::use_vulkan(boo ...
  • 1 前言 高性能的HTTP和反向代理伺服器,Nginx用來: 搭建Web Server 作負載均衡 供配置的日誌欄位豐富,從各類HTTP頭部到內部性能數據都有 Nginx的訪問日誌中,存在499狀態碼的日誌。但常見4xx狀態碼只有400、401、403、404等,499並未在HTTP RFC文檔。這 ...
  • 本篇咱們從零開發一個quarkus應用,支持虛擬線程響應web服務,響應式操作postgresql資料庫,並且在quarkus官方還未支持的情況下,率先並將其製作成docker鏡像 ...
  • 本文深入探討Go語言中的流程式控制制語法,包括基本的if-else條件分支、for迴圈、switch-case多條件分支,以及與特定數據類型相關的流程式控制制,如for-range迴圈和type-switch。文章還詳細描述了goto、fallthrough等跳轉語句的使用方法,通過清晰的代碼示例為讀者提供 ...
  • 一、什麼是kafka,什麼是rabbit Kafka是由Scala語言開發的一種分散式流處理框架,主要用於處理活躍的流式數據,以及大數據量的數據處理。它採用發佈-訂閱模型,支持消息的批量處理,數據的存儲和獲取是本地磁碟順序批量操作,這使得消息處理的效率較高,吞吐量較大。 RabbitMQ則是由Erl ...
  • InlineHook 是一種電腦安全編程技術,其原理是在電腦程式執行期間進行攔截、修改、增強現有函數功能。它使用鉤子函數(也可以稱為回調函數)來截獲程式執行的各種事件,併在事件發生前或後進行自定義處理,從而控制或增強程式行為。Hook技術常被用於系統加速、功能增強、等領域。本章將重點講解Hook... ...
  • pycharm作為開發python程式的最適合編輯器,幾乎已經普及了所有的python開發者 但是還有很多同學不會免費使用pycharm的方法,今天我就給大家普及兩種Pycharm安裝和免費激活的方式 本文提供圖文教程,一步一步地演示永久激活 Pycharm 的方法。適用於最新的幾個版本,步驟簡單, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...