Spring Cloud Gateway 實現簡單自定義過濾器

ghimi發表於2024-08-13

背景

Spring Cloud Gateway 是 Spring Cloud 退出的第二代閘道器框架,我們可以用它來實現 反向代理,路由轉發,許可權校驗等功能,這裡介紹一個它的基礎功能,透過 Filter 機制實現一個簡單的 HTTP 介面處理。

從總體上來看 Spring Cloud Gateway 提供的過濾器可以分為兩類,一種是對全域性流量都生效的全域性過濾器(Global Filter),另外一種是針對特定路徑生效的自定義過濾器;透過全域性過濾器我們可以實現一些全域性請求的處理操作,如請求效能監控,請求日誌記錄,請求快取等;透過自定義過濾器我們可以針對特定的請求實現一些特定的請求處理,如新增請求頭,新增引數,新增響應頭等功能等。

Spring Cloud Gateway 的過濾器處理是經典的 23 種設計模式中的責任鏈模式(想要學習設計模式的童鞋可以去翻原始碼)。當一個請求滿足路由規則時,負責過濾網路請求的處理器會將全部 GlobalFilter 和特定路由實現的 Filter 新增到過濾器鏈中,多個 Filter 的排序透過 Ordered 介面實現,使用者可以透過手動實現 Ordered 介面中的 getOrder 方法來指定實際執行的順序, getOrder 返回的數字越小,表示執行的優先順序越高,即 Filter 會被越早呼叫。

img

當一個請求進入到應用中後,會在請求實際處理前(pre),和結果處理後(post)經過每個 Filter 各一次,我們可以透過 Filter 實現對請求的記錄,攔截,日誌列印,執行時間記錄等功能。

Spring Cloud Gateway 預設已經提供了一些 Filter 供開發者使用,GatewayMetricsFilter 可以用來記錄一些指標,透過 spring-boot-starter-actuator 庫可以講這些指標透傳出去,可以實現請求內容和響應結果的分析。也可以實現一個告警系統,在系統出現異常請求時及時發起告警。LocalResponseCache 則可以將請求的響應結果快取下來,這樣對於一些重複的請求就可以減少對代理系統的負載,從而提升整體系統的效能。路由轉發 Filter ForwardRoutingFilter則可以實現請求的轉發,透過修改原始請求路徑後,將新的請求轉發到目標節點上。NettyRoutingFilter 可以透過 Netty 的 HttpClient 生成下游的請求,然後將響應轉發到後續的 Filter 中。除此之外還有一些其他的過濾器,可以透過這些過濾器靈活組合實現想要的功能。

除了 GlobalFilter 之外,還有一類 Filter 的職責沒有那麼廣,他們是專門針對特定的請求實現一些特定的功能,這些 Filter 透過配置的規則作用到目標的請求上,完成一些增強功能。如 AddRequestHeader 可以為進入的請求新增額外的 header ,可以供後面的代理伺服器鑑權認證使用;AddRequestParameter 可以新增額外的請求引數,AddResponseHeader 用於新增額外的響應頭...諸如此類。

首先用 Spring Starter 建立一個 Spring Cloud Gateway 應用,然後新增配置,為指定 url 增加一個過濾器:

spring:
  cloud:
    gateway: #閘道器路由配置
      routes:
        - id: user-grpc #路由 id,沒有固定規則,但唯一,建議與服務名對應
          uri: https://[::1]:443 #匹配後提供服務的路由地址
          predicates:
            #以下是斷言條件,必選全部符合條件
            - Path=/**               #斷言,路徑匹配 注意:Path 中 P 為大寫
            - Header=Content-Type,application/json # 斷言,請求頭匹配:
          filters:
            - CustomFilter #自定義過濾器名稱
@Slf4j
@Component
public class CustomFilterFactory extends AbstractGatewayFilterFactory<Object> {

    public CustomFilterFactory(ResourceLoader resourceLoader) {
        super(Object.class);
    }

    @Override
    public GatewayFilter apply(Object config) {
        GatewayFilter filter = new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                CustomResponseDecorator modifiedResponse = new CustomResponseDecorator(exchange);
                ServerWebExchange newExchange = exchange.mutate().response(modifiedResponse).build();
                // 跳過後續自動路由到下游代理
                ServerWebExchangeUtils.setAlreadyRouted(newExchange);
                return modifiedResponse.writeWith(exchange.getRequest().getBody()).then(chain.filter(newExchange));
            }

            @Override
            public String toString() {
                return filterToStringCreator(CustomFilterFactory.this).toString();
            }
        };

        // 設定 Filter 優先順序在寫入響應前
        int order = NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
        return new OrderedGatewayFilter(filter, order);
    }

    @Override
    public String name() {
        return "CustomFilter";
    }

    @Slf4j
    static class CustomResponseDecorator extends ServerHttpResponseDecorator {

        /**
         * 返回成功的訊息
         */
        public static final HttpMsg SUCCESS_MESSAGE;

        private final ServerWebExchange exchange;

        static {
            SUCCESS_MESSAGE = new HttpMsg();
            SUCCESS_MESSAGE.setCode(200);
            SUCCESS_MESSAGE.setMessage("success");
        }

        public CustomResponseDecorator(ServerWebExchange exchange) {
            super(exchange.getResponse());
            this.exchange = exchange;
        }

        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> bodyPub) {
            ServerHttpRequest request = exchange.getRequest();
            HttpMethod method = request.getMethod();
            String path = request.getPath().value();
            long contentLength = request.getHeaders().getContentLength();
            String clientIp = Optional.ofNullable(request.getRemoteAddress()).map(InetSocketAddress::getAddress)
                    .map(InetAddress::toString).orElse(null);
            Mono<String> reqBodyMono;
            // 當請求包含 body 時,解析 body 內容為 string
            if (contentLength > 0) {
                reqBodyMono = DataBufferUtils.join(bodyPub).mapNotNull(buffer -> {
                    String str = buffer.toString(StandardCharsets.UTF_8);
                    // 解析完成後,釋放釋放 buffer
                    DataBufferUtils.release(buffer);
                    return str;
                });
            } else {
                // 不包含 body 時,直接返回空字串
                reqBodyMono = Mono.just("");
            }
            Mono<DataBuffer> res = reqBodyMono.handle((body, sink) -> {
                // 記錄請求資訊
                log.debug("receive request client ip:{} path: {}({}),body: {}", clientIp, path, method, body);
                sink.next(SUCCESS_MESSAGE);
            }).map(resMsg -> {
                getHeaders().setContentType(MediaType.APPLICATION_JSON);
                return JSONUtil.toJsonStr(resMsg);
            }).map(str -> bufferFactory().wrap(ByteBuffer.wrap(str.getBytes(StandardCharsets.UTF_8))));
            return super.writeWith(res);
        }
    }
}

參考資料

  • Global Filters
  • Configuring Route Predicate Factories and Gateway Filter Factories

相關文章