SpringCloudGateway(读取、修改RequestBody)的操作
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 = Request();
HttpHeaders headers = Headers();
MultiValueMap<String, HttpCookie> cookies = Cookies();
MultiValueMap<String, String> queryParams = QueryParams();
Flux<DataBuffer> body = Body();
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.write 这个 package 下写我们⾃⼰的类就好了。
———– 分割线 ———-
最终到解决⽅案还是通过研读 SCG 的源码。
本⽂使⽤的版本:
Spring Cloud: Greenwich.RC2
Spring Boot: 2.1.1.RELEASE
在 org.springframework.cloud.gateway.write 包下有个 ModifyRequestBodyGatewayFilterFactory,顾名思义,这就是修改Request Body 的过滤器⼯⼚类。
但是这个类我们⽆法直接使⽤,因为要⽤的话这个 FilterFactory 只能⽤ Fluent API 的⽅式配置,⽽⽆法在配置⽂件中使⽤,类似于这样
.route("rewrite_request_upper", r -> r.host("*.")
.filters(f -> f.prefixPath("/httpbin")
.addResponseHeader("X-TestHeader", "rewrite_request_upper")
.modifyRequestBody(String.class, String.class,
(exchange, s) -> {
return Mono.UpperCase()+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 = Request().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
springcloud和springboot
// new body map
Map<String, Object> newBodyMap = new HashMap<>();
return Mono.just(encodeBody(newBodyMap));
}
pty();
});
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.Request().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
ServerHttpRequestDecorator decorator = new ServerHttpRequestDecorator(
@Override
public HttpHeaders getHeaders() {
long contentLength = ContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.Headers());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
Body();
}
};
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("="))
.Map(arr -> arr[0], arr -> arr[1]));
}
private String encodeBody(Map<String, Object> map) {
Set().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)的操作就是⼩编分享给⼤家的全部内容了,希望能给⼤家⼀个参考,也希望⼤家多多⽀持。