问题1:无法获取body内容
问题原因分析
在使用过程中碰到过滤器中获取的内容一直都是空的,尝试了网上的各种解析body内容的方法,但是得到结果都是一样,死活获取不到body数据,一度很崩溃。后来进行了各种尝试,最终发现使用不同的spring boot版本和spring cloud版本,对结果影响很大。
方案1:降低版本
springboot版本:2.0.5-RELEASE
springcloud版本:Finchley.RELEASE
使用以上的版本会报以下的错误:
java.lang.IllegalStateException: Only one connection receive subscriber allowed.
原因在于spring boot在2.0.5版本如果使用了WebFlux就自动配置HiddenHttpMethodFilter过滤器。查看源码发现,这个过滤器的作用是,针对当前的浏览器一般只支持GET和POST表单提交方法,如果想使用其他HTTP方法(如:PUT、DELETE、PATCH),就只能通过一个隐藏的属性如(_method=PUT)来表示,那么HiddenHttpMethodFilter的作用是将POST请求的_method参数里面的value替换掉http请求的方法。但是这就导致已经读取了一次body,导致后面的过滤器无法读取body。解决方案就是可以自己重写HiddenHttpMethodFilter来覆盖原来的实现,实际上gateway本身就不应该做这种事情,原始请求是怎样的,转发给下游的请求就应该是怎样的。
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new HiddenHttpMethodFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(exchange);
}
};
}
这个方案也是gateway官方开发者目前所提出的解决方案。
方案2:不降低版本,缓存body内容
springboot版本:2.1.5-RELEASE
springcloud版本:Greenwich.SR1
在较高版本中,上面的方法已经行不通了,可以自定义一个高优先级的过滤器先获取body内容并缓存起来,解决body只能读取一次的问题。具体解决方案见问题2。
问题2:body只能读取一次
这个问题网上主要的解决思路就是获取body之后,重新封装request,然后把封装后的request传递下去。思路很清晰,但是实现的方式却千奇百怪。在使用的过程中碰到了各种千奇百怪的问题,比如说第一次请求正常,第二次请求报400错误,这样交替出现。最终定位原因就是我自定义的全局过滤器把request重新包装导致的,去掉就好了。鉴于踩得坑比较多,下面给出在实现过程中笔者认为的最佳实践。
核心代码
package com.*_*.*_*.gateway.config;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.*_*.*_*.gateway.utils.IpUtil;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
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.data.redis.core.StringRedisTemplate;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/***
* @ClassName: MyGlobalFilter
* @Description: 自定义gateway全局过滤器, 全局过滤器无需配置, 对所有的路由都生效
* @Author: TKQ
* @Create_time: 14:30 2021-03-02
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class MyGlobalFilter implements GlobalFilter, Ordered {
private final StringRedisTemplate stringRedisTemplate;
/**
* 请求频率限制时间
*/
private static final Integer OFFSET = 3;
@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取当前请求的url
String requestUrl = exchange.getRequest().getURI().toString();
if (requestUrl.contains("chat-record/timCallBack")) {
log.debug("{} 请求进入 {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")),requestUrl);
}
//TODO 校验token合法性等等,自定义逻辑
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String methodName = serverHttpRequest.getMethodValue();
String contentType = serverHttpRequest.getHeaders().getFirst("Content-Type");
URI uri = serverHttpRequest.getURI();
//post请求拦截判断重复调用
if(HttpMethod.POST.name().equals(methodName) && !contentType.startsWith("multipart/form-data")){
AtomicReference<String> bodyRef = new AtomicReference<>();
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());
DataBufferUtils.retain(dataBuffer);
bodyRef.set(charBuffer.toString());
String bodyStr = formatStr(bodyRef.get());
Flux<DataBuffer> cachedFlux = Flux
.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
// 缓存请求到redis
String clientIp = IpUtil.getClientIp(serverHttpRequest);
log.info("重复请求过滤器;url={},ip ={}", uri.toString(), clientIp);
String key = clientIp + "-" + uri.toString();
if (stringRedisTemplate.hasKey(key)) {
//判断缓存参数是否相同
if (StringUtils.isEmpty(bodyStr)) {
//参数为空存入key
bodyStr = key;
}
String param = stringRedisTemplate.opsForValue().get(key);
if (bodyStr.equals(param)) {
//参数相同表示重复请求,再次刷新缓存时间
stringRedisTemplate.opsForValue().set(key, bodyStr);
stringRedisTemplate.expire(key, OFFSET, TimeUnit.SECONDS);
//自定义响应结果
ServerHttpResponse response = exchange.getResponse();
JSONObject message = new JSONObject();
message.set("status", -1);
message.set("data", "重复请求");
byte[] bits = JSONUtil.toJsonStr(message).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "text/plain;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
}
//第一次请求放入缓存
stringRedisTemplate.opsForValue().set(key, bodyStr);
stringRedisTemplate.expire(key, OFFSET, TimeUnit.SECONDS);
//封装request,传给下一级
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest).build());
});
}
//放行请求
return chain.filter(exchange);
}
/**
* 去掉空格,换行和制表符
* @param str
* @return
*/
private static String formatStr(String str){
if (str != null && str.length() > 0) {
Pattern p = Pattern.compile("\\s*|\t|\r|\n");
Matcher m = p.matcher(str);
return m.replaceAll("");
}
return str;
}
@Override
public int getOrder() {
//设置最高级别,优先执行该过滤器,防止gateway读取body后后续的过滤器不能正常获取body
return Ordered.HIGHEST_PRECEDENCE;
}
}
IpUtil.java
package com.*_*.*_*.gateway.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Optional;
/**
* IP地址工具类
*
* @author tkq
* @date 2021/1/25 17:02
*/
@Slf4j
@Component
public class IpUtil {
private static final String IP_UNKNOWN = "unknown";
private static final String IP_LOCAL = "127.0.0.1";
private static final int IP_LEN = 15;
/**
* 私有化构造器
*/
private IpUtil() {
}
/**
* 获取客户端真实ip
* @param request request
* @return 返回ip
*/
public static String getClientIp(ServerHttpRequest request) {
HttpHeaders headers = request.getHeaders();
String ipAddress = headers.getFirst("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = headers.getFirst("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || IP_UNKNOWN.equalsIgnoreCase(ipAddress)) {
ipAddress = Optional.ofNullable(request.getRemoteAddress())
.map(address -> address.getAddress().getHostAddress())
.orElse("");
if (IP_LOCAL.equals(ipAddress)) {
// 根据网卡取本机配置的IP
try {
InetAddress inet = InetAddress.getLocalHost();
ipAddress = inet.getHostAddress();
} catch (UnknownHostException e) {
// ignore
}
}
}
// 对于获取到多ip的情况下,找到公网ip.
String sIp = null;
// 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
if (ipAddress != null && ipAddress.length() > IP_LEN) {
String[] ipsz = ipAddress.split(",");
for (String anIpsz : ipsz) {
if (!isInnerIp(anIpsz.trim())) {
sIp = anIpsz.trim();
break;
}
}
if (null == sIp) {
sIp = ipsz[0].trim();
}
ipAddress = sIp;
}
return ipAddress;
}
/**
* 判断IP是否是内网地址
* @param ipAddress ip地址
* @return 是否是内网地址
*/
public static boolean isInnerIp(String ipAddress) {
boolean isInnerIp;
long ipNum = getIpNum(ipAddress);
/**
私有IP:A类 10.0.0.0-10.255.255.255
B类 172.16.0.0-172.31.255.255
C类 192.168.0.0-192.168.255.255
当然,还有127这个网段是环回地址
**/
long aBegin = getIpNum("10.0.0.0");
long aEnd = getIpNum("10.255.255.255");
long bBegin = getIpNum("172.16.0.0");
long bEnd = getIpNum("172.31.255.255");
long cBegin = getIpNum("192.168.0.0");
long cEnd = getIpNum("192.168.255.255");
isInnerIp = isInner(ipNum, aBegin, aEnd) || isInner(ipNum, bBegin, bEnd) || isInner(ipNum, cBegin, cEnd)
|| IP_LOCAL.equals(ipAddress);
return isInnerIp;
}
private static long getIpNum(String ipAddress) {
String[] ip = ipAddress.split("\\.");
long a = Integer.parseInt(ip[0]);
long b = Integer.parseInt(ip[1]);
long c = Integer.parseInt(ip[2]);
long d = Integer.parseInt(ip[3]);
return a * 256 * 256 * 256 + b * 256 * 256 + c * 256 + d;
}
private static boolean isInner(long userIp, long begin, long end) {
return (userIp >= begin) && (userIp <= end);
}
}
MyGlobalFilter 这个全局过滤器的目的就是把原有的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)
其他过滤器获取body使用如下方法即可
/**
* 方法一:
* 网上有网友说这种方式最多能获取1024字节的数据,数据过长会被截断,
* 导致数据丢失。这里笔者没有亲自验证过,只是把这种方式提供在这里供大家参考。
*/
private String getBodyContent(ServerWebExchange exchange){
Flux<DataBuffer> body = exchange.getRequest().getBody();
AtomicReference<String> bodyRef = new AtomicReference<>();
// 缓存读取的request body信息
body.subscribe(dataBuffer -> {
CharBuffer charBuffer = StandardCharsets.UTF_8.decode(dataBuffer.asByteBuffer());
DataBufferUtils.release(dataBuffer);
bodyRef.set(charBuffer.toString());
});
//获取request body
return bodyRef.get();
}
/**
* 方法二:
* 读取字节方式拼接字符串
*/
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest){
//获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
StringBuilder sb = new StringBuilder();
body.subscribe(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
sb.append(bodyString);
});
return formatStr(sb.toString());
}