【Spring Cloud Gateway专题】四、Spring Cloud Gateway中RequestBody只能获取一次的问题解决方案
1、前言在网关应用中,如果我想要记录所有请求的参数,然后将请求流转到下游,就会遇到读取RequestBody的问题。无论在Spring5的webflux编程或者普通web编程中,只能从request中获取body一次,后面无法再获取,这个问题怎么解决呢?网上博客有多种处理办法,对不同的spring cloud gateway版本不一定有用。本文着重说明下版本环境:spring cloud g...
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中寻找问题以及对应的处理办法。
更多推荐
所有评论(0)