Spring Cloud Gateway(读取、修改 Request Body)的操作
spring cloud gateway(以下简称 scg)做为网关服务,是其他各服务对外中转站,通过 scg 进行请求转发。
在请求到达真正的微服务之前,我们可以在这里做一些预处理,比如:来源合法性检测,权限校验,反爬虫之类…
因为业务需要,我们的服务的请求参数都是经过加密的。
之前是在各个微服务的拦截器里对来解密验证的,现在既然有了网关,自然而然想把这一步骤放到网关层来统一解决。
如果是使用普通的 web 编程中(比如用 zuul),这本就是一个 pre filter 的事儿,把之前 interceptor 中代码搬过来稍微改改就 ok 了。
不过因为使用的 scg,它基于 spring 5 的 webflux,即 reactor 编程,要读取 request body 中的请求参数就没那么容易了。
本篇内容涉及 webflux 的响应式编程及 scg 自定义全局过滤器,如果对这两者不了解的话,可以先看看相关的内容。
两个大坑
我们先建一个 filter 来看看
public class validatefilter implements globalfilter, ordered { @override public mono<void> filter(serverwebexchange exchange, gatewayfilterchain chain) { serverhttprequest request = exchange.getrequest(); httpheaders headers = request.getheaders(); multivaluemap<string, httpcookie> cookies = request.getcookies(); multivaluemap<string, string> queryparams = request.getqueryparams(); flux<databuffer> body = request.getbody(); return null; } @override public int getorder() { return 0; } }
从上边的返回值可以看出,如果是取 header、cookie、query params 都易如反掌,如果你需要校验的数据在这三者之中的话,就没必要往下看了。
说回 body,这里是一个flux<databuffer>,即一个包含 0-n 个databuffer类型元素的异步序列。
首先不考虑 request body 只能读取一次问题(这个问题可以用缓存解决),我们先来把这个 flux 转化成我们可以处理的字符串,第一反应想到的有两个办法:
block() 异步变同步
subscribe() 订阅并触发序列
but,理想很丰满,现实却很骨感——这两个办法都有问题:
webflux 中不能使用阻塞的操作
java.lang.illegalstateexception: block()/blockfirst()/blocklast() are blocking, which is not supported in thread reactor-http-server-epoll-7
subscribe() 只会接收到第一个发出的元素,所以会导致获取不全的问题(太长的 body 会被截断)。这个问题网上有人用 atomicreference<string> 来包装获取到字符串,有人用 stringbuilder/stringbuffer
以上两个问题在网上找了半天,也没找到一个靠谱的解决办法,都是人云亦云。特别是第二个问题的所谓的“解决办法”,大家无非就在是不遗余力的在展示 databuffer 转 string 的 n 种写法,而没有从根本上解决被截断的问题。
正确姿势
2019.08.26 更新:
评论里有网友提醒到 spring cloud gateway 2.1.2 下 defaultserverrequest、cachedbodyoutputmessage 类的访问权限已经改了。这一块我看了一下,源码确实改动了一些,不过 defaultserverrequest 这个类已经不需要了,而 cachedbodyoutputmessage 类我们可以模(chao)仿(xi)它的实现。
其实这里的实现不管再怎么变,我们只要死盯着 modifyrequestbodygatewayfilterfactory 就行了。即使以后这里边的相关类的访问权限都改成 default 了,我们也不用一个个去抄一遍,只要在org.springframework.cloud.gateway.filter.factory.rewrite 这个 package 下写我们自己的类就好了。
———– 分割线 ———-
最终找到解决方案还是通过研读 scg 的源码。
本文使用的版本:
spring cloud: greenwich.rc2
spring boot: 2.1.1.release
在 org.springframework.cloud.gateway.filter.factory.rewrite 包下有个 modifyrequestbodygatewayfilterfactory,顾名思义,这就是修改 request body 的过滤器工厂类。
但是这个类我们无法直接使用,因为要用的话这个 filterfactory 只能用 fluent api 的方式配置,而无法在配置文件中使用,类似于这样
.route("rewrite_request_upper", r -> r.host("*.rewriterequestupper.org") .filters(f -> f.prefixpath("/httpbin") .addresponseheader("x-testheader", "rewrite_request_upper") .modifyrequestbody(string.class, string.class, (exchange, s) -> { return mono.just(s.touppercase()+s.touppercase()); }) ).uri(uri) )
我更喜欢用配置文件来配置路由,所以这种方式并不是我的菜。
这时候我就需要自己弄一个 globalfilter 了。既然官方已经提供了“葫芦”,那么我们就画个“瓢”吧。
如果了解的 gatewayfilterfactory 和 gatewayfilter 的关系的话,不用我说你就知道该怎么办了。不知道也没关系,我们把 modifyrequestbodygatewayfilterfactory 中红框部分 copy 出来,粘贴到我们之前创建的 validatefilter#filter 中
我们稍作修改,即可实现读取并修改 request body 的功能了(核心部分见上图黄色箭头处)
/** * @author yibo */ public class validatefilter implements globalfilter, ordered { @override public mono<void> filter(serverwebexchange exchange, gatewayfilterchain chain) { serverrequest serverrequest = new defaultserverrequest(exchange); // mediatype mediatype mediatype = exchange.getrequest().getheaders().getcontenttype(); // read & modify body mono<string> modifiedbody = serverrequest.bodytomono(string.class) .flatmap(body -> { if (mediatype.application_form_urlencoded.iscompatiblewith(mediatype)) { // origin body map map<string, object> bodymap = decodebody(body); // todo decrypt & auth // new body map map<string, object> newbodymap = new hashmap<>(); return mono.just(encodebody(newbodymap)); } return mono.empty(); }); bodyinserter bodyinserter = bodyinserters.frompublisher(modifiedbody, string.class); httpheaders headers = new httpheaders(); headers.putall(exchange.getrequest().getheaders()); // the new content type will be computed by bodyinserter // and then set in the request decorator headers.remove(httpheaders.content_length); cachedbodyoutputmessage outputmessage = new cachedbodyoutputmessage(exchange, headers); return bodyinserter.insert(outputmessage, new bodyinsertercontext()) .then(mono.defer(() -> { serverhttprequestdecorator decorator = new serverhttprequestdecorator( exchange.getrequest()) { @override public httpheaders getheaders() { long contentlength = headers.getcontentlength(); httpheaders httpheaders = new httpheaders(); httpheaders.putall(super.getheaders()); if (contentlength > 0) { httpheaders.setcontentlength(contentlength); } else { httpheaders.set(httpheaders.transfer_encoding, "chunked"); } return httpheaders; } @override public flux<databuffer> getbody() { return outputmessage.getbody(); } }; return chain.filter(exchange.mutate().request(decorator).build()); })); } @override public int getorder() { return 0; } private map<string, object> decodebody(string body) { return arrays.stream(body.split("&")) .map(s -> s.split("=")) .collect(collectors.tomap(arr -> arr[0], arr -> arr[1])); } private string encodebody(map<string, object> map) { return map.entryset().stream().map(e -> e.getkey() + "=" + e.getvalue()).collect(collectors.joining("&")); } }
至于拿到 body 后具体要做什么,也就上边代码中的todo部分,就由你自己来发挥吧~ 别玩坏就好
建议大家可以多关注关注 scg 的源码,说不定什么时候就会多出一些有用的 filter 或 filterfactory。
另外,目前 modifyrequestbodygatewayfilterfactory 上的 javadoc 有这么一句话:
this filter is beta and may be subject to change in a future release.
所以大家要保持关注呀~
以上这篇spring cloud gateway(读取、修改 request body)的操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持。
推荐阅读
-
记录spring-cloud-gateway获取post请求body参数,以及后端服务处理后的响应参数过程 gateway:2.2.0.RELEASE
-
Spring Cloud Gateway 获取请求体(Request Body)的多种方法
-
SpringCloud gateway request的body验证或修改方式
-
Spring Cloud Gateway(读取、修改 Request Body)的操作
-
解决spring cloud gateway 获取body内容并修改的问题
-
记录spring-cloud-gateway获取post请求body参数,以及后端服务处理后的响应参数过程 gateway:2.2.0.RELEASE
-
Spring Cloud Gateway 获取请求体(Request Body)的多种方法
-
Spring Cloud Gateway(读取、修改 Request Body)的操作
-
SpringCloud gateway request的body验证或修改方式
-
解决spring cloud gateway 获取body内容并修改的问题