记录 spring cloud gateway 自定义filter使用ServerHttpRequestDecorator 的bug (Netty OutOfDirectMemoryError)

参考的文章

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());
                });
    }

     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值