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

curl请求正常但Kotlin RestTemplate实现SSL握手失败求助

问题:RestTemplate执行带客户端证书的POST请求时出现SSL握手错误

背景

我有一条可正常运行的curl命令,用于获取访问令牌:

curl --request POST --url 'https://myurl' --header 'content-type: application/x-www-form-urlencoded' --data grant_type=client_credentials --data 'client_id=performance-client' --cert mycert.pem --key mykey.pem

尝试用Kotlin的RestTemplate实现相同逻辑时,代码抛出SSL握手错误,无法正常工作。

我的Kotlin代码

fun generateToken(): QuantumKAccessTokenResponse {
    val headers = HttpHeaders()
    headers.contentType = MediaType.APPLICATION_FORM_URLENCODED
    val requestBody: MultiValueMap<String, String> = LinkedMultiValueMap()
    requestBody.add("grant_type", "client_credentials")
    requestBody.add("client_id", "performance-client")
    val requestEntity = HttpEntity(requestBody, headers)
    val client = getHttpsClient()
    val response = client.postForEntity(quantumKUrl, requestEntity, QuantumKAccessTokenResponse::class.java)
    logger.info("Received a ${response.statusCode.value()} response when fetching access token")
    return response.body ?: QuantumKAccessTokenResponse()
}

private fun getHttpsClient(): RestTemplate {
    val keyStore = KeyStore.getInstance("PKCS12")
    keyStore.load(ClassPathResource("qk_certificate.p12").inputStream, null)
    val sslContext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, null)
        .build()

    val client = HttpClientBuilder.create()
        .setSSLContext(sslContext)
        .build()

    return RestTemplate(HttpComponentsClientHttpRequestFactory(client))
}

错误日志

org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://myurl": Remote host terminated the handshake; nested exception is javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake
       org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:791)
       org.springframework.web.client.RestTemplate.execute(RestTemplate.java:717)
       org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:474)
       com.company.QuantumKAccessTokenGenerator.generateToken(QuantumKAccessTokenGenerator.kt:37)
       com.company.QuantumKAccessTokenGeneratorTest.testGenerateToken(QuantumKAccessTokenGeneratorTest.kt:10)
       [...]
     Caused by: javax.net.ssl.SSLHandshakeException: Remote host terminated the handshake
       java.base/sun.security.ssl.SSLSocketImpl.handleEOF(SSLSocketImpl.java:1701)
       java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1519)
       java.base/sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1421)
       java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:456)
       java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:427)
       [...]
       Suppressed: java.net.SocketException: Broken pipe (Write failed)
         java.base/java.net.SocketOutputStream.socketWrite0(Native Method)
         java.base/java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:110)
         java.base/java.net.SocketOutputStream.write(SocketOutputStream.java:150)
         java.base/sun.security.ssl.SSLSocketOutputRecord.encodeAlert(SSLSocketOutputRecord.java:81)
         java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:396)
         [...]
     Caused by: java.io.EOFException: SSL peer shut down incorrectly
       java.base/sun.security.ssl.SSLSocketInputRecord.read(SSLSocketInputRecord.java:489)
       java.base/sun.security.ssl.SSLSocketInputRecord.readHeader(SSLSocketInputRecord.java:478)
       java.base/sun.security.ssl.SSLSocketInputRecord.decode(SSLSocketInputRecord.java:160)
       java.base/sun.security.ssl.SSLTransport.decode(SSLTransport.java:111)
       java.base/sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1511)
       [...]

补充信息

原始私钥和证书为pem格式,我通过以下openssl命令合并为单个p12文件:

openssl pkcs12 -export -inkey key.pem -in cert.pem -out qk_certificate.p12

不确定SSLContext和HttpClient的配置是否正确,恳请协助排查问题。


排查与解决方案

1. 修复PKCS12文件密码加载问题

生成p12文件时若设置了密码,代码中keyStore.load和loadKeyMaterial传入null会导致无法正确加载密钥库。即使未设置密码,也建议显式传入空字符串而非null:

val password = "" // 若生成p12时设了密码,替换为实际密码
keyStore.load(ClassPathResource("qk_certificate.p12").inputStream, password.toCharArray())
val sslContext = SSLContexts.custom()
    .loadKeyMaterial(keyStore, password.toCharArray())
    .build()

2. 确保证书链完整

curl的--cert若使用包含完整证书链的pem文件,而生成p12时仅打包了客户端证书,会导致服务端无法验证证书链。可重新生成p12时加入CA证书:

openssl pkcs12 -export -inkey key.pem -in cert.pem -certfile ca.pem -out qk_certificate.p12

也可在代码中额外加载信任库:

// 加载CA证书到信任库
val trustStore = KeyStore.getInstance(KeyStore.getDefaultType())
trustStore.load(null, null)
val caCert = CertificateFactory.getInstance("X.509")
    .generateCertificate(ClassPathResource("ca.pem").inputStream)
trustStore.setCertificateEntry("ca", caCert)

val sslContext = SSLContexts.custom()
    .loadKeyMaterial(keyStore, password.toCharArray())
    .loadTrustMaterial(trustStore, TrustAllStrategy.INSTANCE)
    .build()

3. 强制指定兼容的TLS版本

部分服务端仅支持特定TLS版本(如TLS 1.2+),可在代码中强制指定:

val sslContext = SSLContexts.custom()
    .loadKeyMaterial(keyStore, password.toCharArray())
    .setProtocol("TLSv1.2") // 或TLSv1.3
    .build()

val sslConnectionSocketFactory = SSLConnectionSocketFactory(
    sslContext,
    arrayOf("TLSv1.2", "TLSv1.3"),
    null,
    SSLConnectionSocketFactory.getDefaultHostnameVerifier()
)

val client = HttpClientBuilder.create()
    .setSSLSocketFactory(sslConnectionSocketFactory)
    .build()

4. 启用SSL调试日志定位差异

添加JVM参数启用SSL调试,查看握手细节:

-Djavax.net.debug=ssl:handshake:verbose

对比curl的curl -v输出,找出两者握手过程的差异点(如支持的加密套件、证书链等)。

5. 对齐信任库配置

curl默认使用系统信任库,而Java使用自带的信任库。若服务端证书为自签名或不在Java默认信任库中,需将CA证书添加到Java信任库,或在代码中加载自定义信任库。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 08:39:54