乐趣区

Spring-Cloud-Gateway-获取request-body基于源码改造不走弯路

在使用 Spring Cloud Gateway 的过程中,经常需要获取 request body,比如用来做日志记录、签名验证、加密解密等等。

网上的资料,解决方案五花八门。所以就整理了经过验证且已经在线上使用的两种方法,都是基于官方源码进行扩展。

本文使用的 Spring Cloud Gateway 版本为 2.1.1.RELEASE。

ModifyRequestBodyGatewayFilterFactory

ModifyRequestBodyGatewayFilterFactory在官方文档中的介绍如下:

This filter can be used to modify the request body before it is sent downstream by the Gateway.

也就是用来修改 request body 的,既然能修改,自然就能获取到。

java 配置

一个简单的配置如下:

@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {return builder.routes()
    .route("rewrite_request_body", r -> r.path("/post_json")
           .filters(f -> f.modifyRequestBody(String.class, String.class, MediaType.APPLICATION_JSON_VALUE, (exchange, s) -> Mono.just(s)))
           .uri("lb://waiter"))
    .build();}

注意 modifyRequestBody 的方法,有四个参数

  • inClass:转换前 request body 的类型。
  • outClass:转换后 request body 的类型。
  • newContentType:转换后的 ContentType。
  • rewriteFunction:改写 body 的方法。这里就可以取到 request body。

需要特别注意的是 inClass 和 outClass 的类型,如果设置得不对,会报错。

小结

优点

  • 不仅可以读取 request body,还可以进行修改。

缺点

  • 如果只需要获取不需要修改,用这个 filter 就显得有点重了。
  • 如果多个 filter 里都需要读取,官方提供的并没有支持缓存,无法一次读多次取。(主要也不是用来干这个的)

ReadBodyPredicateFactory(推荐)

ReadBodyPredicateFactory 是用来读取并判断 request body 是否匹配的谓词。只是在官方文档中都没有说明,但不影响使用。

在代码里有如下注释:

We can only read the body from the request once, once that happens if we try to read the body again an exception will be thrown. The below if/else caches the body object as a request attribute in the ServerWebExchange so if this filter is run more than once (due to more than one route using it) we do not try to read the request body multiple times

大致意思是我们只能从 request 中读取 request body 一次,如果读取过了,再次读取就会抛错。下面的代码把 request body 当作了 request attribute 缓存在 ServerWebExchange 中,如果这个 filter 运行了多次,也无需读取 request body 多次。

java 配置

一个简单的配置如下:

@Autowired
private LogRequestBodyGatewayFilterFactory logRequestBodyGatewayFilterFactory;

@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {return builder.routes()
    .route("rewrite_json", r -> r.path("/post_json")
           .and()
           .readBody(String.class, requestBody -> true)
           .filters(f -> f.filter(logRequestBodyGatewayFilterFactory.apply(new LogRequestBodyGatewayFilterFactory.Config())))
           .uri("lb://waiter"))
    .build();}

注意 readBody 的方法,有两个参数

  • inClass:转换前 request body 的类型,会把 request body 转为这个类型。
  • predicate:谓词判断,如果只是想用做记录,这里可以一直返回 true,如果返回 false,会导致路由匹配不上。

需要特别注意的是 inClass 的类型,如果设置得不对,会报错。

LogRequestBodyGatewayFilterFactory是自定义的一个 GatewayFilterFactory,由于ReadBodyPredicateFactory 会缓存 request body 到 ServerWebExchange,需要用的地方只需要从ServerWebExchange 中获取即可。

@Slf4j
@Component
public class LogRequestBodyGatewayFilterFactory extends
        AbstractGatewayFilterFactory<LogRequestBodyGatewayFilterFactory.Config> {

    private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject";

    public LogRequestBodyGatewayFilterFactory() {super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {return (exchange, chain) -> {String requestBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY);
            log.info(requestBody);
            return chain.filter(exchange);
        };
    }

    public static class Config {}}

小结

优点

  • 一次读多次取。
  • 可以用做谓词判断,只接受指定类型的请求体。

扩展

官方文档中说这两种方式都无法通过配置文件进行处理,这样不是很灵活,很难满足实际的需要。

扩展起来其实也不是很难,只要把无法通过配置文件进行配置的内容屏蔽掉就可以,对于 ModifyRequestBodyGatewayFilterFactoryRewriteFunction,对于 ReadBodyPredicateFactoryPredicate

有两种可行的方案

  • 直接在代码中把逻辑写死,去掉这个配置属性。
  • 通过注入 bean 的方式,把原来基于接口的实现改为 bean 注入。类似于 RequestRateLimiterGatewayFilterFactory 中注入keyResolver

上文中 ReadBodyPredicateFactory 部分写的 java config 和以下配置文件是等效的

spring.cloud.gateway.routes[0].id=rewrite_json
spring.cloud.gateway.routes[0].predicates[0]=Path=/post_json
spring.cloud.gateway.routes[0].predicates[1].name=ReadBodyPredicateFactory
spring.cloud.gateway.routes[0].predicates[1].args.inClass=#{T(String)}
spring.cloud.gateway.routes[0].predicates[1].args.predicate=#{@testPredicate}
spring.cloud.gateway.routes[0].filters[0].name=LogRequestBody
spring.cloud.gateway.routes[0].uri=lb://waiter

需要提供一个 bean,也就是配置文件中的testPredicate

@Component
public class TestPredicate implements Predicate {
    @Override
    public boolean test(Object o) {return true;}
}

主要利用了 SpEL,注入类型和 bean。

未来

目前无论是 ModifyRequestBodyGatewayFilterFactory 还是 ReadBodyPredicateFactory 在 2.1.1.RELEASE 都还是 BETA 版本,但在 2.2.0.RC1 中,这两个类上关于 BETA 版本的注释都已经没了,放心大胆的用吧。

参考

  • Spring Cloud Gateway 2.1.1.RELEASE Reference Doc
  • Spring Cloud(十九):Spring Cloud Gateway(读取、修改 Request Body)
  • SpringCloud Gateway 获取 postrequest body(request body)

看到了这里一定是真爱了,关注微信公众号【憨憨的春天】第一时间获取更新。

退出移动版