Gateway网关里做Sign校验,getBody读取为Null的解决办法

Gateway网关里做Sign校验,获取body踩坑实践

一、需求描述:系统需要提供第三方调用接口,提供Appid和AppSecrect以及Body里的一系列规则来做校验,校验通过后才允许调用内部接口,要求在Gateway里完成校验功能。

二、问题:Flux body = serverHttpRequest.getBody(); 取出来解析为Null。

ps:笔者将完整代码附在最后总结里,包括缓存Body、读取Body、校验Sign成功后取出—“data”,映射到接口,没有提供业务中具体的Sign 校验方法,希望读者自行完善,如有需要可直接去复制即可,也欢迎批评纠正代码Bug

ps:笔者将完整代码附在最后总结里,包括缓存Body、读取Body、校验Sign成功后取出—“data”,映射到接口,没有提供业务中具体的Sign 校验方法,希望读者自行完善,如有需要可直接去复制即可,也欢迎批评纠正代码Bug

三、原因:

Flux和Mono是Java反应式中的重要概念,但是很多同学包括我在开始都难以理解它们。这其实是规定了两种流式范式,这种范式让数据具有一些新的特性,比如基于发布订阅的事件驱动,异步流、背压等等。另外数据是推送(Push)给消费者的以区别于平时我们的拉(Pull)模式。同时我们可以像Stream Api一样使用类似map、flatmap等操作符(operator)来操作它们。对Flux和Mono这两个概念需要花一些时间去理解它们,不能操之过急。

是一个发出(emit)0-N个元素组成的异步序列的Publisher,可以被onComplete信号或者onError信号所终止。在响应流规范中存在三种给下游消费者调用的方法 onNext, onComplete, 和onError。下面这张图表示了Flux的抽象模型:

响应式编程

简单小结:响应式编程是天然为高并发而生的,其资源是消耗式的。当你在读取getBody时,内部过滤器会先读取一次Body里的内容,导致之后传递下去的请求,无法读取到Body值。

四、 GateWay官网和解决方案

网关的角色是作为一个 API 架构,用来保护、增强和控制对于 API 服务的访问。

API 网关是一个处于应用程序或服务(提供 REST API 接口服务)之前的系统,用来管理授权、访问控制和流量限制等,这样 REST API 接口服务就被 API 网关保护起来,对所有的调用者透明。因此,隐藏在 API 网关后面的业务系统就可以专注于创建和管理服务,而不用去处理这些策略性的基础设施。网关应该尽量保持请求的完整性。

4.1官网提出的解决方案

在这里插入图片描述

4.2 采用方案

注意:解决思路就是获取body之后,重新封装request,然后把封装后的request传递下去。思路很清晰,但是实现的方式却千奇百怪。在使用的过程中碰到了各种千奇百怪的问题,比如说第一次请求正常,第二次请求报400错误,这样交替出现。最终定位原因就是我自定义的全局过滤器把request重新包装导致的,去掉就好了。鉴于踩得坑比较多,下面给出在实现过程中笔者认为的最佳实践。

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;

/**
 * 这个过滤器解决body不能重复读的问题
 * 实际上这里没必要把body的内容放到attribute中去,因为从attribute取出body内容还是需要强转成
 * Flux<DataBuffer>,然后转换成String,和直接读取body没有什么区别
 */
@Component
public class CacheBodyGlobalFilter implements Ordered, GlobalFilter {

//  public static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    if (exchange.getRequest().getHeaders().getContentType() == null) {
      return chain.filter(exchange);
    } else {
      return DataBufferUtils.join(exchange.getRequest().getBody())
          .flatMap(dataBuffer -> {
            DataBufferUtils.retain(dataBuffer);
            Flux<DataBuffer> cachedFlux = Flux
                .defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
                exchange.getRequest()) {
              @Override
              public Flux<DataBuffer> getBody() {
                return cachedFlux;
              }
            };
//            exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, cachedFlux);

            return chain.filter(exchange.mutate().request(mutatedRequest).build());
          });
    }
  }

  @Override
  public int getOrder() {
    return Ordered.HIGHEST_PRECEDENCE;
  }
}

这里需要注意:getOrder 设置为最高级或设置在获取Body前CacheBodyGlobalFilter,这个全局过滤器的目的就是把原有的request请求中的body内容读出来,并且使用ServerHttpRequestDecorator这个请求装饰器对request进行包装,重写getBody方法,并把包装后的请求放到过滤器链中传递下去。这样后面的过滤器中再使用exchange.getRequest().getBody()来获取body时,实际上就是调用的重载后的getBody方法,获取的最先已经缓存了的body数据。这样就能够实现body的多次读取了。
> 值得一提的是,这个过滤器的order设置的是Ordered.HIGHEST_PRECEDENCE,即最高优先级的过滤器。优先级设置这么高的原因是某些系统内置的过滤器可能也会去读body
> 当然,读者也可自行去尝试放在不影响原本业务功能之后,但是一定要放在读取Body内容之前

4.3 解析Body

接下来就是如何解析Body,由于spring cloud gateway使用的是webFlux,因此获取的body内容是Flux结构的,读取的方式如下:

import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.server.reactive.ServerHttpRequest;
import reactor.core.publisher.Flux;

import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RequestUtil
{
    /**
     * 读取body内容
     * @param serverHttpRequest
     * @return
     */
    public static 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);
