参考的文章
1. Spring-Cloud-Gateway-实现XSS、SQL注入拦截_gateway xss-CSDN博客
版本
spring-boot-starter-parent: 2.2.13.RELEASE
spring-cloud-dependencies: Hoxton.RELEASE
spring-cloud-alibaba-dependencies: 2.2.6.RELEASE
spring-cloud-starter-gateway: 2.2.0.RELEASE
背景
需要在gateway增加一个Filter,将get、post的参数,进行xss、sql注入检验,拦截有问题的请求。具体操作见参考的文章。
异常信息
生产环境一段时间后,出现Netty的直接内存溢出异常:io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 16777216 byte(s) of direct memory (used: 2063597575, max: 2080374784)
排查:
Netty自己提供了自带的内存检测工具,仅需要配置jvm的启动参数即可,如下:
java -Dio.netty.leakDetectionLevel=paranoid -jar your-application.jar
直接在本地进行简单复现。日志中会显示未进行ByteBuf
释放的地方。
当前问题:
Java 处理 HTTP 请求的 body 时,读取一次后,就无法再读取了。所以在请求拦截后,读取了body的内容,并进行xss检测后,需要使用ServerHttpRequestDecorator 重新构建一个request,供后续的代码读取body内容。
其中比较重要的是,
1.释放 读取涉及直接内存的对象。
2. 新request的body,不再使用toDataBuffer创建。而是使用 bufferFactory().wrap方法,最终使用的ByteBuf为UnpooledHeapByteBuf(非池化堆内存,会被GC回收)
private Mono<Void> handPost(URI uri, ServerWebExchange exchange, ServerHttpRequest serverHttpRequest, HttpMethod method, GatewayFilterChain chain) {
return DataBufferUtils.join(serverHttpRequest.getBody()).flatMap(d -> Mono.just(Optional.of(d))).defaultIfEmpty(
Optional.empty())
.flatMap(optional -> {
// 取出body中的参数
String bodyString = "";
byte[] oldBytes = null;
if (optional.isPresent()) {
oldBytes = new byte[optional.get().readableByteCount()];
optional.get().read(oldBytes);
bodyString = new String(oldBytes, StandardCharsets.UTF_8);
// 释放
DataBufferUtils.release(optional.get());
}
HttpHeaders httpHeaders = serverHttpRequest.getHeaders();
// 执行XSS清理
log.debug("{} - [{}] XSS处理前参数:{}", method, uri.getPath(), bodyString);
bodyString = XssCleanRuleUtils.xssPostClean(bodyString);
log.debug("{} - [{}] XSS处理后参数:{}", method, uri.getPath(), bodyString);// 如果存在sql注入,直接拦截请求
if (bodyString.contains(XssCleanRuleUtils.FORBIDDEN)) {
log.error("{} - [{}] 参数:{}, 包含不允许xss、sql的关键词,请求拒绝", method, uri.getPath(), bodyString);
return returnErrorResponse(exchange);
}ServerHttpRequest newRequest = serverHttpRequest.mutate().uri(uri).build();
if (null == oldBytes) {
oldBytes = "".getBytes(StandardCharsets.UTF_8);
}
// 重新构造body
//byte[] newBytes = bodyString.getBytes(StandardCharsets.UTF_8);
//DataBuffer bodyDataBuffer = toDataBuffer(newBytes);Flux<DataBuffer> bodyFlux = Flux.just(exchange.getResponse().bufferFactory().wrap(oldBytes));
// 重新构造header
HttpHeaders headers = new HttpHeaders();
headers.putAll(httpHeaders);
// 由于修改了传递参数,需要重新设置CONTENT_LENGTH,长度是字节长度,不是字符串长度
int length = oldBytes.length;
headers.remove(HttpHeaders.CONTENT_LENGTH);
headers.setContentLength(length);
headers.set(HttpHeaders.CONTENT_TYPE, "application/json;charset=utf8");
// 重写ServerHttpRequestDecorator,修改了body和header,重写getBody和getHeaders方法
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}@Override
public HttpHeaders getHeaders() {
return headers;
}
};return chain.filter(exchange.mutate().request(newRequest).build());
});
}