1.背景
开放平台项目,开发者接入开放平台后,调用开平接口,开平网关需要获取开发者的信息,进而做判断开发者权限、 记录流量信息等操作。
2.问题
开平网关获取开发者信息后,经判断开发者没有权限,想做两个操作 1.返回开发者无权限信息;2.截断之后的filter及向业务服务的调用,如下图所示:
3.互联网解决方案
public static Mono<Void> badReturn(ServerWebExchange exchange, Status statusCode) {
if(statusCode == null) {
statusCode = Status.ERROR_ParaMETERS;
}
//exchange中获取response
ServerHttpResponse response = exchange.getResponse();
//设置response code及header信息
response.setStatusCode(HttpStatus.OK);
HttpHeaders headers = response.getHeaders();
headers= HttpHeaders.writableHttpHeaders(headers);
headers.setContentType(MediaType.APPLICATION_JSON);
//生成返回开发者的信息
String fastResult = returnString(statusCode, "F");
//将string信息写入dataBuffer中
DataBuffer dataBuffer = response.bufferFactory().allocateBuffer().write(fastResult.getBytes(StandardCharsets.UTF_8));
//response写回
return response.writeWith(Mono.just(dataBuffer));
}
或者是此种方法的变体,使用ServerHttpResponseDecorator进行封装,类似这样:
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
List<String> authorization = exchange.getRequest().getHeaders().get("Authorization");
if (CollectionUtils.isEmpty(authorization) &&
!PatternMatchUtils.simpleMatch(URL_WITHOUT_AUTH, exchange.getRequest().getURI().toString())) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
return super.writeWith(fluxBody.map(dataBuffer -> {
// probably should reuse buffers
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
byte[] uppedContent = new String(content, Charset.forName("UTF-8")).toupperCase().getBytes();
return bufferFactory.wrap(uppedContent);
}));
}
return super.writeWith(body); // if body is not a flux. never got there.
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build()); // replace response with decorator
}
String token = authorization.get(0).split(" ")[1];
// token validation
return chain.filter(exchange);
}
3.1互联网方案的局限
我借用了第一种方式,并在项目中实测,确实可以正确的返回开发者信息。虽然可以使用,但在我的项目(gateway版本2.2.8.RELEASE)中产生了两个严重的副作用:1. filter重复执行;2. 第二次执行filter时报错。虽然在重复实行和报错时,因为response已经返回,所以并不会影响调用者,但我觉得此种方式不够完美。
3.2复现说明
我项目中filter链是这样的,其中0和3两个filter是我添加的,其它filter一部分是项目遗留的,用来记录日志,判断内部调用时token的合法性等的filter,另一部分就是gateway自己的filter了。
当执行到index=3时,执行到我自定义的filter
filter首次执行时的情况如下图,可以看到body中是有数据的,经过判断后判定没权限,返回调用者无权限。
调用者确实可以正常收到结果
但继续看后台代码执行过程,诡异的现象出现了。就算此时filter没有截断,也要执行index=4的filter了,但此时,index=1,filter重复执行了,更诡异的是,filter是从index=1执行的,不是index=0的filter。
执行到index=3时,再次执行到我定义的filter。
此时body数据为空,当然也就无权限,再次返回调用者无权限信息
后台发生错误,推测可能是因为过滤器链第一次执行时,response已经返回,链接关闭导致。
思考
这个问题可以分成两部分来看,一部分是response返回后filter没有截断,后续filter和业务服务调用依然要发生,只是在发生后续操作的时候,出现了另一个问题,就是filter重复执行,并且从index=1的filter开始执行,不是index=0的filter。
仔细思考一下,网上的第二种形式应该也是有问题的,它应该也不能完成filter的截断,至于会不会重复执行index=1及之后的filter的情况,我没有实验。
那如何完成filter的截断呢?
抛开gateway,从更高的层面想一下,Spring FrameWork 5使用的响应式编程框架是Project Reactor3(以下简称Reactor),Reactor是怎么处理的呢?它是形如这样的代码:
private void reactorExample() {
Flux.fromIterable(Arrays.asList("a,b,c".split(",")))
.subscribe(result->{
//成功时执行此处
}, error->{
//失败时执行此处
});
}
数据发出后经过Flux或者Mono操作符的处理,处理成功执行result代表的consumer代码块,处理失败执行error代表的errorConsumer代码块。
gateway执行filter时,实际上是在执行consumer,如果想让filter截断,需要让它执行到其它的分支上去,比如errorConsumer代码分支。
最终方案
顺着以上思路,我做了如下三点改动:
1.权限验证失败时,不在使用response回写body数据,而是返回了Mono.error。
2.自定义了GatewayException封装返回给调用方的信息。
3.定义全局异常处理,在异常里返回调用方信息。
亲测问题解决。