Scala+Akka客户端对接Nginx时遭遇SSLHandshakeException问题排查
问题:Scala+Akka客户端SSL握手失败(certificate_unknown)
问题描述
近一周配置Scala+Akka客户端向运行Nginx的服务器发送请求,始终遇到错误:javax.net.ssl.SSLHandshakeException: (certificate_unknown)
环境配置
Nginx配置
server { listen 443 ssl default_server; listen [::]:443 ssl default_server; server_name localhost; ssl_certificate /home/hydra/.localhost-ssl/localhost.crt; //<- 合并证书(server_cert + rootCA) ssl_certificate_key /home/hydra/.localhost-ssl/localhost-key.key; ssl_trusted_certificate /home/hydra/.localhost-ssl/rootCA.crt; //<- 仅根证书rootCA index index.html index.htm; root /home/hydra/ui/; location / { try_files $uri.html $uri/index.html @public @nextjs; add_header Cache-Control "public, max-age=3600"; } location @public { add_header Cache-Control "public, max-age=3600"; } location /ping { proxy_pass http://localhost:8080; } location @nextjs { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto https; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header X-Forwarded-Host $remote_addr; } }
客户端请求代码
private val sslContext: SSLContext = SSLManager.getClientSSLContext private val connectionContext = ConnectionContext.httpsClient(sslContext) ... val request = HttpRequest(method = HttpMethods.GET, uri = s"https://${server.serverIP}/ping") http.singleRequest(request, connectionContext).pipeTo(self)
客户端SSL上下文创建代码
def getClientSSLContext: SSLContext = { val keyStore = KeyStore.getInstance("JKS") keyStore.load(null, null) // 创建空密钥库 keyStore.setCertificateEntry("rootCA", loadRootCertificate()) // 设置信任根CA证书的TrustManager val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) trustManagerFactory.init(keyStore) val trustManagers = trustManagerFactory.getTrustManagers // 使用自定义TrustManager创建SSLContext val sslContext = SSLContext.getInstance("TLS") sslContext.init(null, trustManagers, new SecureRandom()) sslContext }
Root CA证书写入文件代码
val rootCA = new StringBuilder() rootCA.append("-----BEGIN CERTIFICATE-----\n") rootCA.append(Base64.getEncoder.encodeToString(rootCACertificate.getEncoded)) rootCA.append("\n-----END CERTIFICATE-----") writeFile(ROOT_CA_PATH, Seq(rootCA.toString))
服务器证书签名代码
def createCSR(keyPair: KeyPair, subject: String, keyAlgorithm: String): PKCS10CertificationRequest = { val csrGen = new PKCS10CertificationRequestBuilder(new X500Name(subject), SubjectPublicKeyInfo.getInstance(keyPair.getPublic.getEncoded)) val signer = new JcaContentSignerBuilder("SHA256with" + keyAlgorithm).build(keyPair.getPrivate) csrGen.build(signer) } // 使用根CA私钥签名CSR以生成证书 def signCertificate(csr: PKCS10CertificationRequest, rootCACertificate: X509Certificate, rootCAPrivateKey: PrivateKey): X509Certificate = { val notBefore = new Date() val notAfter = new Date(notBefore.getTime + 36500L * 24 * 60 * 60 * 1000) // 有效期1年 val certGen = new X509v3CertificateBuilder( new X500Name("CN=Hydra SSL Certificate"), new BigInteger(128, new Random()), notBefore, notAfter, csr.getSubject, csr.getSubjectPublicKeyInfo ) // 添加SubjectAlternativeName(SAN)扩展 val sanNames = Array[GeneralName]( new GeneralName(GeneralName.iPAddress, SERVER_IP) ) val generalNames = new GeneralNames(sanNames) certGen.addExtension(Extension.subjectAlternativeName, false, generalNames) // 使用根CA私钥签名 val signer = new JcaContentSignerBuilder("SHA256withRSA").build(rootCAPrivateKey) val certificateHolder: X509CertificateHolder = certGen.build(signer) // 转换为JCE证书 val converter = new JcaX509CertificateConverter().setProvider("BC") converter.getCertificate(certificateHolder) } ... // 步骤1:加载根CA证书和私钥 val rootCACertificate = loadRootCertificate() val rootCAPrivateKey = loadRootPrivateKey() // 步骤2:生成SSL证书的新密钥对 val keyPair = generateKey("RSA") // 步骤3:创建CSR(证书签名请求) val csr = createCSR(keyPair, s"CN=hydra_server_$SERVER_ID, O=Hydra, C=UK", "RSA") // 步骤4:使用根CA签名CSR生成SSL证书 val sslCertificate = signCertificate(csr, rootCACertificate, rootCAPrivateKey)
已尝试操作
执行openssl s_client -connect 192.168.0.4:443 -showcerts,确认Nginx返回了完整且顺序正确的证书链(根CA证书生成于2025年7月2日,服务器证书为当日生成)。
排查方向
- 服务器证书与请求地址不匹配:客户端通过IP地址发起请求,需确认服务器证书的SAN扩展中
SERVER_IP变量是否准确设置为192.168.0.4,且格式为纯IP字符串(无端口)。Java SSL验证中,IP访问必须依赖SAN中的IP条目,Subject中的CN不会被用于IP地址匹配。 - Root CA证书加载异常:检查
loadRootCertificate()方法是否正确读取了目标路径下的Root CA证书,证书内容是否为完整的PEM格式(无多余字符或换行错误)。可添加日志打印加载后的证书信息,确认其Issuer、Subject等字段与签发服务器证书的根CA一致。 - TrustManager配置未生效:在客户端SSL上下文创建代码中,添加日志输出
trustManagers中的信任证书列表,确认Root CA已被正确加入信任列表。同时检查Akka HTTP的connectionContext是否被正确传入singleRequest方法,未被默认配置覆盖。 - BouncyCastle Provider未注册:服务器证书使用BouncyCastle生成,客户端JVM需注册该Provider才能正确解析证书。在客户端初始化代码中添加
Security.addProvider(new BouncyCastleProvider()),并确保依赖中包含BouncyCastle库。 - 证书链验证逻辑问题:对比服务器证书的Issuer与Root CA的Subject,确认两者完全一致(包括所有字段)。若根CA证书是自签名的,需确保其被标记为CA证书(具备
basicConstraints = CA:TRUE扩展)。
内容的提问来源于stack exchange,提问作者Kris Rice
相关产品推荐
相关产品推荐

