javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

最近在使用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不兼容
  • 客户端和服务器端使用的ssl协议版本不一致。(sslv2, sslv3, tlsv1.0, tlsv1.1, tlsv1.2)
  • 服务器的根证书不在客户端的可信目录中

然而,在尝试了以上各种原因后,依然不行,正要崩溃时,在网上看到了关于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,可以参见参考文档2来暂时规避掉此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

 

转换dmesg中时间戳的方法

如果发现线上tomcat服务器进程突然没有了(可以通过ps aux | grep java来确认),一种可能是进程被内核给干掉了。

此时在命令行执行dmesg,可以看到类似信息:

[84368070.456270] Out of memory: Kill process 15166 (java) score 621 or sacrifice child
[84368070.456304] Killed process 15166, UID 59846, (java) total-vm:4077904kB, anon-rss:2515140kB, file-rss:612kB

这种情况,一般是服务器内存不足,然后内核把占内存最大的进程给杀掉了。

在上面这个信息中,最前面方括号里面的就是杀死进程的时间戳,这个数字84368070.456304的意思代表内核自从启动到现在经过的秒数,小数点后面精确到纳秒。这个数字很不直观,因此,需要转换成可读的时间表示。

方法1:(-T不是所有版本都支持,如果不支持,用方法2手动计算)

dmesg -T

方法2:

转换的思路很简单,[当前时间] – [内核启动后经过的秒数] + [dmesg的时间秒数]

date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+84368070.456304" | bc`seconds"

具体执行时,把84368070.45630换成你dmesg出来的时间戳即可。

也可以把以下代码放到文本文件里,作为校本方便以后使用。

date -d "1970-01-01 UTC `echo "$(date +%s)-$(cat /proc/uptime|cut -f 1 -d' ')+$1" | bc`seconds"

比如存为showDmesgTimestamp.sh,则可以直接执行

sh showDmesgTimestamp.sh 84368070.45630

即可打印可读形式的时间表示。