背景
这个情况出现在,我需要进行验证码的校验,因此用户的请求首先需要被验证码过滤器校验,而验证码过滤器不需要设定为全局过滤器,因此我就单纯的把它设定为了一个局部过滤器,代码如下
@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(); }
到此为止,这个问题大概是结束了。