Gateway 簡介

呆煒發表於2022-02-23

概述

微服務可能分佈在不同的主機上,這樣有許多缺點:前端需要硬編碼呼叫不同地址的微服務很麻煩;存在跨域訪問的問題;微服務地址直接暴露是不安全的。還有所以需要為前端提供一個統一的訪問入口。Gateway 就是用於解決以上問題的框架。Gateway 本身是一個微服務,要註冊到 Eureka 中。

入門案例

  1. maven 依賴:
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
</dependencies>
  1. 啟動類新增 @EnableDiscoveryClient 註解
  2. 配置檔案:基本的 Eureka 配置 + 路由配置
server:
  port: 10010
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: user-service-route # 路由的標識 ID,任意名稱都可以
          uri: http://127.0.0.1:9092 # 路由的轉發地址
          predicates:
            - Path=/user/** # 路由的對映範圍
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true
  1. 是否配置成功?

啟動例項後能在 Eureka 註冊中心看到註冊,http://localhost:10010/user/** 的請求會被轉發到 http://localhost:9092/user/**

動態路由

入門案例中使用的是靜態IP 地址,下面是用服務名稱代替具體 IP,這樣避免直接使用 IP,並可以新增負載均衡。

只需要將配置中的 uri 更換為「loadbalance 協議」

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: user-service-route # 路由的標識 ID,任意名稱都可以
          uri: lb://USER-SERVICE # 路由的轉發地址
          predicates:
            - Path=/user/** # 路由的對映範圍

路由規則

  • 新增字首:http://localhost:10010/**-->lb://USER-SERVICE/user/**
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: user-service-route # 路由的標識 ID,任意名稱都可以
          uri: lb://USER-SERVICE # 路由的轉發地址
          predicates:
            - Path=/** # 路由的對映範圍
          filters:
            - PrefixPath=/user
  • 去除字首:http://localhost:10010/api/api/user/**-->lb://USER-SERVICE/user/**
spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      routes:
        - id: user-service-route # 路由的標識 ID,任意名稱都可以
          uri: lb://USER-SERVICE # 路由的轉發地址
          predicates:
            - Path=/api/api/user/** # 路由的對映範圍
          filters:
            - StripPrefix=2 # 去除兩層字首

過濾器

前面修改請求其實都是通過 Gateway 的過濾器實現的,上面用到的過濾器為 StripPrefixGatewayFilterFactory 產生的過濾器。Gateway 提供了幾十種過濾器,過濾器可以過濾指定規則的請求,也可以過濾所有請求。

spring:
  cloud:
    gateway:
      default-filters:
        - AddResponseHeader=this-is-a-header, Have-Fun

這個過濾器可以為所有請求新增一個響應頭,this-is-a-header: Have-Fun

路由器分為區域性路由和全域性路由,區域性路由分為兩種(上面展示的兩種):

  1. spring.cloud.gateway.routes.- id.filters
  2. spring.cloud.gateway.default-filters (功能等價於全域性過濾器)

全域性過濾器:不在配置檔案中配置,需要實現 GlobalFilter 介面。

生命週期

過濾器類似於攔截器,但是隻在微服務處理前後運作,請求轉發不會觸發過濾器。

自定義區域性過濾器

自定義過濾器需要實現 AbstractGatewayFilterFactory 抽象類。該類使用內部類接受配置檔案中的引數,需要給這個內部類的提供 Getter/Setter 方法。

package com.example.gateway.filter;

import java.util.Arrays;
import java.util.List;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;

@Component
public class MyMyGatewayFilterFactory extends AbstractGatewayFilterFactory<MyMyGatewayFilterFactory.Config> {
    public MyMyGatewayFilterFactory() {
        super(Config.class);
    }

    // 這個方法指定配置檔案注入 Config 的哪一個欄位
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("param");
    }

    // 由於接受配置檔案引數
    public static class Config {
        private String param;

        public String getParam() {
            return param;
        }

        public void setParam(String param) {
            this.param = param;
        }

    }

    // 此為過濾器邏輯,返回值是一個過濾方法,可以使用 Lambda 表示式。
    // exchange 引數可以獲取請求內容。chain 用於執行請求。
    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            // 前置處理
            ServerHttpRequest request = exchange.getRequest();
            if (request.getQueryParams().containsKey(config.param)) {
                request.getQueryParams().get(config.param)
                        .forEach(o -> System.out.printf("區域性過濾器:%s = %s \n", config.param, o));
            }
            return chain.filter(exchange); // 執行請求
        };
    }
}

這裡我們使用配置檔案將 "name" 注入到 Config.param 欄位上。注意配置中使用該過濾器類的字首來注入。比如 MyMyGatewayFilterFactory 的字首為 MyMy。過濾器類的命名格式是固定的,為 XxxxGatewayFilterFactory,需要使用 @Component 注入到 Spring 容器中,泛型使用 Config 內部類。

spring:
  cloud:
    gateway:
      routes:
        - id: user-service-route # 路由的標識 ID,任意名稱都可以
          uri: lb://USER-SERVICE # 路由的轉發地址
          predicates:
            - Path=/api/api/user/** # 路由的對映範圍
          filters:
            - StripPrefix=2 # 去除兩層字首
            - MyMy=name
      default-filters:
        - AddResponseHeader=this-is-a-header, Have-Fun

自定義全域性過濾器

實現 GlobalFilter, Ordered 介面並注入到 Spring 容器中。

package com.example.gateway.filter;

import org.apache.commons.lang.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;

import reactor.core.publisher.Mono;

@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {

    // 設定執行的優先順序,數字越小優先順序越高
    @Override
    public int getOrder() {
        return 1;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        System.out.println("自定義全域性過濾器 MyGlobalFilter 執行");
        String token = exchange.getRequest().getHeaders().getFirst("token");
        // 如果沒有 token 將響應狀態設定為「未驗證 401」,並直接返回,不執行後面的操作。
        if (StringUtils.isBlank(token)) {
            exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }
}

負載均衡和熔斷配置

Gateway 預設整合了 Ribbon 和 Hystrix。

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000 #服務降級超時時間
      circuitBreaker:
        errorThresholdPercentage: 50 # 觸發熔斷錯誤比例閾值,預設值50%
        sleepWindowInMilliseconds: 10000 # 熔斷後休眠時長,預設值5秒
        requestVolumeThreshold: 10 # 熔斷觸發最小請求次數,預設值是20
ribbon:
  ConnectTimeout: 1000 # 連線超時時長
  ReadTimeout: 2000 # 資料通訊超時時長
  MaxAutoRetries: 0 # 當前伺服器的重試次數
  MaxAutoRetriesNextServer: 0 # 重試多少次服務
  OkToRetryOnAllOperations: false # 是否對所有的請求方式都重試

跨域訪問配置

微服務會面臨跨域訪問(比如埠不同)的問題,預設情況下服務不允許別的域訪問,我們需要在配置中設定「白名單」告知從哪些域來的請求是被允許的。

spring:
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]': # 哪些請求可以跨域
            allowedOrigins: # 跨域白名單
              - "http://docs.spring.io"
            allowedMethods: # 允許的請求方式
              - GET 

一個常見的情況是前後端分離,前端後端部署在不同埠。比如上面配置中 http://docs.spring.io 為前端的域,完成這個配置之後,前端就可以正常使用 Ajax 訪問其他域的所有微服務。

Gateway 高可用

啟動多個 Gateway 例項,實現微服務內部訪問的高可用。但是 Gateway 主要適用於直接暴露給客戶端,僅內部高可用並不是想要的,這時候需要使用 Nginx 代理 Gateway。

Gateway 和 Feign 的區別

Gateway 是作為整體微服務暴露給客戶端的訪問入口,通常用作許可權鑑定和流量控制。Feign 是微服務內部之間互相呼叫的入口。一句話說就是 Gateway、Feign 一外一內。



尊重原創,轉載請標明出處。