Spring Cloud Gateway(讀取、修改 Request Body

內容簡介:Spring Cloud Gateway(以下簡稱 SCG)做爲網關服務,是其他各服務對外中轉站,通過 SCG 進行請求轉發。在請求到達真正的微服務之前,我們可以在這裏做一些預處理,比如:來源合法性檢測,權限校驗,反爬蟲之類…因爲業務需要,我們的服務的請求參數都是經過加密的。

本文轉載自:https://windmt.com/2019/01/16/spring-cloud-18-spring-cloud-gateway-read-and-modify-request-body/,本站轉載出於傳遞更多信息之目的,版權歸原作者或者來源機構所有。

Spring Cloud Gateway(以下簡稱 SCG)做爲網關服務,是其他各服務對外中轉站,通過 SCG 進行請求轉發。

在請求到達真正的微服務之前,我們可以在這裏做一些預處理,比如:來源合法性檢測,權限校驗,反爬蟲之類…

因爲業務需要,我們的服務的請求參數都是經過加密的。

之前是在各個微服務的攔截器裏對來解密驗證的,現在既然有了網關,自然而然想把這一步驟放到網關層來統一解決。

Spring Cloud(十八):Spring Cloud Gateway(讀取、修改 Request Body)

如果是使用普通的 Web 編程中(比如用 Zuul),這本就是一個 pre filter 的事兒,把之前 Interceptor 中代碼搬過來稍微改改就 OK 了。

不過因爲使用的 SCG,它基於 Spring 5 的 WebFlux,即 Reactor 編程,要讀取 Request Body 中的請求參數就沒那麼容易了。

本篇內容涉及 WebFlux 的響應式編程及 SCG 自定義全局過濾器,如果對這兩者不瞭解的話,可以先看看相關的內容。

Reactive

Spring Cloud(十四):Spring Cloud Gateway(過濾器)

兩個大坑

我們先建一個 Filter 來看看

public class ValidateFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        MultiValueMap<String, HttpCookie> cookies = request.getCookies();
        MultiValueMap<String, String> queryParams = request.getQueryParams();
        Flux<DataBuffer> body = request.getBody();
        return null;
    }

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

從上邊的返回值可以看出,如果是取 Header、Cookie、Query Params 都易如反掌,如果你需要校驗的數據在這三者之中的話,就沒必要往下看了。

說回 Body,這裏是一個 Flux<DataBuffer> ,即一個包含 0-N 個 DataBuffer 類型元素的異步序列。

 

首先不考慮 Request Body 只能讀取一次問題(這個問題可以用緩存解決),我們先來把這個 Flux 轉化成我們可以處理的字符串,第一反應想到的有兩個辦法:

block()
subscribe()

BUT,理想很豐滿,現實卻很骨感——這兩個辦法都有問題:

  1. WebFlux 中不能使用阻塞的操作

    java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-server-epoll-7
  2. subscribe() 只會接收到第一個發出的元素,所以會導致獲取不全的問題(太長的 Body 會被截斷)。這個問題網上有人用 AtomicReference<String> 來包裝獲取到字符串,有人用 StringBuilder/StringBuffer

以上兩個問題在網上找了半天,也沒找到一個靠譜的解決辦法,都是人云亦云。特別是第二個所謂的“解決辦法”,大家無非就在是不遺餘力的在展示 DataBufferString 的“回字的N種寫法”。

正確姿勢

最終找到解決方案還是通過研讀 SCG 的源碼。

本文使用的版本:

  • Spring Cloud: Greenwich.RC2
  • Spring Boot: 2.1.1.RELEASE

org.springframework.cloud.gateway.filter.factory.rewrite 包下有個 ModifyRequestBodyGatewayFilterFactory ,顧名思義,這就是修改 Request Body 的過濾器工廠類。

但是這個類我們無法直接使用,因爲要用的話這個 FilterFactory 只能用 Fluent API 的方式配置,而無法在配置文件中使用,類似於這樣

.route("rewrite_request_upper", r -> r.host("*.rewriterequestupper.org")
	.filters(f -> f.prefixPath("/httpbin")
			.addResponseHeader("X-TestHeader", "rewrite_request_upper")
			.modifyRequestBody(String.class, String.class,
					(exchange, s) -> {
						return Mono.just(s.toUpperCase()+s.toUpperCase());
					})
	).uri(uri)
)

我更喜歡用配置文件來配置路由,所以這種方式並不是我的菜。

這時候我就需要自己弄一個 GlobalFilter 了。既然官方已經提供了“葫蘆”,那麼我們就畫個“瓢”吧。

如果瞭解的 GatewayFilterFactoryGatewayFilter 的關係的話,不用我說你就知道該怎麼辦了。不知道也沒關係,我們把 ModifyRequestBodyGatewayFilterFactory 中紅框部分 copy 出來,粘貼到我們之前創建的 ValidateFilter#filter

Spring Cloud(十八):Spring Cloud Gateway(讀取、修改 Request Body)

我們稍作修改,即可實現讀取並修改 Request Body 的功能了(核心部分見上圖黃色箭頭處)

/**
 * @author yibo
 */
public class ValidateFilter implements GlobalFilter, Ordered {


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerRequest serverRequest = new DefaultServerRequest(exchange);
        // mediaType
        MediaType mediaType = exchange.getRequest().getHeaders().getContentType();
        // read & modify body
        Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
                .flatMap(body -> {
                    if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)) {

                        // origin body map
                        Map<String, Object> bodyMap = decodeBody(body);

                        // TODO decrypt & auth

                        // new body map
                        Map<String, Object> newBodyMap = new HashMap<>();

                        return Mono.just(encodeBody(newBodyMap));
                    }
                    return Mono.empty();
                });

        BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
        HttpHeaders headers = new HttpHeaders();
        headers.putAll(exchange.getRequest().getHeaders());

        // the new content type will be computed by bodyInserter
        // and then set in the request decorator
        headers.remove(HttpHeaders.CONTENT_LENGTH);

        CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
        return bodyInserter.insert(outputMessage,  new BodyInserterContext())
                .then(Mono.defer(() -> {
                    ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
                            exchange.getRequest()) {
                        @Override
                        public HttpHeaders getHeaders() {
                            long contentLength = headers.getContentLength();
                            HttpHeaders httpHeaders = new HttpHeaders();
                            httpHeaders.putAll(super.getHeaders());
                            if (contentLength > 0) {
                                httpHeaders.setContentLength(contentLength);
                            } else {
                                httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
                            }
                            return httpHeaders;
                        }

                        @Override
                        public Flux<DataBuffer> getBody() {
                            return outputMessage.getBody();
                        }
                    };
                    return chain.filter(exchange.mutate().request(decorator).build());
                }));
    }

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

    private Map<String, Object> decodeBody(String body) {
        return Arrays.stream(body.split("&"))
                .map(s -> s.split("="))
                .collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
    }

    private String encodeBody(Map<String, Object> map) {
        return map.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&"));
    }
}

至於拿到 Body 後具體要做什麼,就由你自己來發揮吧~ 別玩壞就好

建議大家可以多關注關注 SCG 的源碼,說不定什麼時候就會多出一些有用的 Filter 或 FilterFactory。

另外,目前 ModifyRequestBodyGatewayFilterFactory 上的 Javadoc 有這麼一句話:

This filter is BETA and may be subject to change in a future release.

所以大家要保持關注呀~

另,後續有時間了再來講講 Spring Cloud Gateway 的持久化動態路由。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章