最近在使用Java的HttpsURLConnection来访问https网站时,抛出了一个异常: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

代码大致如下:

URL url = new URL("https://www.google.com/");
HttpsURLConnection connection = (HttpsURLConnection)url.openConnection();
connection.setHostnameVerifier((t, v) -> true);
BufferedInputStream inputStream = new BufferedInputStream(connection.getInputStream());
int respInt = inputStream.read();
while (respInt != -1) {
respInt = inputStream.read();
}
inputStream.close();

网上查了下,发现很多情况都可能导致抛出这个异常,一种可行的排查方法是,在执行Java程序时,加 -Djavax.net.debug=all 选项打开SSL连接时的debug开关,这样就会把建立连接的信息打印到控制台。

这样执行程序时可以在控制台打印类似的消息(部分摘取):

keyStore is :
keyStore type is : jks
keyStore provider is :
init keystore
init keymanager of type SunX509
trustStore is: /Library/Java/JavaVirtualMachines/jdk1.8.0_66.jdk/Contents/Home/jre/lib/security/cacerts
trustStore type is : jks
trustStore provider is :
init truststore
adding as trusted cert:
......
*** ClientHello, TLSv1.2
RandomCookie:  GMT: 1527255937 bytes = { 72, 31, 116, 116, 76, 130, 134, 23, 59, 194, 6, 185, 160, 110, 14, 131, 74, 175, 192, 56, 83, 130, 176, 102, 53, 50, 126, 139 }
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, ...]
Compression Methods:  { 0 }
Extension elliptic_curves, curve names: {secp256r1, sect163k1, sect163r2, secp192r1, secp224r1, sect233k1, sect233r1, sect283k1, sect283r1, secp384r1, sect409k1, ...}
Extension ec_point_formats, formats: [uncompressed]
Extension signature_algorithms, signature_algorithms: SHA512withECDSA, SHA512withRSA, SHA384withECDSA, SHA384withRSA, SHA256withECDSA, SHA256withRSA, SHA224withECDSA, SHA224withRSA, SHA1withECDSA, SHA1withRSA, SHA1withDSA, MD5withRSA
***
......
[Raw read]: length = 5
0000: 15 03 03 00 02                                     .....
[Raw read]: length = 2
0000: 02 28                                              .(
main, READ: TLSv1.2 Alert, length = 2
main, RECV TLSv1.2 ALERT:  fatal, handshake_failure
main, called closeSocket()
main, handling exception: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

也就是在clientHello阶段,服务器端返回了握手失败。而这一步握手失败有很多原因:

  • 客户端和服务器端的cipher suites不兼容</li>
  • 客户端和服务器端使用的ssl协议版本不一致。(sslv2, sslv3, tlsv1.0, tlsv1.1, tlsv1.2)</li>
  • 服务器的根证书不在客户端的可信目录中</li>

然而,在尝试了以上各种原因后,依然不行,正要崩溃时,在网上看到了关于JDK bug导致此错误的案例。

简单来说就是,JDK1.8的特定版本,如果自定义HttpsURLConnection的HostnameVerifier(类似上面的代码中的connection.setHostnameVerifier((t, v) -> true);)会导致握手的ClientHello阶段,client不发送SNI extension。(注意,可能你的代码中没有自定义,但是可能依赖的jar包中有自定义的操作)

正常情况下在ClientHello阶段,会发送:

Extension server_name, server_name: [type=host_name (0), value=www.google.com]

而在上面的出错的信息中,没有这个extension。

解决办法,升级到JDK1.8的8u152版本或者以上,或者降低到u66版本以下。

另外,如果不升级JDK,可以参见(https://stackoverflow.com/questions/41692736/all-trusting-hostnameverifier-causes-ssl-errors-with-httpurlconnection)来暂时规避掉此BUG.

另外,不是所有的网站都会出现这个问题,理论上来说与SNI相关的网站出问题的可能比较大。关于SNI(Server Name Indication),可以参考下:(https://en.wikipedia.org/wiki/Server_Name_Indication)

参考文档:

1)介绍了这类问题的参考思路:

https://stackoverflow.com/questions/6353849/received-fatal-alert-handshake-failure-through-sslhandshakeexception?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa

2)分析并解决了我这种情况的问题:

https://stackoverflow.com/questions/41692736/all-trusting-hostnameverifier-causes-ssl-errors-with-httpurlconnection

3)JDK bug详情:

https://bugs.java.com/view_bug.do?bug_id=JDK-8144566