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和响应式编程很有意思,做了一个小总结,但对这两部分并不是了解的很透彻,欢迎各位网友批评指正,提供读者学习的动力。