【Java】SpringCloud Gateway自定义过滤器中获取ServerHttpRequest的body中的数据为NULL的问题

简介: 【Java】SpringCloud Gateway自定义过滤器中获取ServerHttpRequest的body中的数据为NULL的问题

背景

这个情况出现在,我需要进行验证码的校验,因此用户的请求首先需要被验证码过滤器校验,而验证码过滤器不需要设定为全局过滤器,因此我就单纯的把它设定为了一个局部过滤器,代码如下

@Component
public class ValidateCodeFilter //implements GlobalFilter, Ordered
        extends AbstractGatewayFilterFactory<Object>
{
    //需要生成验证码的路径
    private final static String[] VALIDATE_URL =
            new String[] { "/auth/login", "/auth/register" };
    //验证码服务
    @Autowired
    private ValidateCodeService validateCodeService;
    //验证码配置学习
    @Autowired
    private CaptchaProperties captchaProperties;
    //验证码内容
    private static final String CODE = "code";
    //验证码的uuid
    private static final String UUID = "uuid";
    @Override
    public GatewayFilter apply(Object config)
    {
         return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            // 非登录/注册请求或验证码关闭,不处理
            if (!StringUtils.containsAnyIgnoreCase(request.getURI().getPath(),
                    VALIDATE_URL) || !captchaProperties.getEnabled())
            {
                return chain.filter(exchange);
            }
            try
            {
                String rspStr = resolveBodyFromRequest(request);
                //接收JSON格式的请求
                JSONObject obj = JSON.parseObject(rspStr);
                validateCodeService.checkCaptcha(obj.getString(CODE), obj.getString(UUID));
            }
            catch (Exception e)
            {
                return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
            }
            return chain.filter(exchange);
        };
    }
    private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest)
    {
        // 获取请求体
        Flux<DataBuffer> body = serverHttpRequest.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });
        return bodyRef.get();
    }
}

然后我进行请求的时候,json参数如下

然后请求经过解析后会发现,字符串居然是null

具体原因不太确定,但是应该是网络传递的时候,这个数据丢失了,原本的数据应该封装在这里

然后我就想着,有没有可能是这个局部过滤器的位置的问题,因为我之前就是由于这个局部过滤器的位置,放在的位置比较靠后,导致他压根没有被执行。

例如这是我早期的配置,可以发现,请求的路径是重复处理的,那么就会导致之前的过滤器处理完毕之后,这个验证码的过滤器压根就不会被执行,所以我就试着把这个过滤器的位置放在了更前面,方法确实得到了执行。但是这样子并不能解决说ServerHttpRequest的getBody返回null的问题。

这是在我没有修改过滤器为之前的执行流程,后面我修改了代码。

我也明白为什么会导致null,其实原因是因为

request.getInputStream(); request.getReader(); 和request.getParameter(“key”)这三个方法中的任何一个方法执行之后,之后再次执行,就会失效。

所以我就想着,我应该可以考虑重写一下过滤器的流程,把传递过来的ServerHttpRequest进行修改,然后重载其getBody方法,让其去缓存中获取数据,而网络上其实已经有很多解决方式了

所以其实我只要能做一个缓存,让之后的ServerHttpRequest去这个缓存中获取数据就好。

代码如下

package com.towelove.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
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;
/**
 * @author: Blossom
 * CacheBodyGlobalFilterk的作用是为了解决
 * ServerHttpRequest中body的数据为NULL的情况
 */
@Slf4j
@Component
public class CacheBodyGlobalFilter implements Ordered, GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (exchange.getRequest().getHeaders().getContentType() == null) {
            return chain.filter(exchange);
        } else {
            //获取databuffer
            return DataBufferUtils.join(exchange.getRequest().getBody())
                    .flatMap(dataBuffer -> { //设定返回值并处理
                        DataBufferUtils.retain(dataBuffer); //设定存储空间
                        Flux<DataBuffer> cachedFlux = Flux//读取Flux中所有数据并且保存
                                .defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
                        ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator( //得到ServerHttpRequest
                                exchange.getRequest()) {
                            @Override //重载getBody方法 让其从我设定的缓存获取
                            public Flux<DataBuffer> getBody() {
                                return cachedFlux;
                            }
                        };
                        //放行 并且设定exchange为我重载后的
                        return chain.filter(exchange.mutate().request(mutatedRequest).build());
                    });
        }
    }
      //尽可能早的对这个请求进行封装
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

CacheBodyGlobalFilter这个全局过滤器的目的就是把原有的request请求中的body内容读出来,并且使用ServerHttpRequestDecorator这个请求装饰器对request进行包装,重写getBody方法,并把包装后的请求放到过滤器链中传递下去。这样后面的过滤器中再使用exchange.getRequest().getBody()来获取body时,实际上就是调用的重载后的getBody方法,获取的最先已经缓存了的body数据。这样就能够实现body的多次读取了。

值得一提的是,这个过滤器的order设置的是Ordered.HIGHEST_PRECEDENCE,即最高优先级的过滤器。优先级设置这么高的原因是某些系统内置的过滤器可能也会去读body,这样就会导致我们自定义过滤器中获取body的时候报body只能读取一次这样的错误如下:

