You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring WebFlux WebClient请求出现SSL Tag mismatch异常求助

问题

服务从MVC/Undertow栈迁移至WebFlux/Netty栈后,使用Spring WebClient(原用OkHttp Client)向下游发起HTTPS请求时出现异常。服务基于JDK 17构建,运行在JVM 18环境。生产环境中,服务启动15分钟至120分钟后开始出现SSL错误,且错误量随时间递增;重启服务后错误消失,但一段时间后会再次出现。

抛出的异常如下:

io.netty.handler.codec.DecoderException: javax.net.ssl.SSLException: Tag mismatch
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:480)
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:279)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357)
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379)
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365)
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919)
    at io.netty.channel.epoll.AbstractEpollStreamChannel$EpollStreamUnsafe.epollInReady(AbstractEpollStreamChannel.java:800)
    at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe$1.run(AbstractEpollChannel.java:425)
    at io.netty.util.concurrent.AbstractEventExecutor.runTask(AbstractEventExecutor.java:174)
    at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:167)
    at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:470)
    at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:391)
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:995)
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
    at java.base/java.lang.Thread.run(Thread.java:833)
Caused by: javax.net.ssl.SSLException: Tag mismatch
    at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:133)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:371)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:314)
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:309)
    at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:123)
    at java.base/sun.security.ssl.SSLEngineImpl.decode(SSLEngineImpl.java:736)
    at java.base/sun.security.ssl.SSLEngineImpl.readRecord(SSLEngineImpl.java:691)
    at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:506)
    at java.base/sun.security.ssl.SSLEngineImpl.unwrap(SSLEngineImpl.java:482)
    at java.base/javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:679)
    at io.netty.handler.ssl.SslHandler$SslEngineType$3.unwrap(SslHandler.java:295)
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1342)
    at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1235)
    at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1284)
    at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:510)
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:449)
    ... 18 common frames omitted
Caused by: javax.crypto.AEADBadTagException: Tag mismatch
    at java.base/com.sun.crypto.provider.GaloisCounterMode$GCMDecrypt.doFinal(GaloisCounterMode.java:1591)
    at java.base/com.sun.crypto.provider.GaloisCounterMode.engineDoFinal(GaloisCounterMode.java:454)
    at java.base/javax.crypto.Cipher.doFinal(Cipher.java:2501)
    at java.base/sun.security.ssl.SSLCipher$T12GcmReadCipherGenerator$GcmReadCipher.decrypt(SSLCipher.java:1659)
    at java.base/sun.security.ssl.SSLEngineInputRecord.decodeInputRecord(SSLEngineInputRecord.java:239)
    at java.base/sun.security.ssl.SSLEngineInputRecord.decode(SSLEngineInputRecord.java:196)
    at java.base/sun.security.ssl.SSLEngineInputRecord.decode(SSLEngineInputRecord.java:159)
    at java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:111)
    ... 29 common frames omitted

WebClient配置代码如下:

@Bean
public WebClient webClient(MetricsWebClientCustomizer metricsCustomizer,
                           WebClient.Builder builder,
                           @Value("${s2s.client.read-timeout-ms}") int readTimeoutMs) throws SSLException {
    metricsCustomizer.customize(builder);
    HttpClient httpClient = createNettyHttpClient(readTimeoutMs);
    WebClient webClient = builder
        .clientConnector(new ReactorClientHttpConnector(httpClient))
        .build();

    // warming up web client to allow smooth first requests
    httpClient.warmup().subscribe((ignored) -> log.info("web client initialized"));

    return webClient;
}

private HttpClient createNettyHttpClient(int readTimeoutMs) throws SSLException {
    final ConnectionProvider connectionProvider = ConnectionProvider
        .builder("webclient-connection-provider")
        .maxIdleTime(Duration.ofSeconds(poolConnectionMaxIdleTimeSeconds))
        .maxConnections(maxConnections)
        .pendingAcquireMaxCount(pendingAcquireMaxCount)
        .build();

    return HttpClient.create(connectionProvider)
        .doOnConnected(
            c -> c.addHandlerLast(new ReadTimeoutHandler(readTimeoutMs))
                .addHandlerLast(new WriteTimeoutHandler(readTimeoutMs)))
        .option(CONNECT_TIMEOUT_MILLIS, readTimeoutMs)
        .compress(true)
        .runOn(LoopResources.create("eventloop-webclient", nettyWorkerThreadsCount, true));
}

原因分析与解决办法

核心原因

Tag mismatch属于GCM加密套件的校验失败,本质是连接复用过程中,Netty的SSL Handler与JDK的SSLEngine状态不同步,导致解密时校验标签不通过。结合“错误随时间递增、重启消失”的特征,大概率是:

  • 连接池中的空闲连接被下游服务主动关闭,但客户端未正确感知,复用了已失效的连接;
  • Netty与跨版本JVM(JDK17构建+JVM18运行)之间的SSLEngine存在兼容性问题。

针对性解决措施

  1. 优化连接池回收策略

    • 缩短maxIdleTime:设置为下游服务超时时间的1/2,避免空闲连接被下游关闭后仍留在池中;
    • 添加maxLifeTime:强制连接的最大存活时间,防止长时间复用的连接出现SSL状态异常。示例修改:
      final ConnectionProvider connectionProvider = ConnectionProvider
          .builder("webclient-connection-provider")
          .maxIdleTime(Duration.ofSeconds(30))
          .maxLifeTime(Duration.ofMinutes(5))
          .maxConnections(maxConnections)
          .pendingAcquireMaxCount(pendingAcquireMaxCount)
          .build();
      
  2. 切换SSL Provider至OPENSSL
    优先使用Netty的OPENSSL实现替代JDK默认的SSLEngine,避免JVM版本差异带来的兼容性问题:

    import io.netty.handler.ssl.SslProvider;
    // ...
    return HttpClient.create(connectionProvider)
        .secure(sslContextSpec -> sslContextSpec.sslProvider(SslProvider.OPENSSL))
        .doOnConnected(...)
        // 其他配置...
    

    需引入对应依赖:

    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-tcnative-boringssl-static</artifactId>
        <version>与当前Netty版本适配的版本</version>
    </dependency>
    
  3. 调整超时处理器的添加顺序
    当前把超时处理器添加到handlerLast会导致超时触发时SSL状态未被正确清理,需改为addHandlerFirst:

    .doOnConnected(
        c -> c.addHandlerFirst(new ReadTimeoutHandler(readTimeoutMs))
            .addHandlerFirst(new WriteTimeoutHandler(readTimeoutMs)))
    
  4. 对齐JDK运行版本
    统一服务构建与运行环境为JDK17,避免跨版本JVM带来的底层加密库行为不一致。

  5. 开启连接健康检查与日志排查
    添加后台清理失效连接的逻辑,并开启wiretap日志排查异常连接:

    final ConnectionProvider connectionProvider = ConnectionProvider
        .builder("webclient-connection-provider")
        .maxIdleTime(Duration.ofSeconds(30))
        .maxLifeTime(Duration.ofMinutes(5))
        .maxConnections(maxConnections)
        .pendingAcquireMaxCount(pendingAcquireMaxCount)
        .evictInBackground(Duration.ofSeconds(10))
        .build();
    

    开启wiretap日志:

    return HttpClient.create(connectionProvider)
        .wiretap(true)
        // 其他配置...
    

内容的提问来源于stack exchange,提问作者Asher Nave

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 01:25:31