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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 11:35:15