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存在兼容性问题。
针对性解决措施
优化连接池回收策略
- 缩短
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();
- 缩短
切换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>调整超时处理器的添加顺序
当前把超时处理器添加到handlerLast会导致超时触发时SSL状态未被正确清理,需改为addHandlerFirst:.doOnConnected( c -> c.addHandlerFirst(new ReadTimeoutHandler(readTimeoutMs)) .addHandlerFirst(new WriteTimeoutHandler(readTimeoutMs)))对齐JDK运行版本
统一服务构建与运行环境为JDK17,避免跨版本JVM带来的底层加密库行为不一致。开启连接健康检查与日志排查
添加后台清理失效连接的逻辑,并开启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
相关产品推荐
相关产品推荐

