內容簡介: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 進行請求轉發。
在請求到達真正的微服務之前,我們可以在這裏做一些預處理,比如:來源合法性檢測,權限校驗,反爬蟲之類…
因爲業務需要,我們的服務的請求參數都是經過加密的。
之前是在各個微服務的攔截器裏對來解密驗證的,現在既然有了網關,自然而然想把這一步驟放到網關層來統一解決。
如果是使用普通的 Web 編程中(比如用 Zuul),這本就是一個 pre filter 的事兒,把之前 Interceptor 中代碼搬過來稍微改改就 OK 了。
不過因爲使用的 SCG,它基於 Spring 5 的 WebFlux,即 Reactor 編程,要讀取 Request Body 中的請求參數就沒那麼容易了。
本篇內容涉及 WebFlux 的響應式編程及 SCG 自定義全局過濾器,如果對這兩者不瞭解的話,可以先看看相關的內容。
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,理想很豐滿,現實卻很骨感——這兩個辦法都有問題:
-
WebFlux 中不能使用阻塞的操作
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-server-epoll-7
-
subscribe()
只會接收到第一個發出的元素,所以會導致獲取不全的問題(太長的 Body 會被截斷)。這個問題網上有人用AtomicReference<String>
來包裝獲取到字符串,有人用StringBuilder/StringBuffer
以上兩個問題在網上找了半天,也沒找到一個靠譜的解決辦法,都是人云亦云。特別是第二個所謂的“解決辦法”,大家無非就在是不遺餘力的在展示 DataBuffer
轉 String
的“回字的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 了。既然官方已經提供了“葫蘆”,那麼我們就畫個“瓢”吧。
如果瞭解的 GatewayFilterFactory
和 GatewayFilter
的關係的話,不用我說你就知道該怎麼辦了。不知道也沒關係,我們把 ModifyRequestBodyGatewayFilterFactory
中紅框部分 copy 出來,粘貼到我們之前創建的 ValidateFilter#filter
中
我們稍作修改,即可實現讀取並修改 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 的持久化動態路由。