仅在使用 RestTemplate 时出现 SSLHandshakeException

SSLHandshakeException only when using RestTemplate

我正在使用 Java 8,尝试 post https 第三方(其他子域工作),与 postman 一起工作,但使用 RestTemplate 抛出 SSLHandshakeException

new RestTemplate().postForEntity("https://external-host.com" ,new HttpEntity<>(null, new HttpHeaders()), String.class);

我在 jdk1.8.0_151\jre\lib\security\policy\unlimited 文件夹中有 JCE Unlimited jar,我有 bouncy castle bcpkix-jdk15on 和 bcprov-jdk15on 版本 1.55

异常:

org.springframework.web.client.ResourceAccessException: I/O error on POST request for "https://external-host.com": Received fatal alert: handshake_failure; nested exception is javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:785)
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:711)
    at org.springframework.web.client.RestTemplate.postForEntity(RestTemplate.java:468)
...
at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:353)
SSLConnectionSocketFactory.java:353
    at org.apache.http.impl.conn.DefaultHttpClientConnectionOperator.connect(DefaultHttpClientConnectionOperator.java:141)
DefaultHttpClientConnectionOperator.java:141
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:353)
PoolingHttpClientConnectionManager.java:353
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:380)
MainClientExec.java:380
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:236)
MainClientExec.java:236
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:184)
ProtocolExec.java:184
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:88)
RetryExec.java:88
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:110)
RedirectExec.java:110
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
InternalHttpClient.java:184
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
CloseableHttpClient.java:82
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:55)
CloseableHttpClient.java:55
    at org.springframework.http.client.HttpComponentsClientHttpRequest.executeInternal(HttpComponentsClientHttpRequest.java:87)
HttpComponentsClientHttpRequest.java:87
    at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
AbstractBufferingClientHttpRequest.java:48
    at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:66)
AbstractClientHttpRequest.java:66
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:776)
RestTemplate.java:776
    ... 42 more

相同的输出使用其他 solutions 配置 RestTemplate 为:

TrustStrategy acceptingTrustStrategy = (x509Certificates, s) -> true;
SSLContext sslContext = org.apache.http.ssl.SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());

SSL 日志:

Allow unsafe renegotiation: false
Allow legacy hello messages: true
Is initial handshake: true
Is secure renegotiation: false
Ignoring unsupported cipher suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 for TLSv1
Ignoring unsupported cipher suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 for TLSv1
Ignoring unsupported cipher suite: TLS_RSA_WITH_AES_128_CBC_SHA256 for TLSv1
Ignoring unsupported cipher suite: TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 for TLSv1
Ignoring unsupported cipher suite: TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 for TLSv1
Ignoring unsupported cipher suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 for TLSv1
Ignoring unsupported cipher suite: TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 for TLSv1
Ignoring unsupported cipher suite: TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256 for TLSv1.1
Ignoring unsupported cipher suite: TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 for TLSv1.1
Ignoring unsupported cipher suite: TLS_RSA_WITH_AES_128_CBC_SHA256 for TLSv1.1
Ignoring unsupported cipher suite: TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256 for TLSv1.1
Ignoring unsupported cipher suite: TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256 for TLSv1.1
Ignoring unsupported cipher suite: TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 for TLSv1.1
Ignoring unsupported cipher suite: TLS_DHE_DSS_WITH_AES_128_CBC_SHA256 for TLSv1.1
%% No cached client session
*** ClientHello, TLSv1.2
RandomCookie:  GMT: 1645533027 bytes = { 229, 64, 215, 234, 240, 91, 46, 176, 144, 108, 104, 176, 6, 192, 147, 200, 69, 213, 196, 106, 125, 235, 5, 167, 51, 215, 144, 174 }
Session ID:  {}
Cipher Suites: [TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, TLS_RSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_RSA_WITH_AES_128_CBC_SHA256, TLS_DHE_DSS_WITH_AES_128_CBC_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA, TLS_ECDH_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, TLS_RSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256, TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_RSA_WITH_AES_128_GCM_SHA256, TLS_DHE_DSS_WITH_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA, TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods:  { 0 }
Extension elliptic_curves, curve names: {secp256r1, secp384r1, secp521r1, sect283k1, sect283r1, sect409k1, sect409r1, sect571k1, sect571r1, secp256k1}
Extension ec_point_formats, formats: [uncompressed]
Extension signature_algorithms, signature_algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA256withDSA, SHA224withECDSA, SHA224withRSA, SHA224withDSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA
Extension server_name, server_name: [type=host_name (0), value=external-host.com]
***
ajp-nio-8009-exec-32, WRITE: TLSv1.2 Handshake, length = 211
ajp-nio-8009-exec-32, READ: TLSv1.2 Alert, length = 2
ajp-nio-8009-exec-32, RECV TLSv1.2 ALERT:  fatal, handshake_failure
ajp-nio-8009-exec-32, called closeSocket()
ajp-nio-8009-exec-32, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
08:42:43.535 [ajp-nio-8009-exec-32] TRACE o.s.web.servlet.DispatcherServlet - Failed to complete request

EDIT 我从站点下载 cer 文件,添加到 Java 的 cacerts 并创建 p12 文件,并尝试使用以下代码,但是仍然握手异常

KeyStore clientStore = KeyStore.getInstance("PKCS12");
        clientStore.load(new FileInputStream(utils.getStoreProperty("./external.p12")),
                "MYPASS".toCharArray());

        SSLContextBuilder sslContextBuilder = new SSLContextBuilder();
        sslContextBuilder.useProtocol("TLSv1.2");
        sslContextBuilder.loadKeyMaterial(clientStore, "MYPASS".toCharArray());
        TrustStrategy acceptingTrustStrategy = (X509Certificate[] chain, String authType) -> true;
        sslContextBuilder.loadTrustMaterial(null, acceptingTrustStrategy);
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContextBuilder.build());
        CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(sslConnectionSocketFactory)
                .build();
        

