Gateway学习笔记一 responseBody和resquestBody的获取

1 引言

笔者在实现开发者服务网关模块的任务过程中,遇到下列需求(有关requestBody和responseBody部分):

  • 对所有的请求,取出requestBody作为参数,调用鉴权接口
  • 不影响requsetBody前提下,路由转发
  • 从路由转发的回复中取出responseBody,作为参数调用统计接口

gateway的工作流程如图,filter的传递中,我们通常用ServerWebExchange来获取请求、回复、相关参数等。

我最初的思路,想当然地希望从ServerWebExchange中直接使用exchange.getXXX()方法获取body,

 

然而实际上从exchange中无法读取具体的requestBody和responseBody。

 

 

 

2 原因分析


2.1 ServerWebExchange

ServerWebExchange命名为服务网络交换器,存放着重要的请求-响应属性、请求实例和响应实例等等,有点像Context的角色

 

注意到ServerWebExchange.mutate()方法,通过使用decorator将exchange重新包装起来。

ServerWebExchange实例可以理解为不可变实例,如果我们想要修改它,需要通过mutate()方法生成一个新的实例

 

 

2.2 ServerHttpRequest

ServerHttpRequest实例是用于承载请求相关的属性和请求体。
Spring Cloud Gateway中底层使用Netty处理网络请求,通过追溯源码,可以从ReactorHttpHandlerAdapter中得知ServerWebExchange实例中持有的ServerHttpRequest实例的具体实现是ReactorServerHttpRequest

ReactorServerHttpRequest的父类AbstractServerHttpRequest中初始化内部属性headers的时候把请求的HTTP头部封装为只读的实例,所以不能直接从ServerHttpRequest实例中直接获取请求实例并且进行修改。

 

如果要修改,需要使用2.1中提到的mutate方法重新包装生成一个新实例,具体的实现在下面介绍。

 

3 解决方案


3.1 基于ReadBodyPredicateFactory的实现

ReadBodyPredicateFactory源码指出,body只允许从request中读取一次,再次读取时会抛异常。因此对于已经读取过的requestBody,为了不影响后期,需要对请求体内容进行二次包装,即第一次读取内容进行缓存,后面对同个请求体的读取则直接返回缓存内容。

 

下面是router提供的body读取方法,其中bodyToMono方法我们可以拿到完整的body内容,并返回指定类型inClass,body即为读取到的请求体内容对应的数组。

下面是仿照ReadBodyPredicateFactory的方式获取body的解决方案。
 1 public final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
 2 
 3 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
 4     log.info("仿照ReadBodyPredicateFactory的方式获取body---------成功");
 5     return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange,
 6             (serverHttpRequest) -> ServerRequest
 7                     //用mutate()方法重新封装
 8                     .create(exchange.mutate().request(serverHttpRequest)    
 9                             .build(), messageReaders)
10                     //获取完整的body内容,转成string
11                     .bodyToMono(String.class)    
12                     .doOnNext(bodyString -> {
13                             //以下是业务逻辑,把bodyString去除空格换行,再放入attributes中
14                             bodyString = bodyString.replaceAll("\r\n", "");
15                             bodyString = bodyString.replaceAll(" ", "");
16                         exchange.getAttributes().put(
17                                 Constants.AUTH_SIGN_VO_REQUEST_BODY, bodyString);
18                         log.info("放入参数的body为:{}", bodyString);
19                     })
20                     .then(chain.filter(exchange)));
21 }

用cacheRequestBodyAndRequest方法缓存了requestBody,本质上还是用ServerHttpRequestDecorator重新封装了requestBody,覆盖对应获取请求体数据缓冲区,以达到多次读取的目的。
缺陷就是,这里的bodyToMono的class写死为string,不够灵活。

3.2 基于ModifyRequestBodyGatewayFilterFactory的实现


官网提供了ModifyRequestBodyFilter和ModifyResponseBodyFilter来获取修改body,但仅支持以Java DSL的方式来配置。

