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
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...