【SpringCloud技術專題】「Gateway閘道器係列」(3)微服務閘道器服務的Gateway全流程開發實踐指南(2.2.X)

浩宇天尚發表於2022-01-15

開發指南須知

本次實踐主要在版本:2.2.0.BUILD-SNAPSHOT上進行構建,這個專案提供了構建在Spring生態系統之上API閘道器。

Spring Cloud Gateway的介紹

Spring Cloud Gateway目標是用一個簡單、有效的方式路由到API,並且提供橫切的一些關注點,例如:安全、監控、系統效能和彈性等。

API閘道器介紹

API 閘道器出現的原因是微服務架構的出現,不同的微服務一般會有不同的網路地址,而外部客戶端可能需要呼叫多個服務的介面才能完成一個業務需求,如果讓客戶端直接與各個微服務通訊,會有以下的問題:

  1. 客戶端會多次請求不同的微服務,增加了客戶端的複雜性。
  2. 存在跨域請求,在一定場景下處理相對複雜。
  3. 認證複雜,每個服務都需要獨立認證。
  4. 難以重構,隨著專案的迭代,可能需要重新劃分微服務。
    • 例如,可能將多個服務合併成一個或者將一個服務拆分成多個。如果客戶端直接與微服務通訊,那麼重構將會很難實施。
  5. 某些微服務可能使用了防火牆 / 瀏覽器不友好的協議,直接訪問會有一定的困難。

以上這些問題可以藉助 API 閘道器解決。API 閘道器是介於客戶端和伺服器端之間的中間層,所有的外部請求都會先經過 API 閘道器這一層。也就是說,API 的實現方面更多的考慮業務邏輯,而安全、效能、監控可以交由 API 閘道器來做,這樣既提高業務靈活性又不缺安全性。

SpringCloud Gateway技術基礎

它是基於spring官方Spring 5.0、Spring Boot2.0和Project Reactor等技術開發的閘道器,Spring Cloud Gateway旨在為微服務架構提供簡單、有效和統一的API路由管理方式,Spring Cloud Gateway作為Spring Cloud生態系統中的閘道器,目標是替代Netflix Zuul,其不僅提供統一的路由方式,並且還基於Filer鏈的方式提供了閘道器基本的功能,例如:安全、監控/埋點、限流等。

如何引用Spring Cloud Gateway

maven座標為:

<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

有關使用當前Spring Cloud構建系統的詳細資訊,如果你引入了starter,但不想開啟gateway,可以設定:

spring.cloud.gateway.enabled=false。

注意

  1. Spring Cloud Gateway 構建在 Spring Boot 2.0, Spring WebFlux, and Project Reactor之上,因此,許多熟悉的同步庫(例如:Spring Data 、Spring Security)或模式不適用於Spring Cloud Gateway。

  2. Spring Cloud Gateway需要SpringBoot和SpringWebFlux提供的netty執行時,它不再執行於傳統的Servlet容器或一個WAR包。

Route

閘道器基本構件塊,也是閘道器最基礎的部分,路由資訊有一個ID、一個目的URL、一組斷言predicates和一組filters組成。如果聚合斷言為真,則匹配路由,說明請求的URL和配置。

Predicate

Java8中的斷言函式。Spring Cloud Gateway中的斷言函式輸入型別是Spring5.0框架中的ServerWebExchange。Spring Cloud Gateway中的斷言函式允許開發者去定義匹配來自於http request中的任何資訊,比如請求頭和引數等。

Java 8 Function Predicate. 輸入型別是SpringFramework ServerWebExchange. 這允許開發人員匹配來自HTTP請求的任何內容,例如頭或引數。

Filter

使用特定工廠構造的 Spring Framework GatewayFilter 例項。在這裡,可以在傳送downstream 請求之前或之後修改requests和responses。

一個標準的Spring webFilter。Spring cloud gateway中的filter分為兩種型別的Filter,分別是Gateway Filter和Global Filter。過濾器Filter將會對請求和響應進行修改處理。

