Spring Boot自动配置Azure EventHub SPN认证SSL握手问题求助
问题
基于Java Spring Boot自动配置的应用,通过SPN认证向Azure EventHub发送消息时遇到SSL握手证书问题,使用连接字符串连接正常,仅SPN认证时出错。
环境信息
Java 17 Spring Boot: solace-spring-boot-starter:2.0.0 Spring Azure: spring-cloud-azure-starter-integration-eventhubs:5.13.0
EventHubProducerService代码
package com.mysample.producer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.azure.messaging.eventhubs.EventData; import com.azure.messaging.eventhubs.EventHubProducerClient; import java.util.Collections; @Service public class EventHubProducerService { @Autowired private EventHubProducerClient producerClient; public void sendEventHubMessage(String message){ producerClient.send(Collections.singletonList(new EventData(message))); log.info("message sent successfully to EventHub"); } }
application.yml配置
spring.cloud.azure: eventhubs: namespace: "<namespaceName>" eventHubName: "<eventHubName>" credential: client-id: "<SPN ClientId>" client-secret: "<SPN SecretValue>" profile: tenant-id: "<TenantId>"
错误日志
2024-07-11T14:57:45.612-04:00 WARN 9632 --- [ctor-http-nio-1] r.netty.http.client.HttpClientConnect : [330ebfae, L:/<ip>:<port> - R:login.microsoftonline.com/<ip>:443] The connection observed an error javax.net.ssl.SSLHandshakeException: General OpenSslEngine problem at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.handshakeException(ReferenceCountedOpenSslEngine.java:1927) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.wrap(ReferenceCountedOpenSslEngine.java:848) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at java.base/javax.net.ssl.SSLEngine.wrap(SSLEngine.java:564) ~[na:na] at io.netty.handler.ssl.SslHandler.wrap(SslHandler.java:1129) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.SslHandler.wrapNonAppData(SslHandler.java:973) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1509) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.SslHandler.decodeNonJdkCompatible(SslHandler.java:1347) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1387) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:530) ~[netty-codec-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:469) ~[netty-codec-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) ~[netty-codec-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1407) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:918) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) ~[netty-transport-4.1.111.Final.jar:4.1.111.Final] at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:994) ~[netty-common-4.1.111.Final.jar:4.1.111.Final] at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.111.Final.jar:4.1.111.Final] at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.111.Final.jar:4.1.111.Final] at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na] Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:439) ~[na:na] at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:306) ~[na:na] at java.base/sun.security.validator.Validator.validate(Validator.java:264) ~[na:na] at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:285) ~[na:na] at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:144) ~[na:na] at io.netty.handler.ssl.EnhancingX509ExtendedTrustManager.checkServerTrusted(EnhancingX509ExtendedTrustManager.java:69) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.ReferenceCountedOpenSslClientContext$ExtendedTrustManagerVerifyCallback.verify(ReferenceCountedOpenSslClientContext.java:235) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.ReferenceCountedOpenSslContext$AbstractCertificateVerifier.verify(ReferenceCountedOpenSslContext.java:797) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.internal.tcnative.CertificateVerifierTask.runTask(CertificateVerifierTask.java:36) ~[netty-tcnative-classes-2.0.65.Final.jar:2.0.65.Final] at io.netty.internal.tcnative.SSLTask.run(SSLTask.java:48) ~[netty-tcnative-classes-2.0.65.Final.jar:2.0.65.Final] at io.netty.internal.tcnative.SSLTask.run(SSLTask.java:42) ~[netty-tcnative-classes-2.0.65.Final.jar:2.0.65.Final] at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.runAndResetNeedTask(ReferenceCountedOpenSslEngine.java:1533) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.ReferenceCountedOpenSslEngine.access$700(ReferenceCountedOpenSslEngine.java:94) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.ReferenceCountedOpenSslEngine$TaskDecorator.run(ReferenceCountedOpenSslEngine.java:1505) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.SslHandler.runDelegatedTasks(SslHandler.java:1649) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1495) ~[netty-handler-4.1.111.Final.jar:4.1.111.Final] ... 21 common frames omitted Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141) ~[na:na] at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126) ~[na:na] at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297) ~[na:na] at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:434) ~[na:na] ... 36 common frames omitted
解决思路
核心原因
错误指向login.microsoftonline.com的SSL证书未被JVM信任。SPN认证需要先向Azure AD的OAuth2端点请求令牌,这个请求的SSL证书不在JVM默认信任存储中;而连接字符串方式直接与EventHub建立连接,无需走Azure AD认证,因此没有这个问题。
具体解决步骤
获取Azure AD根证书
- 访问
https://login.microsoftonline.com,在浏览器中导出网站的根CA证书(通常为DigiCert或Microsoft颁发的根证书),保存为.cer格式。
- 访问
导入证书到JVM全局信任存储
- 找到JDK安装目录下的信任存储文件:Java 17路径为
$JAVA_HOME/lib/security/cacerts。 - 使用
keytool命令导入证书:keytool -import -alias azure-ad-root -file /path/to/your/certificate.cer -keystore $JAVA_HOME/lib/security/cacerts - 默认密码为
changeit,按提示完成导入操作。
- 找到JDK安装目录下的信任存储文件:Java 17路径为
验证证书导入结果
- 运行以下命令确认证书已成功导入:
keytool -list -keystore $JAVA_HOME/lib/security/cacerts -alias azure-ad-root
- 运行以下命令确认证书已成功导入:
替代方案:自定义SSL上下文(无需修改全局信任存储)
- 如果不想修改JVM全局配置,可通过Spring配置自定义SSL上下文,仅让EventHub客户端信任目标证书:
@Configuration public class AzureSslConfig { @Bean public EventHubProducerClientBuilderCustomizer eventHubProducerClientBuilderCustomizer() throws Exception { // 加载本地证书文件 CertificateFactory cf = CertificateFactory.getInstance("X.509"); File certFile = new File("path/to/azure-ad-cert.cer"); X509Certificate cert = (X509Certificate) cf.generateCertificate(new FileInputStream(certFile)); // 构建信任管理器 TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType()); ks.load(null); ks.setCertificateEntry("azure-ad-root", cert); tmf.init(ks); // 创建自定义SSL上下文 SslContext sslContext = SslContextBuilder.forClient() .trustManager(tmf) .build(); // 注入到EventHub客户端构建器 return builder -> builder.httpClient(new NettyAsyncHttpClientBuilder() .sslContext(sslContext) .build()); } }
- 如果不想修改JVM全局配置,可通过Spring配置自定义SSL上下文,仅让EventHub客户端信任目标证书:
排查网络环境
- 若在企业内网环境,需确认是否有代理拦截SSL连接,此时需将企业代理的根证书也导入到信任存储中。
内容的提问来源于stack exchange,提问作者Klam
相关产品推荐
相关产品推荐