openssl s_client -connect host:443 的建议输出如下 @dave_thompson_085 , @yan

WARNING: can't open config file: /usr/local/ssl/openssl.cnf
CONNECTED(000001A4)
depth=3 C = US, O = "The Go Daddy Group, Inc.", OU = Go Daddy Class 2 Certification Authority
verify error:num=19:self signed certificate in certificate chain
---
---
No client certificate CA names sent
Peer signing digest: SHA256
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 5814 bytes and written 433 bytes
---
New, TLSv1/SSLv3, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: A4966A2268EE5CCFA25DEE734DC980D01DF40A4763A4DF3CD19ADA49FF9AD90E
    Session-ID-ctx:
    Master-Key: 58BE6AF39E3DB3CBF3166A286550F2333028E66A9CC59AE886EAA777BAEA82A21D318E89746B97B1BFE0E3E7BF60F5E1
    Key-Arg   : None
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    TLS session ticket lifetime hint: 300 (seconds)
    TLS session ticket:
    0000 - 25 c2 24 26 c2 8f d1 6d-32 5e 2f f6 40 95 af d6   %.$&...m2^/.@...
    0010 - 02 de 28 3e 34 ae 47 96-2c 6a 87 2e 61 e6 fd a2   ..(>4.G.,j..a...
    0020 - 75 3b 3c 3b b2 ee 3c 16-ba e5 49 1c 18 f6 a1 16   u;<;..<...I.....
    0030 - e3 4e 7b 6f 48 5d e7 e3-03 df c5 86 45 81 12 5b   .N{oH]......E..[
    0040 - 4e d7 27 da f5 cd ed 18-6d 41 00 55 7a 8e 62 54   N.'.....mA.Uz.bT
    0050 - 31 75 90 3c aa 2a f9 2b-51 c2 c0 10 5d ca 02 6f   1u.<.*.+Q...]..o
    0060 - 51 0e e0 62 6d 94 12 bd-85 4b 01 88 dc 5d 90 ad   Q..bm....K...]..
    0070 - 30 53 8c 09 a7 01 d9 d7-1b 89 ec 77 35 93 9f ae   0S.........w5...
    0080 - b1 00 c7 ba 1c ea 84 77-36 bf 58 59 7a 78 44 f2   .......w6.XYzxD.
    0090 - 77 55 f4 41 2b dd 3c 54-02 38 ae 37 ec 8a c6 10   wU.A+.<T.8.7....
    00a0 - 6e d5 23 0a 05 5c 19 9f-02 4d 9a 0a 1c 9a be 2e   n.#..\...M......

    Start Time: 1645945411
    Timeout   : 300 (sec)
    Verify return code: 19 (self signed certificate in certificate chain)

EDIT 我关注了@taleb ,但同样的握手异常仍然存在

Add the bcprov-.jar to /usr/lib/jvm/jre/lib/ext

Edit /usr/lib/jvm/jre/lib/security/java.security adding the following line to the list of providers:

security.provider.6=org.bouncycastle.jce.provider.BouncyCastleProvider

(I added it as the 6th entry but you can add higher in the order if you prefer)

重新启动我的应用程序

请注意,您的调试输出中描述的密码套件并未显示 opensslECDHE-RSA-AES256-GCM-SHA384 实际使用的密码套件。事实上,它们不包含任何需要 AES 256 的密码套件。它可能不相关,但它可能是任何配置错误的症状,并且可以解释握手失败的原因。如描述 Java 8 支持的密码套件时的 Oracle documentation 所示:

Cipher suites that use AES_256 require installation of the JCE Unlimited Strength Jurisdiction Policy Files.

因此,请务必安装并正确配置 JCE Unlimited Strength Jurisdiction Policy Files

正如@dave_thompson_085 在他的精彩评论中指出的那样,只有 8u161 以下的 Oracle Java 8 需要添加无限制策略,如上述 Oracle 文档的 Appendix C 所述。

JCE Unlimited Strength Jurisdiction 策略文件被捆绑到 JDK 自 JDK 8u151, but the unlimited policy was not defined as the default one 自 JDK 8u161。

在 JDK 8u151 或 8u152 中,如前面引用链接的 one 中所述,并由 @dave_thompson_085 解释 - 再次非常感谢,以便使 unlimited 版本的 JCE 成为应该使用的版本,您需要定义系统 属性 crypto.policy。来自文档:

This release introduces a new feature whereby the JCE jurisdiction policy files used by the JDK can be controlled via a new Security property. In older releases, JCE jurisdiction files had to be downloaded and installed separately to allow unlimited cryptography to be used by the JDK. The download and install steps are no longer necessary. To enable unlimited cryptography, one can use the new crypto.policy Security property. If the new Security property (crypto.policy) is set in the java.security file, or has been set dynamically by using the Security.setProperty() call before the JCE framework has been initialized, that setting will be honored. By default, the property will be undefined. If the property is undefined and the legacy JCE jurisdiction files don't exist in the legacy lib/security directory, then the default cryptographic level will remain at 'limited'. To configure the JDK to use unlimited cryptography, set the crypto.policy to a value of 'unlimited'. See the notes in the java.security file shipping with this release for more information.

OpenJDK 中不存在该问题。

作为替代解决方案,如 中所建议,使用 BouncyCastle 等替代提供商可能也会有所帮助。