理解:
  1. 斷言(Predicate):請求匹配;
  2. 過濾器(Filter):對請求或者返回進行過濾增強。

閘道器提供API全託管服務,豐富的API管理功能,輔助企業管理大規模的API,以降低管理成本和安全風險,包括協議適配、協議轉發、安全策略、防刷、流量、監控日誌等功能。一般來說閘道器對外暴露的URL或者介面資訊,我們統稱為路由資訊。

Spring Cloud Gateway的工作原理

GatewayClient請求 Spring Cloud Gateway,如果Gateway Handler Mapping 確定請求與路由匹配,該請求被髮送到Gateway Web Handler。此Handler執行時傳送請求到具體的請求,其中通過過濾器鏈。

過濾器鏈被虛線分隔的原因是過濾器可以在傳送代理請求之前或之後執行邏輯。執行所有“預”過濾邏輯,然後發出代理請求。在發出代理請求後,將執行“post”過濾器邏輯。URIs 在路由中沒有設定埠,則按照HTTP和HTTPS預設埠設定為80和443。

Spring cloud Gateway發出請求。然後再由Gateway Handler Mapping中找到與請求相匹配的路由,將其傳送到Gateway web handler。Handler再通過指定的過濾器鏈將請求傳送到我們實際的服務執行業務邏輯,然後返回。

Spring Cloud Gateway-路由斷言工廠

Spring Cloud Gateway匹配路由作為SpringWebFlux HandlerMapping基礎設施的一部分。Spring Cloud Gateway包含許多內建的路由斷言工廠,這些斷言匹配不同屬性的HTTP請求,可以組合多個路由斷言工廠,並通過邏輯組合。

Before Route Predicate Factory

Before Route Predicate Factory 有一個時間引數,此斷言匹配發生在該時間引數之前的請求。

這個路由匹配發生在 Jan 20, 2017 17:42 Mountain Time (Denver)之前的請求。

After Route Predicate Factory

After Route Predicate Factory有一個時間引數,此斷言匹配發生在該時間引數之後的請求。

這個路由匹配發生在 Jan 20, 2017 17:42 Mountain Time (Denver)之後的請求。

Between Route Predicate Factory

Between Route Predicate Factory 有兩個時間引數。此斷言匹配發生在這兩個時間之間的請求。

這個路由匹配發生在 Jan 20, 2017 17:42 Mountain Time (Denver)與Jan 21, 2017 17:42 Mountain Time (Denver)之間的請求。可應用於維護視窗。

Cookie Route Predicate Factory 有兩個引數,包括cookie名稱和正規表示式。此斷言匹配cookies包括給定的名稱和符合正規表示式的值。

此路由匹配cookie名稱為chocolate ,cookie值為ch.p正規表示式,匹配chap,chbp等。

Header Route Predicate Factory

Header Route Predicate Factory 包括兩個引數包括頭名稱和值的正規表示式。此斷言匹配一個頭資訊包括該名稱和符合該正規表示式值得請求。

此路由匹配頭名稱為X-Request-Id且值匹配\d+ 表示式(包含一個或多個數字)。

Host Route Predicate Factory

Host Route Predicate Factory包括一個引數host 名稱模式列表。此模式是一種 Ant 風格模式,以 "." 作為分隔符。此斷言匹配Host頭。另外Host頭來源有兩種:第一種是請求地址;第二種是自己在http的header頭中放入Host變數值。

URI 模板變數也支援這種格式 {sub}.myhost.org。

此路由匹配標頭檔案中的Host值www.somehost.org,beta.somehost.org 或 www.anotherhost.org。此示例均為預設埠80,如果為其他埠,需要在表示式中定義。

此斷言提取URI模板變數(如上面示例中定義的子變數)作為名稱和值的對映,並將其放置在ServerWebExchange.getAttributes()中,其鍵在ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE屬性中定義。

