Spring Cloud Gateway(讀取、修改 Request Body)的操作
Spring Cloud Gateway(以下簡(jiǎn)稱 SCG)做為網(wǎng)關(guān)服務(wù),是其他各服務(wù)對(duì)外中轉(zhuǎn)站,通過(guò) SCG 進(jìn)行請(qǐng)求轉(zhuǎn)發(fā)。
在請(qǐng)求到達(dá)真正的微服務(wù)之前,我們可以在這里做一些預(yù)處理,比如:來(lái)源合法性檢測(cè),權(quán)限校驗(yàn),反爬蟲之類…
因?yàn)闃I(yè)務(wù)需要,我們的服務(wù)的請(qǐng)求參數(shù)都是經(jīng)過(guò)加密的。
之前是在各個(gè)微服務(wù)的攔截器里對(duì)來(lái)解密驗(yàn)證的,現(xiàn)在既然有了網(wǎng)關(guān),自然而然想把這一步驟放到網(wǎng)關(guān)層來(lái)統(tǒng)一解決。

如果是使用普通的 Web 編程中(比如用 Zuul),這本就是一個(gè) pre filter 的事兒,把之前 Interceptor 中代碼搬過(guò)來(lái)稍微改改就 OK 了。
不過(guò)因?yàn)槭褂玫?SCG,它基于 Spring 5 的 WebFlux,即 Reactor 編程,要讀取 Request Body 中的請(qǐng)求參數(shù)就沒那么容易了。
本篇內(nèi)容涉及 WebFlux 的響應(yīng)式編程及 SCG 自定義全局過(guò)濾器,如果對(duì)這兩者不了解的話,可以先看看相關(guān)的內(nèi)容。
兩個(gè)大坑
我們先建一個(gè) Filter 來(lái)看看
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 都易如反掌,如果你需要校驗(yàn)的數(shù)據(jù)在這三者之中的話,就沒必要往下看了。
說(shuō)回 Body,這里是一個(gè)Flux<DataBuffer>,即一個(gè)包含 0-N 個(gè)DataBuffer類型元素的異步序列。
首先不考慮 Request Body 只能讀取一次問(wèn)題(這個(gè)問(wèn)題可以用緩存解決),我們先來(lái)把這個(gè) Flux 轉(zhuǎn)化成我們可以處理的字符串,第一反應(yīng)想到的有兩個(gè)辦法:
block() 異步變同步
subscribe() 訂閱并觸發(fā)序列
BUT,理想很豐滿,現(xiàn)實(shí)卻很骨感——這兩個(gè)辦法都有問(wèn)題:
WebFlux 中不能使用阻塞的操作
java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-server-epoll-7
subscribe() 只會(huì)接收到第一個(gè)發(fā)出的元素,所以會(huì)導(dǎo)致獲取不全的問(wèn)題(太長(zhǎng)的 Body 會(huì)被截?cái)啵?。這個(gè)問(wèn)題網(wǎng)上有人用 AtomicReference<String> 來(lái)包裝獲取到字符串,有人用 StringBuilder/StringBuffer
以上兩個(gè)問(wèn)題在網(wǎng)上找了半天,也沒找到一個(gè)靠譜的解決辦法,都是人云亦云。特別是第二個(gè)問(wèn)題的所謂的“解決辦法”,大家無(wú)非就在是不遺余力的在展示 DataBuffer 轉(zhuǎn) String 的 N 種寫法,而沒有從根本上解決被截?cái)嗟膯?wèn)題。
正確姿勢(shì)
2019.08.26 更新:
評(píng)論里有網(wǎng)友提醒到 Spring Cloud Gateway 2.1.2 下 DefaultServerRequest、CachedBodyOutputMessage 類的訪問(wèn)權(quán)限已經(jīng)改了。這一塊我看了一下,源碼確實(shí)改動(dòng)了一些,不過(guò) DefaultServerRequest 這個(gè)類已經(jīng)不需要了,而 CachedBodyOutputMessage 類我們可以模(chao)仿(xi)它的實(shí)現(xiàn)。
其實(shí)這里的實(shí)現(xiàn)不管再怎么變,我們只要死盯著 ModifyRequestBodyGatewayFilterFactory 就行了。即使以后這里邊的相關(guān)類的訪問(wèn)權(quán)限都改成 Default 了,我們也不用一個(gè)個(gè)去抄一遍,只要在org.springframework.cloud.gateway.filter.factory.rewrite 這個(gè) package 下寫我們自己的類就好了。

