Spring Cloud Gateway 如何修改HTTP响应信息
作者:帷幄庸者 发布时间:2021-07-20 13:33:40
Gateway 修改HTTP响应信息
实践Spring Cloud的过程中,使用Gateway作为路由组件,并且基于Gateway实现权限的验证、拦截、过滤,对于下游微服务的响应结果,我们总会有需要修改以统一数据格式,或者修改过滤用户没有权限看到的数据信息,这时候就需要有一个能够修改响应体的Filter。
Spring Cloud Gateway 版本为2.1.0
在当前版本,ModifyRequestBodyGatewayFilterFactory是官方提供的修改响应体的参考类,This filter is BETA and may be subject to change in a future release.,类的注释中说明这个类在以后版本中会改进,实际使用可以参考实现功能,但是性能影响较大,不过没有别的选择还是得选择这个。
官方文档:
实现
最终代码
先贴最终代码
public class ResponseDecryptionGlobalFilter implements GlobalFilter, Ordered {
private static Logger log = LoggerFactory.getLogger(ResponseDecryptionGlobalFilter.class);
@Override
public int getOrder() {
// 控制在NettyWriteResponseFilter后执行
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return processResponse(exchange, chain);
}
private Mono<Void> processResponse(ServerWebExchange exchange, GatewayFilterChain chain) {
// 路由中如果不需要过滤则不进行过滤
if (!BooleanUtils.isTrue()) {
return chain.filter(exchange);
}
ServerHttpResponseDecorator responseDecorator = new ServerHttpResponseDecorator(exchange.getResponse()) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
String originalResponseContentType = exchange.getAttribute(ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add(HttpHeaders.CONTENT_TYPE, originalResponseContentType);
ResponseAdapter responseAdapter = new ResponseAdapter(body, httpHeaders);
DefaultClientResponse clientResponse = new DefaultClientResponse(responseAdapter, ExchangeStrategies.withDefaults());
Mono<String> rawBody = clientResponse.bodyToMono(String.class).map(s -> s);
BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(rawBody, String.class);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, exchange.getResponse().getHeaders());
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
Flux<DataBuffer> messageBody = outputMessage.getBody();
Flux<DataBuffer> flux = messageBody.map(buffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
DataBufferUtils.release(buffer);
// 将响应信息转化为字符串
String responseStr = charBuffer.toString();
if (StringUtils.isNotBlank(responseStr)) {
try {
JSONObject result = JSONObject.parseObject(responseStr);
System.out.println(dataFilter(result));
if (result.containsKey("data")) {
responseStr = dataFilter(result);
} else {
log.error("响应结果序列化异常:{}", responseStr);
}
} catch (JSONException e) {
log.error("响应结果序列化异常:{}", responseStr);
}
}
return getDelegate().bufferFactory().wrap(responseStr.getBytes(StandardCharsets.UTF_8));
});
HttpHeaders headers = getDelegate().getHeaders();
// 修改响应包的大小,不修改会因为包大小不同被浏览器丢掉
flux = flux.doOnNext(data -> headers.setContentLength(data.readableByteCount()));
return getDelegate().writeWith(flux);
}));
}
};
return chain.filter(exchange.mutate().response(responseDecorator).build());
}
/**
* 权限数据过滤
*
* @param result
* @return
*/
private String dataFilter(JSONObject result) {
Object data = result.get("data");
return result.toJSONString();
}
private class ResponseAdapter implements ClientHttpResponse {
private final Flux<DataBuffer> flux;
private final HttpHeaders headers;
@SuppressWarnings("unchecked")
private ResponseAdapter(Publisher<? extends DataBuffer> body, HttpHeaders headers) {
this.headers = headers;
if (body instanceof Flux) {
flux = (Flux) body;
} else {
flux = ((Mono) body).flux();
}
}
@Override
public Flux<DataBuffer> getBody() {
return flux;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
@Override
public HttpStatus getStatusCode() {
return null;
}
@Override
public int getRawStatusCode() {
return 0;
}
@Override
public MultiValueMap<String, ResponseCookie> getCookies() {
return null;
}
}
}
踩过的坑
响应体报文过大: 起初直接读取buffer的响应信息,包小的情况没有问题,但是包大了会抛出json无法转换异常,因为没能读取完整的响应内容,参考ModifyRequestBodyGatewayFilter,等待buffer全部读完再转为数组,然后执行处理。本质原因是底层的Reactor-Netty的数据块读取大小限制导致获取到的DataBuffer实例里面的数据是不完整的。
修改响应信息后,响应的ContentLength会发生变化,忘记修改response中的Content-Length长度,导致前端请求无法获取修改后的响应结果。
flux = flux.doOnNext(data -> headers.setContentLength(data.readableByteCount()));
order值必须小于-1,因为覆盖返回响应体,自定义的GlobalFilter必须比NettyWriteResponseFilter处理完后执行。order越小越早进行处理,越晚处理响应结果。
理解ServerWebExchange
先看ServerWebExchange的注释:
Contract for an HTTP request-response interaction. Provides access to the HTTP request and response and also exposes additional server-side processing related properties and features such as request attributes.
翻译一下大概是:
ServerWebExchange是一个**HTTP请求-响应交互的契约。**提供对HTTP请求和响应的访问,并公开额外的服务器端处理相关属性和特性,如请求属性。
ServerWebExchange有点像Context的角色,我把它理解为http请求信息在Filter透传的容器,之所以称之为容器,因为它可以存储我们像放进去的数据。
注意:
ServerHttpRequest是一个只读类,因此需要通过下面例子的方法来进行修改,对于读多写少的场景,这种设计模式是值得借鉴的
ServerHttpRequest newRequest = request.mutate().headers("key","value").path("/myPath").build();
ServerWebExchange newExchange = exchange.mutate().response(responseDecorator).build();
Gateway 修改返回的响应体
问题描述:
在gateway中修改返回的响应体,在全局Filter中添加如下代码:
import org.springframework.core.Ordered;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class RequestGlobalFilter implements GlobalFilter, Ordered {
//...
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//...
ResponseDecorator decorator = new ResponseDecorator(exchange.getResponse());
return chain.filter(exchange.mutate().response(decorator).build());
}
@Override
public int getOrder() {
return -1000;
}
}
通过.response(decorator)设置一个响应装饰器(自定义),以下是装饰器具体实现:
import cn.hutool.json.JSONObject;
import org.reactivestreams.Publisher;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
/**
* @author visy.wang
* @desc 响应装饰器(重构响应体)
*/
public class ResponseDecorator extends ServerHttpResponseDecorator{
public ResponseDecorator(ServerHttpResponse delegate){
super(delegate);
}
@Override
@SuppressWarnings(value = "unchecked")
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if(body instanceof Flux) {
Flux<DataBuffer> fluxBody = (Flux<DataBuffer>) body;
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
DataBufferUtils.release(join);// 释放掉内存
String bodyStr = new String(content, Charset.forName("UTF-8"));
//修改响应体
bodyStr = modifyBody(bodyStr);
getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
return bufferFactory().wrap(bodyStr.getBytes());
}));
}
return super.writeWith(body);
}
//重写这个函数即可
private String modifyBody(String jsonStr){
JSONObject json = new JSONObject(jsonStr);
//TODO...修改响应体
return json.toString();
}
}
来源:https://blog.csdn.net/weiwoyonzhe/article/details/90814680