@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
            .filters(f -> f.prefixPath("/httpbin")
                .modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
                    (exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
        .build();
}

@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
            .filters(f -> f.prefixPath("/httpbin")
                .modifyResponseBody(String.class, String.class,
                    (exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri))
        .build();
}

而在开发中,我们更倾向于使用yml配置的方式来配置filter,为了灵活开发,我在这里仿照ModifyRequestBodyFilter来实现了自己的全局过滤器

获取reqeustBody最终方案
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
    Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)    //bodyToMono获取body的内容
            .flatMap(
                    data -> {
                        try {
                            byte[] body = data.getBytes(StandardCharsets.UTF_8);
                            //以下是具体业务逻辑
                            String bodyString = new String(body);
                            //删除所有换行和空格
                            bodyString = bodyString.replaceAll("\r\n", "");
                            bodyString = bodyString.replaceAll(" ", "");
                            log.debug("request body string is :{}", bodyString);
                            //把bodyString放入attribute供后续使用
                            exchange.getAttributes().put(
                                    Constants.AUTH_SIGN_VO_REQUEST_BODY, bodyString);
                        } catch (ExceptionWithErrorCode e) {
                            return Mono.error(e);
                        }
                        return Mono.just(data);
                    });
    //重新封装修改后的body,本业务中实际无修改,但也必须重新封装body
    BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
    HttpHeaders headers = new HttpHeaders();
    headers.putAll(exchange.getRequest().getHeaders());
    headers.remove(HttpHeaders.CONTENT_LENGTH);
    CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
    return bodyInserter.insert(outputMessage, new BodyInserterContext())
            // .log("modify_request", Level.INFO)
            .then(Mono.defer(() -> {
                ServerHttpRequestDecorator decorator = CommonUtils.decorate(exchange, headers, outputMessage);
                //decorate重新封装
                return chain.filter(exchange.mutate().request(decorator).build());
            }));
}
  • 失败尝试

用正确实现取body,但不使用bodyInserter重新封装request

public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
    return serverRequest.bodyToMono(String.class)
            .flatMap(
                    //同上实现,省略
                    ……
                    }).then(chain.filter(exchange));
}

结果,每两次调用,第二次都会报错

 

前面也提到,requestBody只允许访问一次,若不重新设置的话会导致后面访问抛异常。

 

3.3 思路三


笔者在查询资料过程中,看到网上博文提供了第三种解决思路:
由于从exchange.getRequest中获取的是FluxMap类型,因此可以通过重写Flux<DataBuffer> getBody()方法,包装后的请求放到过滤器链中传递下去。这样后面的过滤器中再使用exchange.getRequest().getBody()来获取body时,实际上就是调用的重载后的getBody方法,获取的最先已经缓存了的body数据。这样就能够实现body的多次读取了。
不过这种思路同样绕不过要使用ServerHttpRequestDecorator这个请求装饰器对request进行重新包装。

3.4 获取responseBody


responseBody的获取方法也是同样的思路,可以参找ModifyResponseBodyGatewayFilterFactory的实现方式

下面是获取resopnseBody的解决方案
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpResponse originalResponse = exchange.getResponse();
    originalResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON);

    DataBufferFactory bufferFactory = originalResponse.bufferFactory();
    ServerHttpResponseDecorator response = new ServerHttpResponseDecorator(originalResponse) {
        @Override
        public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
            if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) {
                Flux<? extends DataBuffer> fluxBody = Flux.from(body);
                return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
                    DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
                    //dataBuffer合并成一个,解决获取结果不全问题
                    DataBuffer join = dataBufferFactory.join(dataBuffers);
                    byte[] content = new byte[join.readableByteCount()];
                    join.read(content);
                    DataBufferUtils.release(join);
                    // 转为字符串
                    String responseData = new String(content, Charsets.UTF_8);
                    /** 业务逻辑 */
                    //去除所有换行和空格
                    responseData = responseData.replaceAll("\r\n", "");
                    responseData = responseData.replaceAll(" ", "");
                    log.debug("responseBody string is:{}", responseData);
                    exchange.getAttributes().put(Constants.AUTH_SIGN_VO_RESPONSE_BODY, responseData);
                    byte[] uppedContent = responseData.getBytes(Charsets.UTF_8);
                    originalResponse.getHeaders().setContentLength(uppedContent.length);
                    return bufferFactory.wrap(uppedContent);
                }));
            }
            return super.writeWith(body);
        }

        @Override
        public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
            return writeWith(Flux.from(body).flatMapSequential(p -> p));
        }
    };
    return chain.filter(exchange.mutate().response(response).build());
}

 

4 总结

  • ServerWebExchange中存放着重要的请求-响应属性、请求实例和响应实例,类似于上下文
  • ReadBodyPredicateFactory源码指出,body只允许从request中读取一次,再次读取时会抛异常。因此需要二次封装
  • bodyToMono()方法用于获取body内容,ServerHttpResponseDecorator用于重新封装请求,ServerWebExchange.mutate()方法用于重新生成实例
  • 可以参照ModifyRequestBodyGatewayFilterFactory 和ReadBodyPredicateFactory实现上述需求,本质都是获取并缓存body后二次封装。

参考

posted @ 2022-03-31 10:48  BKYCZJ  阅读(5487)  评论(0编辑  收藏  举报