———– 分割線 ———-
最終找到解決方案還是通過(guò)研讀 SCG 的源碼。
本文使用的版本:
Spring Cloud: Greenwich.RC2
Spring Boot: 2.1.1.RELEASE
在 org.springframework.cloud.gateway.filter.factory.rewrite 包下有個(gè) ModifyRequestBodyGatewayFilterFactory,顧名思義,這就是修改 Request Body 的過(guò)濾器工廠類。
但是這個(gè)類我們無(wú)法直接使用,因?yàn)橐玫脑掃@個(gè) FilterFactory 只能用 Fluent API 的方式配置,而無(wú)法在配置文件中使用,類似于這樣
.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)
)
我更喜歡用配置文件來(lái)配置路由,所以這種方式并不是我的菜。
這時(shí)候我就需要自己弄一個(gè) GlobalFilter 了。既然官方已經(jīng)提供了“葫蘆”,那么我們就畫個(gè)“瓢”吧。
如果了解的 GatewayFilterFactory 和 GatewayFilter 的關(guān)系的話,不用我說(shuō)你就知道該怎么辦了。不知道也沒關(guān)系,我們把 ModifyRequestBodyGatewayFilterFactory 中紅框部分 copy 出來(lái),粘貼到我們之前創(chuàng)建的 ValidateFilter#filter 中

我們稍作修改,即可實(shí)現(xiàn)讀取并修改 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 后具體要做什么,也就上邊代碼中的TODO部分,就由你自己來(lái)發(fā)揮吧~ 別玩壞就好
建議大家可以多關(guān)注關(guān)注 SCG 的源碼,說(shuō)不定什么時(shí)候就會(huì)多出一些有用的 Filter 或 FilterFactory。
另外,目前 ModifyRequestBodyGatewayFilterFactory 上的 Javadoc 有這么一句話:
This filter is BETA and may be subject to change in a future release.
所以大家要保持關(guān)注呀~
以上這篇Spring Cloud Gateway(讀取、修改 Request Body)的操作就是小編分享給大家的全部?jī)?nèi)容了,希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
java開源項(xiàng)目jeecgboot的超詳細(xì)解析
JeecgBoot是一款基于BPM的低代碼平臺(tái),下面這篇文章主要給大家介紹了關(guān)于java開源項(xiàng)目jeecgboot的相關(guān)資料,文中通過(guò)圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-10-10
解決spring data jpa 批量保存更新的問(wèn)題
這篇文章主要介紹了解決spring data jpa 批量保存更新的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-06-06
SpringBoot配置mybatis駝峰命名規(guī)則自動(dòng)轉(zhuǎn)換的實(shí)現(xiàn)
這篇文章主要介紹了SpringBoot配置mybatis駝峰命名規(guī)則自動(dòng)轉(zhuǎn)換的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-09-09
java實(shí)現(xiàn)登錄之后抓取數(shù)據(jù)
這篇文章給大家分享了用JAVA實(shí)現(xiàn)在登陸以后抓取網(wǎng)站的數(shù)據(jù)的相關(guān)知識(shí),有興趣的朋友可以測(cè)試參考下。2018-07-07
SpringBoot開發(fā)實(shí)戰(zhàn)之自動(dòng)配置
SpringBoot的核心就是自動(dòng)配置,自動(dòng)配置又是基于條件判斷來(lái)配置Bean,下面這篇文章主要給大家介紹了關(guān)于SpringBoot開發(fā)實(shí)戰(zhàn)之自動(dòng)配置的相關(guān)資料,需要的朋友可以參考下2021-08-08

