通过SpringGateway对接口请求进行加解密

通过SpringGateway对接口请求进行加解密

January 9, 2024

有个需求,要求在前端调用接口时,将请求数据与响应数据做加密。做了一下调研,可以用下面的方式来实现:

public class EncryptGatewayFilter implements GlobalFilter, Ordered {

    private static final String BODY_ENCRYPT_HEADER = "X-ENCRYPTED";
    private static final String ENCRYPT_VERSION_1 = "1.0";

    @Override
    public int getOrder() {
        return -99;
    }

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 请求头中标识了该请求是加密过的
        HttpHeaders httpHeaders = exchange.getRequest().getHeaders();
        String bodyEncryptHeader = httpHeaders.getFirst(BODY_ENCRYPT_HEADER);

        if (bodyEncryptHeader != null && bodyEncryptHeader.equals(ENCRYPT_VERSION_1)) {
            // 这里可以处理一些自定义的逻辑,并且将参数传到后续的方法中
            CustomParam step1Param = this.prepareCustomParam();

            // 处理加密的请求
            return processEncryptRequest(exchange, chain, step1Param);
        }

        // 处理未加密的请求
        return chain.filter(exchange);
    }

    private Mono<Void> processEncryptRequest(ServerWebExchange exchange, GatewayFilterChain chain, CustomParam step1Param) {
        // 这里可以做一些前置校验,确定是否需要对请求解密
        boolean needProcess = checkIfNeedProcess(step1Param);

        if (needProcess) {
            try {
                // 这里可以处理 step1Param,并且将生成下个阶段需要用的参数 step2Param, 传到后续的方法中
                CustomParam step2Param = this.handleCustomParam(step1Param);

                // 修改请求内容
                return new ModifiedRequestDecorator(new Config()
                        // 可以移除表示加密的请求头
                        .addHeaderToRemove(BODY_ENCRYPT_HEADER)
                        // 设置 request 重写方法,解密请求
                        .setRewriteFunction(String.class, String.class, (ex, requestData)
                                ->  Mono.just(decryptRequestBody(requestData, step2Param))
                        )).filter(exchange.mutate().response(
                                // 修改响应内容
                                new ModifiedResponseDecorator(exchange, new Config()
                                        // 添加响应头,告知调用者该响应已加密
                                        .addHeaderToAdd(BODY_ENCRYPT_HEADER, ENCRYPT_VERSION_1)
                                        // 设置 response 重写方法,加密响应
                                        .setRewriteFunction(String.class, String.class, (ex, responseData)
                                        ->  Mono.just(encryptResponseBody(responseData, step2Param))
                                ))).build(), chain);

            } catch (Exception e) {
                log.error("Failed to process the request body", e);

                // 处理失败,返回错误的响应
                return buildFailResult(exchange.getResponse(), HttpStatus.BAD_REQUEST);
            }

        }

        // 验证未通过,则不处理,直接将原始请求传到链条中的下一个处理器
        return chain.filter(exchange);
    }

    private String decryptRequestBody(Object originalRequest, CustomParam step2Param) {
        try {
            // 这里需要实现解密的方法
            ...

            return decryptedRequest;
        } catch (Exception e) {
            log.warn("Error when decrypting request.", e);
            return null;
        }
    }

    private String encryptResponseBody(String originalResponse, CustomParam step2Param) {
        try {
            // 这里需要实现加密的方法
            ...

            return oencryptedResponse;
        } catch (Exception e) {
            log.warn("Error when encrypting response.", e);
            return null;
        }
    }
}

其中 ModifiedRequestDecorator 这个类的实现可以参考 org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory。稍微有点不同的是,ModifyRequestBodyGatewayFilterFactory 这个类是作用于 route,而这里需要的是一个全局过滤器。当然,如果不想自己实现,更简便的办法是只做个代理类,直接调用 ModifyRequestBodyGatewayFilterFactory。稍微有点不同的是,ModifyRequestBodyGatewayFilterFactory的方法。详细的细节可以参考: https://blog.csdn.net/cqyhuaming/article/details/105280720

同理,ModifiedResponseDecorator 的实现可以参考 org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory 这类的逻辑。

上面示例代码中 Config 类,也就是 ModifyRequestBodyGatewayFilterFactory 与 ModifyResponseBodyGatewayFilterFactory 中用到的 config 类,可以仿照他们的实现合并成一个。

但是,后续在使用过程中,Spring Cloud Gateway 运行一段时间之后,不时地出现 OutOfDirectMemoryError 的错误。代码里面对于使用 dataBuffer 的地方都特地添加了释放的逻辑,但是没有起到任何作用,问题依然存在。去网上查了一些资料,发现是 gateway 版本的问题,升级到 Hoxton.SR11 之后就解决了。

reactor.netty.ReactorNetty$InternalNettyException: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 32768 byte(s) of direct memory (used: 5222041, max: 5242880)
	Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
	|_ checkpoint ⇢ com.dtstack.gateway.log.RequestLoggingFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.cloud.sleuth.instrument.web.TraceWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
	|_ checkpoint ⇢ HTTP POST &#34;/api/gateway/ky_directMemory_test&#34; [ExceptionHandlingWebHandler]
Stack trace:
Caused by: io.netty.util.internal.OutOfDirectMemoryError: failed to allocate 32768 byte(s) of direct memory (used: 5222041, max: 5242880)
	at io.netty.util.internal.PlatformDependent.incrementMemoryCounter(PlatformDependent.java:754)
	at io.netty.util.internal.PlatformDependent.allocateDirectNoCleaner(PlatformDependent.java:709)
	at io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf.allocateDirect(UnpooledUnsafeNoCleanerDirectByteBuf.java:30)
	at io.netty.buffer.UnpooledDirectByteBuf.&lt;init&gt;(UnpooledDirectByteBuf.java:64)
	at io.netty.buffer.UnpooledUnsafeDirectByteBuf.&lt;init&gt;(UnpooledUnsafeDirectByteBuf.java:41)
	at io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf.&lt;init&gt;(UnpooledUnsafeNoCleanerDirectByteBuf.java:25)
	at io.netty.buffer.UnsafeByteBufUtil.newUnsafeDirectByteBuf(UnsafeByteBufUtil.java:625)
	at io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:381)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:187)
	at io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:178)
	at io.netty.buffer.AbstractByteBufAllocator.ioBuffer(AbstractByteBufAllocator.java:139)
	at io.netty.channel.DefaultMaxMessagesRecvByteBufAllocator$MaxMessageHandle.allocate(DefaultMaxMessagesRecvByteBufAllocator.java:114)
	at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:150)
	at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:714)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:650)
	at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:576)
	at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:493)
	at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:989)
	at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
	at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
	at java.lang.Thread.run(Thread.java:745)

参考链接:

最后更新于