Spring Cloud Gateway-获取body踩坑实践

问题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());
    }
  • 4
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Spring Cloud Gateway 可以通过自定义过滤器来获取请求体(body)。具体步骤如下: 1. 创建一个自定义过滤器类,实现 GatewayFilter 接口。 2. 在过滤器类中重写 filter 方法,在该方法中获取请求体。 3. 在 Spring Cloud Gateway 配置文件中配置该过滤器。 示例代码如下: ```java @Component public class MyFilter implements GatewayFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 获取请求体 ServerHttpRequest request = exchange.getRequest(); Flux<DataBuffer> body = request.getBody(); // 处理请求体 // ... // 调用下一个过滤器 return chain.filter(exchange); } } ``` 在 Spring Cloud Gateway 配置文件中配置该过滤器: ```yaml spring: cloud: gateway: routes: - id: my_route uri: http://localhost:8080 predicates: - Path=/my_path/** filters: - MyFilter ``` 其中,MyFilter 是自定义过滤器类的名称。在 filters 配置中指定该过滤器即可。 ### 回答2: Spring Cloud Gateway是一个基于Spring Boot的API网关,它允许开发者以统一的方式管理和路由HTTP请求到多个微服务。在实际开发中,有需要获取HTTP请求的body,在Spring Cloud Gateway获取HTTP请求的body需要注意以下几点: 1. 所有的Route Predicate都需要配置读取HTTP请求体,否则在路由到下游服务,请求体会丢失。 2. 如果请求体是将JSON字符串作为参数传递,则需要使用JSON库将字符串转成JSON对象。Spring Cloud Gateway中推荐使用与Spring Framework组件集成的Jackson JSON库。 3. HTTP请求的body只能读取一次,所以需要配置路由过滤器来实现将读取过的请求体保存在请求上下文中,以便后续的路由过滤器和路由处理器获取请求体。 在Spring Cloud Gateway获取HTTP请求的body,可以通过自定义GatewayFilter来实现。下面给出获取HTTP请求体的代码示例: ```java public class BodyGatewayFilterFactory extends AbstractGatewayFilterFactory<BodyGatewayFilterFactory.Config> { public BodyGatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer -> { byte[] bytes = new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); String requestBody = new String(bytes, Charset.forName("UTF-8")); exchange.getAttributes().put("requestBody", requestBody); return chain.filter(exchange); }); }; } public static class Config { } } ``` 在上面的代码中,使用DataBufferUtils.join()函数将请求体存储在字节数组中,并通过exchange的setAttribute()方法存储到请求上下文中。这样,在后续的路由过滤器和路由处理器中就可以通过读取exchange.getAttributes().get("requestBody")来获取HTTP请求的body,而无需重新读取请求体。 ### 回答3: Spring Cloud Gateway是一个基于Spring Boot的网关。它可以在微服务架构中起到路由、负载均衡、API管理等多种作用。 在Spring Cloud Gateway中,获取请求体有两种方式:获取单个请求体和获取多个请求体。 获取单个请求体: 在Spring Cloud Gateway中,获取单个请求体可以使用Exchange对象的getBody()方法。这个方法会返回一个Mono对象,需要使用subscribe()方法来订阅结果。 例如: ```java public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { Mono<String> requestBody = exchange.getRequest().getBodyToMono(String.class); requestBody.subscribe(content -> { // 对请求体进行操作 }); return chain.filter(exchange); } ``` 上面代码中,我们使用getBodyToMono()获取请求体,然后使用subscribe()方法来订阅请求体的内容。订阅成功后,我们可以对请求体进行操作。 获取多个请求体: 在Spring Cloud Gateway中,获取多个请求体可以使用GlobalFilter。GlobalFilter是一种全局过滤器,可以对所有的请求进行处理。 我们可以创建一个自定义的GlobalFilter,然后在filter()方法中获取请求体。 例如: ```java @Component public class MyGlobalFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { MediaType mediaType = exchange.getRequest().getHeaders().getContentType(); Flux<DataBuffer> body = exchange.getRequest().getBody(); return chain.filter(exchange.mutate().request( exchange.getRequest().mutate().body(Flux.just(body)).build()) .build()); } } ``` 上面代码中,我们创建了一个MyGlobalFilter类,并实现了GlobalFilter接口。在filter()方法中,我们使用getBody()获取请求体。获取请求体后,我们更改了请求体的数据,然后使用build()方法创建了一个新的Exchange对象,并返回chain.filter()。 总结: Spring Cloud Gateway可以通过Exchange对象来获取请求体。可以使用getBody()方法获取单个请求体,也可以使用GlobalFilter获取多个请求体。 注意:在Spring Cloud Gateway中,请求体是一个Flux对象。如果需要将请求体转换成其他类型,请使用getBodyToMono()方法。由于Flux对象可能包含多个元素,因此在订阅操作需要注意。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值