猜你喜欢
- 译者注:个人觉得用定时任务来跑垃圾回收不是很好的例子,从译者接触到的项目来看,比较常见的是用定时任务来进行非实时计算,清除临时数据、文件等。
- 后台Java代码【验证码生成】/** * 随机生成6位随机验证码 */ public static String createRandomV
- 在早期的Java版本中,如果需要对指定目录下的文件进行遍历,则需要用递归的方法来实现,这种方法有点复杂,而且灵活性也不高。而使用Java7中
- 废话不多说了,直接给大家贴代码,具体代码如下所示://打开Excel文件,转换为DataTable DataTable dtExcel;pr
- 最近在研究rabbitmq,项目中有这样一个场景:在用户要支付订单的时候,如果超过30分钟未支付,会把订单关掉。当然我们可以做一个定时任务
- /// <summary> /// 安装的excel的版本,0为没有安装,大于1说明安装了多个. /// </summar
- 本文实例为大家分享了Android实现View滑动效果的具体代码,供大家参考,具体内容如下一、View的滑动简介View的滑动是Androi
- 本文实例为大家分享了java实现航空用户管理系统的具体代码,供大家参考,具体内容如下题目内容:某航空公司在其航班到达的不同的国家的不同地方设
- 在你布局或者组件混用的时候你可能会发现 gridview 的九宫格没有完全在页面上显示,只是显示了一个局部(第一行)只有一个滚动条,还不能上
- 前言缓存主要是为了提高数据的读取速度。因为服务器和应用客户端之间存在着流量的瓶颈,所以读取大容量数据时,使用缓存来直接为客户端服务,可以减少
- 一.起缘故事缘于一位朋友的一道题:朋友四人玩LOL游戏。第一局,分别选择位置:中单,上单,ADC,辅助;第二局新加入的伙伴要选上单,四人可选
- 通过zookeeper实现分布式锁1、创建zookeeper的client首先通过CuratorFrameworkFactory创建一个连接
- static修饰符是java里面非常常用的一个东西,用法也非常多。然而,在kotlin里竟然没有这个东西!那该如何替代呢?本文就总结了下ja
- 在进行一些小型APP的开发,或者是对拍照界面没有自定义要求时,我们可以用调起系统相机的方式快速完成拍照需求和不需读写权限进行读写操作的方案一
- 有三种方法如下:三个方法都需要动态申请读写权限否则保存图片到相册也会失败方法一/** * 保存bitmap到本地
- 目标了解HTTP 请求/响应头及常见的属性;了解如何使用SpringBoot处理头信息 ;了解如何使用SpringBoot处理Cookie
- 前言安卓6.0之后,一些敏感权限需要进行动态请求,虽说编写请求授权代码并不难,但是每次一需要权限就需要在视图中添加一段代码,严重影响代码美观
- 一、获取系统当前时间long startTime = System.currentTimeMillis(); //获取开始时间doSomet
- 什么是雪花算法雪花算法的本质为生成一个64位长度的具有自增性的分布式全局唯一id。在64bits中,会对不同段的位进行划分。可分为:符号段时
- 一、系统介绍本系统实现的以下功能管理员功能:登录系统、病人信息的增删改查、就医档案的录入、医生信息的增删改查、科室信息的增删改查、收费统计功