//            DataBufferUtils.release(buffer);
            String bodyString = new String(bytes, StandardCharsets.UTF_8);
            sb.append(bodyString);
        });
        return formatStr(sb.toString());
    }

    /**
     * 去掉空格,换行和制表符
     * @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;
    }
}

实际上在网上查找资料的过程中发现,解析body内容网上普遍提到两种方式,一种就是上文中的方式,读取字节方式拼接字符串,另一种方式如下:

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();
    }

但是网上有网友说这种方式最多能获取1024字节的数据,数据过长会被截断,导致数据丢失。但由于笔者自身使用场景测试后并没有出现相关问题,所以需要读者自行去测试。

五、总结

声明:在写改业务功能时,笔者对响应式编程并没有很深入的学习,只是粗略的进行了学习,GateWay的官方文档里也没有描述很详细。因此,本文难免有描述不完整的地方,之后系统了解清楚后再进行更新。
最后附上笔者的代码:

涉及到Sign校验的 verify方法 就不粘贴,望读者自行更改。

@Slf4j
@Component
public class CacheBodyGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String requestUrl = exchange.getRequest().getPath().value();
        List<String> granTypeList = exchange.getRequest().getHeaders().get(SignUtil.GRANT_TYPE);
        String granType;
        if (ObjectUtil.isNotEmpty(granTypeList)) {
            assert granTypeList != null;
            granType = granTypeList.getFirst();
        } else {
            granType = null;
        }
        if (!FilterUtil.checkUri(requestUrl, FilterUtil.API_REQUEST_PREFIX)) {
            // 非第三方需要鉴权Sign的接口-放行
            return chain.filter(exchange);
        }
        else {
            // 需要鉴权-先判断grantType是否为空或是否等于设定值“sign”
            if (ObjectUtil.isNull(granType) || ObjectUtil.notEqual(granType, SignUtil.SIGN)) {
                // 验证签名
                throw new BaseException(ResultEnum.GRANT_TYPE_ERROR, FilterUtil.getI18nLang(exchange.getRequest()));
            } else {
                // 需要缓存body,方便校验sign时去取body----Flux<DataBuffer>
                return DataBufferUtils.join(exchange.getRequest().getBody())
                        .flatMap(dataBuffer -> {
                            DataBufferUtils.retain(dataBuffer);
                            Flux<DataBuffer> cachedFlux = Flux
                                    .defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));
                            ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
                                    exchange.getRequest()) {
                                @Override
                                public Flux<DataBuffer> getBody() {
                                    return cachedFlux;
                                }
                            };
                            return chain.filter(exchange.mutate().request(mutatedRequest).build());
                        });
            }
        }
    }

    @Override
    public int getOrder() {
        return HIGHEST_PRECEDENCE;
    }
}

读取Body后进行Sign校验,成功后读取“data”,映射到接口请求参数。

@Component
@Slf4j
@RequiredArgsConstructor
public class OpenFilterApiCheckSignFilter implements GlobalFilter, Ordered {
    @Override
    public int getOrder() {
        // 该过滤器是后来加的为了不影响原有过滤器,将它放在最后,值越大,优先级越小
        return 50;
    }
    @NotNull
    private final MyRedisTemplate myRedisTemplate;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        String requestUrl = exchange.getRequest().getPath().value();
        if (!FilterUtil.checkUri(requestUrl, FilterUtil.API_REQUEST_PREFIX)) {
            // 不是第三方-需要鉴权Sign的接口
            return chain.filter(exchange);
        }
        //如果是post请求,将请求体取出来,再写入
        HttpMethod method = serverHttpRequest.getMethod();
        //将请求体加入了缓存
        String requestBodyStr = HttpMethod.POST.equals(method) ? resolveBodyFromRequest(serverHttpRequest) : null;

        TreeMap<String, Object> paramMap;
        ServerHttpRequest newRequest;
        if (StringUtils.isNotBlank(requestBodyStr)) {
            paramMap=JSON.parseObject(requestBodyStr,TreeMap.class);
            // 该方法为笔者里的Sign校验方法,不提供,需要读者根据自己的业务去编写
            verify(paramMap);
            // 校验成功构建新的data-请求体转发,与接口的参数映射对应
            String data = paramMap.get(SignUtil.DATA).toString();
            Flux<DataBuffer> newBody = createDataBuffer(data);
            newRequest = newBodyRequest(serverHttpRequest, newBody);
            return chain.filter(exchange.mutate().request(newRequest).build());
        }
        else{
            throw new BaseException(ResultEnum.BODY_ERROR.getDesc());
        }
    }

    /**
     * 从Flux<DataBuffer>中获取字符串的方法
     *
     * @return 请求体
     */
    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());
        });
        //获取request body
        return bodyRef.get();
    }
    private Flux<DataBuffer> createDataBuffer(String body) {
        byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
        return Flux.just(new DefaultDataBufferFactory().wrap(bytes));
    }

    private ServerHttpRequest newBodyRequest(ServerHttpRequest originalRequest, Flux<DataBuffer> newBody) {
        return new ServerHttpRequestDecorator(originalRequest) {
            @Override
            public Flux<DataBuffer> getBody() {
                return newBody;
            }
        };
    }
  }

结语:本文只是笔者闲来无事,觉得GateWay和响应式编程很有意思,做了一个小总结,但对这两部分并不是了解的很透彻,欢迎各位网友批评指正,提供读者学习的动力。

  • 19
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值