1、前言
在網關應用中,如果我想要記錄所有請求的參數,然後將請求流轉到下游,就會遇到讀取RequestBody的問題。無論在Spring5的webflux編程或者普通web編程中,只能從request中獲取body一次,後面無法再獲取,這個問題怎麼解決呢?
網上博客有多種處理辦法,對不同的spring cloud gateway版本不一定有用。本文着重說明下版本環境:
spring cloud gateway 2.2.1.RELEASE版,並且在spring cloud gateway 2.2.2.RELEASE也驗證通過。spring cloud版本爲Hoxton.SR1。
2、普通讀取requestbody的示例
controller類代碼如下:
package cn.iocoder.springcloud.labx08.gatewaydemo.controller;
import cn.hutool.json.JSONUtil;
import cn.iocoder.springcloud.labx08.gatewaydemo.domain.Blog;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("blog")
public class DemoController {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 測試 @Value 註解的屬性
*/
@PostMapping("/blog01")
public Map<String, Object> blog01(@RequestBody Blog blog,HttpServletRequest request) {
Map<String, Object> result = new HashMap<>();
result.put("params" , request.getParameter("token"));
result.put("bodydata" , JSONUtil.toJsonStr(blog));
return result;
}
}
Blog代碼如下:
@Data
public class Blog{
private String title;
private String content;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date publishDate;
}
請求示例
3、使用spring cloud gateway代理
yaml配置
server:
port: 8888
spring:
application:
name: gateway-application
cloud:
# Spring Cloud Gateway 配置項,對應 GatewayProperties 類
gateway:
# 路由配置項,對應 RouteDefinition 數組
routes:
- id: csdn1 # 路由的編號
uri: http://localhost:9090
predicates: # 斷言,作爲路由的匹配條件,對應 RouteDefinition 數組
- Path=/csdnblog/**
filters:
- StripPrefix=1
gateway代碼,該代碼提供了兩種讀取body參數的方法,分別爲APPLICATION_JSON,APPLICATION_FORM_URLENCODED。處理辦法是在向下遊請求時,構造新的ServerWebExchange,並將參數傳遞進去。如果依然使用原ServerWebExchange向下遊傳遞,下游由於請求參數不匹配,直接報404錯誤。
package cn.iocoder.springcloud.labx08.gatewaydemo.filter;
import cn.iocoder.springcloud.labx08.gatewaydemo.context.GatewayContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
/**
* Gateway Context Filter
* @author chenggang
* @date 2019/01/29
*/
@Slf4j
@Component
public class GatewayContextFilter implements GlobalFilter, Ordered {
/**
* default HttpMessageReader
*/
private static final List<HttpMessageReader<?>> MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
GatewayContext gatewayContext = new GatewayContext();
HttpHeaders headers = request.getHeaders();
gatewayContext.setRequestHeaders(headers);
gatewayContext.getAllRequestData().addAll(request.getQueryParams());
/*
* save gateway context into exchange
*/
exchange.getAttributes().put(GatewayContext.CACHE_GATEWAY_CONTEXT,gatewayContext);
MediaType contentType = headers.getContentType();
if(headers.getContentLength()>0){
if(MediaType.APPLICATION_JSON.equals(contentType) || MediaType.APPLICATION_JSON_UTF8.equals(contentType)){
return readBody(exchange, chain,gatewayContext);
}
if(MediaType.APPLICATION_FORM_URLENCODED.equals(contentType)){
return readFormData(exchange, chain,gatewayContext);
}
}
log.debug("[GatewayContext]ContentType:{},Gateway context is set with {}",contentType, gatewayContext);
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -2;
}
/**
* ReadFormData
* @param exchange
* @param chain
* @return
*/
private Mono<Void> readFormData(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext){
HttpHeaders headers = exchange.getRequest().getHeaders();
return exchange.getFormData()
.doOnNext(multiValueMap -> {
gatewayContext.setFormData(multiValueMap);
gatewayContext.getAllRequestData().addAll(multiValueMap);
log.debug("[GatewayContext]Read FormData Success");
})
.then(Mono.defer(() -> {
Charset charset = headers.getContentType().getCharset();
charset = charset == null? StandardCharsets.UTF_8:charset;
String charsetName = charset.name();
MultiValueMap<String, String> formData = gatewayContext.getFormData();
/*
* formData is empty just return
*/
if(null == formData || formData.isEmpty()){
return chain.filter(exchange);
}
StringBuilder formDataBodyBuilder = new StringBuilder();
String entryKey;
List<String> entryValue;
try {
/*
* repackage form data
*/
for (Map.Entry<String, List<String>> entry : formData.entrySet()) {
entryKey = entry.getKey();
entryValue = entry.getValue();
if (entryValue.size() > 1) {
for(String value : entryValue){
formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(value, charsetName)).append("&");
}
} else {
formDataBodyBuilder.append(entryKey).append("=").append(URLEncoder.encode(entryValue.get(0), charsetName)).append("&");
}
}
}catch (UnsupportedEncodingException e){}
/*
* substring with the last char '&'
*/
String formDataBodyString = "";
if(formDataBodyBuilder.length()>0){
formDataBodyString = formDataBodyBuilder.substring(0, formDataBodyBuilder.length() - 1);
}
/*
* get data bytes
*/
byte[] bodyBytes = formDataBodyString.getBytes(charset);
int contentLength = bodyBytes.length;
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(exchange.getRequest().getHeaders());
httpHeaders.remove(HttpHeaders.CONTENT_LENGTH);
/*
* in case of content-length not matched
*/
httpHeaders.setContentLength(contentLength);
/*
* use BodyInserter to InsertFormData Body
*/
BodyInserter<String, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromObject(formDataBodyString);
CachedBodyOutputMessage cachedBodyOutputMessage = new CachedBodyOutputMessage(exchange, httpHeaders);
log.debug("[GatewayContext]Rewrite Form Data :{}",formDataBodyString);
return bodyInserter.insert(cachedBodyOutputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return cachedBodyOutputMessage.getBody();
}
};
return chain.filter(exchange.mutate().request(decorator).build());
}));
}));
}
/**
* ReadJsonBody
* @param exchange
* @param chain
* @return
*/
private Mono<Void> readBody(ServerWebExchange exchange, GatewayFilterChain chain, GatewayContext gatewayContext){
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
/*
* read the body Flux<DataBuffer>, and release the buffer
* //TODO when SpringCloudGateway Version Release To G.SR2,this can be update with the new version's feature
* see PR https://github.com/spring-cloud/spring-cloud-gateway/pull/1095
*/
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
DataBufferUtils.retain(buffer);
return Mono.just(buffer);
});
log.debug("[GatewayContext]Read JsonBody Success");
/*
* repackage ServerHttpRequest
*/
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
ServerWebExchange mutatedExchange = exchange.mutate().request(mutatedRequest).build();
return ServerRequest.create(mutatedExchange, MESSAGE_READERS)
.bodyToMono(String.class)
.doOnNext(objectValue -> {
gatewayContext.setRequestBody(objectValue);
}).then(chain.filter(mutatedExchange/*exchange*/));
});
}
}
源碼:https://github.com/muziye2013/SpringBoot-Labs 請參考labx-08/labx-08-ex-gateway-demo02,labx-08/labx-08-ex-gateway-demo02-controller兩個模塊的代碼。
主要參考的文章及代碼:
關於Spring-webflux編程中body只能獲取一次的問題解決方案:
SpringCloud Gateway 記錄緩存請求Body和Form表單
Spring Cloud Gateway-ServerWebExchange核心方法與請求或者響應內容的修改 注意該文的處理方法只適用於其對應版本
Spring Cloud Gateway 之 Filter,對於filter的講解很詳細。
除此之外,還要學會從spring cloud gateway的issue中尋找問題以及對應的處理辦法。