Lucent's Blog

当时明月在,曾照彩云归。



代码在写我

Bug在De我

螃蟹在剥我的壳

漫天的我落在雪花上

而你在想我...

6ams5piO5pyI

SpringCloudGateway读取、修改请求体(modify SpringCloudGateway request body)

在Spring Cloud Gateway中,读取Get请求的参数并非一件很难的事,但是读取Post请求的请求体(body)也并非一件简单事。

说明

Spring官方虽然提供了预言类:
ReadBodyPredicateFactory (谓词工厂)、ModifyRequestBodyGatewayFilterFactory (过滤器工厂),
但仍然是 bate 版,并没有直接实现获取 request body 的 filter,所以如果想要读取request body,需要参考以上两个预言类,自行实现filter。

实现方式

首先,实现一个CacheRequestBodyFilter用来将request body读取出来并存到exchange的自定义属性中:

package ltd.lemontech.contractor.gateway.filter;

import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import ltd.lemontech.contractor.gateway.constant.FilterConstant;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 *
 * 作用: 此过滤器主要作用是将请求body读取出来并存到exchange的自定义属性中,等待后续过滤器(ModifyRequestBodyFilter)处理
 * 使用: 使用本过滤器应该配合ModifyRequestBodyFilter一起使用,并且两个过滤器执行顺序必须CacheRequestBodyFilter在前
 *      ModifyRequestBodyFilter在后,否则后续业务将无法获得body中的参数
 * @author lucent
 */
@Slf4j
@Component
@Order(value = -100)
public class CacheRequestBodyFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 将 request body 中的内容复制一份,记录到 exchange 的一个自定义属性中
        Object cachedRequestBodyObject = exchange.getAttributeOrDefault(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, null);
        // 如果已经缓存过,略过
        if (ObjectUtil.isNotNull(cachedRequestBodyObject)) {
            return chain.filter(exchange);
        }
        // 如果没有缓存过,获取字节数组存入 exchange 的自定义属性中
        return DataBufferUtils.join(exchange.getRequest().getBody())
                .map(dataBuffer -> {
                    byte[] bytes = new byte[dataBuffer.readableByteCount()];
                    dataBuffer.read(bytes);
                    DataBufferUtils.release(dataBuffer);
                    return bytes;
                }).defaultIfEmpty(new byte[0])
                .doOnNext(bytes -> exchange.getAttributes().put(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, bytes))
                .then(chain.filter(exchange));
    }

}

然后,实现一个ModifyRequestBodyFilter,用来修改请求体,本例中是将原请求的body从exchange中取出来放到一个新请求中继续向下传递,因为每个请求的body只能被读取一次,上面的CacheRequestBodyFilter读取之后如果不用新请求替换旧请求,后面的业务将获取不到request body。
代码:

package ltd.lemontech.contractor.gateway.filter;

import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import ltd.lemontech.contractor.gateway.constant.FilterConstant;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

/**
 * 自定义请求体过滤器
 * 主要作用:将被CacheRequestBodyFilter 读取过body的请求换成一个新的请求继续向下传递
 * 原因:每个请求body只能被读取一次,当body被将被CacheRequestBodyFilter读取后,后续业务将无法正常收到body,
 *      所以用一个新的请求继续,也因此,本过滤器执行顺序(order)必须在CacheRequestBodyFilter之后
 * 扩展:这里也可以对原body进行修改,但目前不需要修改,只需要将原body从exchange自定义属性中取出来放到新请求中即可
 * @author lucent
 */
@Slf4j
@Component
@Order(value = -90)
public class ModifyRequestBodyFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 尝试从 exchange 的自定义属性中取出缓存到的 body
        Object cachedRequestBodyObject = exchange.getAttributeOrDefault(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY, null);
        if (ObjectUtil.isNotNull(cachedRequestBodyObject)) {
            byte[] body = (byte[]) cachedRequestBodyObject;
            DataBufferFactory dataBufferFactory = exchange.getResponse().bufferFactory();
            ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
                @Override
                public Flux<DataBuffer> getBody() {
                    if (body.length > 0) {
                        return Flux.just(dataBufferFactory.wrap(body));
                    }
                    return Flux.empty();
                }
            };
            return chain.filter(exchange.mutate().request(decorator).build());
        }
        // 为空,说明已经读过,或者 request body 原本即为空,不做操作,传递到下一个过滤器链
        return chain.filter(exchange);
    }

}

这两个过滤器执行顺序必须是CacheRequestBodyFilter在前ModifyRequestBodyFilter在后,否则后续业务将无法获得body中的参数

应用

请求通过以上两个filter之后,后续的filter如果想要读取request body,就可以直接从exchange的自定义属性中获取了,这样并不会影响后续业务。

下面实现请求信息打印到日志的功能,LoggerFilter:

package ltd.lemontech.contractor.gateway.filter;

import cn.hutool.core.date.DateUtil;
import lombok.extern.slf4j.Slf4j;
import ltd.lemontech.contractor.gateway.constant.FilterConstant;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
 * 请求日志打印过滤器
 * 作用: 打印指定模块的请求内容,其中请求body是从exchange自定义属性中取出来的,
 *      所以本过滤器的执行顺序(order)应该在CacheRequestBodyFilter和ModifyRequestBodyFilter之后
 * @author lucent
 */
@Slf4j
@Component
@Order(value = 0)
public class LoggerFilter implements GlobalFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String now = DateUtil.now();
        StringBuffer text = new StringBuffer();
        text.append("================请求日志-" + now + "-开始================\n");
        text.append("请求ID: ").append(request.getId()).append("\n");
        text.append("请求方式: ").append(request.getMethodValue()).append("\n");
        text.append("请求Token: ").append(request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION)).append("\n");
        text.append("请求路径: ").append(request.getPath()).append("\n");
        text.append("路径参数: ").append(request.getQueryParams()).append("\n");

        Object cachedRequestBodyObject = exchange.getAttributes().get(FilterConstant.CACHED_REQUEST_BODY_OBJECT_KEY);
        if (cachedRequestBodyObject != null) {
            byte[] body = (byte[]) cachedRequestBodyObject;
            String string = new String(body);
            text.append("请求体: ").append("\n").append(string).append("\n");
        }
        text.append("================请求日志-").append(now).append("-结束================\n");
        System.out.println(text);
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            ServerHttpResponse response = exchange.getResponse();

            HttpStatus statusCode = response.getStatusCode();
            HttpHeaders responseHeaders = response.getHeaders();

            StringBuilder res = new StringBuilder();
            String time = DateUtil.now();
            res.append("================响应日志-").append(time).append("-开始================\n");
            res.append("请求ID: ").append(request.getId()).append("\n");
            if (statusCode != null) {
                res.append("响应状态: ").append(statusCode.value()).append("\n");
            }
            res.append("响应头: ").append(responseHeaders).append("\n");
            res.append("================响应日志-").append(time).append("-结束================\n");
            System.out.println(res);
        }));
    }

}

效果

POST请求:
image.png
GET请求:
image.png

至此就实现了请求日志记录

上一篇

Dayz第三方登录器的使用方法(Dayz sa launcher)

Dayz Sa Launcher 登录器使用方法…

阅读
下一篇

Nginx开启Gzip兼容多层反代

Nginx开启gzip兼容多层nginx反代…

阅读