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
相关产品推荐
相关产品推荐