在過濾其中加入如下程式碼示例:
Map uri_template_variables_attribute=exchange.getAttribute(ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
   // Map variables=ServerWebExchangeUtils.getUriTemplateVariables(exchange);
     uri_template_variables_attribute.forEach((K,V)->{
            System.out.println("K:"+K+"--V:"+V);
      });
  • 訪問Host為:www.myhost.org
  • 輸出結果:  K:sub--V:www

Filter規律器

Global Filter介面與GatewayFilter具有相同的簽名。這些是有條件地應用於所有路由的特殊過濾器。

Combined Global Filter and GatewayFilter Ordering
  • 當請求進入(並匹配路由)時,Filtering Web Handler會將GlobalFilter的所有例項和GatewayFilter的所有路由特定例項新增到過濾器鏈。

  • 此組合過濾器鏈通過org.springframework.core.Ordered介面進行排序。可以通過實現getOrder()方法或者使用@Order註解。

Spring Cloud Gateway區分了過濾器邏輯執行的“請求”和“響應”階段,具有最高優先順序的過濾器將是“請求”階段的第一個和“響應”階段的最後一個 。

ANT萬用字元有三種:

而上面多數的匹配規則運算子號都是有AntPathMatcher物件進行實現的

private AntPathMatcher antPathMatcher = new AntPathMatcher();
  • ?:匹配任意一個單個字元
    • :匹配0或者任意數量的字元
  • ** :匹配0和或者更多目錄的字元
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getURI().getPath();
        //商城api介面,校驗使用者必須登入
        if(antPathMatcher.match("/api/**/auth/**", path)) {
            List<String> tokenList = request.getHeaders().get("token");
            if(null == tokenList) {
                ServerHttpResponse response = exchange.getResponse();
                return out(response);
            } else {
//                Boolean isCheck = JwtUtils.checkToken(tokenList.get(0));
//                if(!isCheck) {
                    ServerHttpResponse response = exchange.getResponse();
                    return out(response);
//                }
            }
        }
        //內部服務介面,不允許外部訪問
        if(antPathMatcher.match("/**/inner/**", path)) {
            ServerHttpResponse response = exchange.getResponse();
            return out(response);
        }
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        return 0;
    }

    private Mono<Void> out(ServerHttpResponse response) {
        JsonObject message = new JsonObject();
        message.addProperty("success", false);
        message.addProperty("code", 28004);
        message.addProperty("data", "鑑權失敗");
        byte[] bits = message.toString().getBytes(StandardCharsets.UTF_8);
        DataBuffer buffer = response.bufferFactory().wrap(bits);
        //response.setStatusCode(HttpStatus.UNAUTHORIZED);
        //指定編碼,否則在瀏覽器中會中文亂碼
        response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
        return response.writeWith(Mono.just(buffer));
    }
}

自定義異常處理

服務閘道器呼叫服務時可能會有一些異常或服務不可用,它返回錯誤資訊不友好,需要我們覆蓋處理。

ErrorHandlerConfig
@Configuration
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfig {

    private final ServerProperties serverProperties;

    private final ApplicationContext applicationContext;

    private final ResourceProperties resourceProperties;

    private final List<ViewResolver> viewResolvers;

    private final ServerCodecConfigurer serverCodecConfigurer;

    public ErrorHandlerConfig(ServerProperties serverProperties,
                                     ResourceProperties resourceProperties,
                                     ObjectProvider<List<ViewResolver>> viewResolversProvider,
                                        ServerCodecConfigurer serverCodecConfigurer,
                                     ApplicationContext applicationContext) {
        this.serverProperties = serverProperties;
        this.applicationContext = applicationContext;
        this.resourceProperties = resourceProperties;
        this.viewResolvers = viewResolversProvider.getIfAvailable(Collections::emptyList);
        this.serverCodecConfigurer = serverCodecConfigurer;
    }

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
        JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
                errorAttributes,
                this.resourceProperties,
                this.serverProperties.getError(),
                this.applicationContext);
        exceptionHandler.setViewResolvers(this.viewResolvers);
        exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
        exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
        return exceptionHandler;
    }
}

參考資料

相關文章