java.lang.IllegalStateException: Only one connection receive subscriber allowed.
  at reactor.ipc.netty.channel.FluxReceive.startReceiver(FluxReceive.java:279)
  at reactor.ipc.netty.channel.FluxReceive.lambda$subscribe$2(FluxReceive.java:129)

之后,只要让我们后面的请求去这个缓存中获取数据即可。增加全局过滤器之后的过滤器链如下。

之后再次发送请求,就可以发现我能拿到数据了,因为其getBody是从CacheBodyGlobalFilter这里获取的数据,所以当你的请求再次执行getBody的时候,他会去这个类中执行getBody方法,所以我在debug的时候,他会再次的执行getBody方法

然后下面是我获取body中数据并且进行解析为字符串的方法

private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest)
    {
        // 获取请求体
        Flux<DataBuffer> body = serverHttpRequest.getBody();
        AtomicReference<String> bodyRef = new AtomicReference<>();
        body.subscribe(buffer -> {
            CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());
            DataBufferUtils.release(buffer);
            bodyRef.set(charBuffer.toString());
        });
        return bodyRef.get();
    }

到此为止,这个问题大概是结束了。


相关文章
|
22天前
|
Java API 开发工具
【Azure Developer】Java代码实现获取Azure 资源的指标数据却报错 "invalid time interval input"
在使用 Java 调用虚拟机 API 获取指标数据时,因本地时区设置非 UTC,导致时间格式解析错误。解决方法是在代码中手动指定时区为 UTC,使用 `ZoneOffset.ofHours(0)` 并结合 `withOffsetSameInstant` 方法进行时区转换,从而避免因时区差异引发的时间格式问题。
117 4
|
2月前
|
数据采集 JSON Java
Java爬虫获取1688店铺所有商品接口数据实战指南
本文介绍如何使用Java爬虫技术高效获取1688店铺商品信息,涵盖环境搭建、API调用、签名生成及数据抓取全流程,并附完整代码示例,助力市场分析与选品决策。
|
2月前
|
数据采集 存储 前端开发
Java爬虫性能优化:多线程抓取JSP动态数据实践
Java爬虫性能优化:多线程抓取JSP动态数据实践
|
29天前
|
算法 Java
Java多线程编程:实现线程间数据共享机制
以上就是Java中几种主要处理多线程序列化资源以及协调各自独立运行但需相互配合以完成任务threads 的技术手段与策略。正确应用上述技术将大大增强你程序稳定性与效率同时也降低bug出现率因此深刻理解每项技术背后理论至关重要.
66 16
|
6月前
|
自然语言处理 Java 关系型数据库
Java|小数据量场景的模糊搜索体验优化
在小数据量场景下,如何优化模糊搜索体验?本文分享一个简单实用的方案,虽然有点“土”,但效果还不错。
92 0
|
6月前
|
JSON API Go
Golang工程组件:自定义HTTP规则的grpc-gateway选项
总的来说,grpc-gateway提供了一种简单有效的方式来为你的gRPC服务提供RESTful风格的API。通过自定义HTTP规则,你可以灵活地定义你的API的行为,以满足你的应用的需求。
138 27
|
10月前
|
前端开发 JavaScript Java
java常用数据判空、比较和类型转换
本文介绍了Java开发中常见的数据处理技巧,包括数据判空、数据比较和类型转换。详细讲解了字符串、Integer、对象、List、Map、Set及数组的判空方法,推荐使用工具类如StringUtils、Objects等。同时,讨论了基本数据类型与引用数据类型的比较方法,以及自动类型转换和强制类型转换的规则。最后,提供了数值类型与字符串互相转换的具体示例。
476 3
|
传感器 分布式计算 安全
Java 大视界 -- Java 大数据在智能安防入侵检测系统中的多源数据融合与分析技术(171)
本文围绕 Java 大数据在智能安防入侵检测系统中的应用展开,剖析系统现状与挑战,阐释多源数据融合及分析技术,结合案例与代码给出实操方案,提升入侵检测效能。
|
7月前
|
前端开发 Cloud Native Java
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
博客不应该只有代码和解决方案,重点应该在于给出解决方案的同时分享思维模式,只有思维才能可持续地解决问题,只有思维才是真正值得学习和分享的核心要素。如果这篇博客能给您带来一点帮助,麻烦您点个赞支持一下,还可以收藏起来以备不时之需,有疑问和错误欢迎在评论区指出~
Java||Springboot读取本地目录的文件和文件结构,读取服务器文档目录数据供前端渲染的API实现
|
8月前
|
数据采集 JSON Java
Java爬虫获取微店快递费用item_fee API接口数据实现
本文介绍如何使用Java开发爬虫程序,通过微店API接口获取商品快递费用(item_fee)数据。主要内容包括:微店API接口的使用方法、Java爬虫技术背景、需求分析和技术选型。具体实现步骤为:发送HTTP请求获取数据、解析JSON格式的响应并提取快递费用信息,最后将结果存储到本地文件中。文中还提供了完整的代码示例,并提醒开发者注意授权令牌、接口频率限制及数据合法性等问题。