解决https接口无法访问的问题

问题描述

最近为我司系统接入某第三方服务,假设该第三方服务为W系统,使用https协议对外提供接口,访问W系统接口的时候,收到如下错误:

org.springframework.web.client.ResourceAccessException: I/O error on GET request for "https://open.wwww.com/api/device/status": Received fatal alert: protocol_version; nested exception is javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_version
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:746)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:672)

根据日志提示,可猜测为SSL协议版本问题造成的异常。

问题分析

首先查看一下W系统支持的SSL协议版本

方法一:通过myssl.com网站

输出如下:

可知,W系统仅支持TLSv1.2。

以上工具地址为:SSL/TLS安全评估报告 (myssl.com)

方法二:通过nmap命令

[root@as01-251-81 ~]# nmap --script ssl-enum-ciphers -p 443 open.wwww.com
Starting Nmap 7.70 ( https://nmap.org ) at 2022-06-28 11:34 CST
Nmap scan report for open.figps.com (120.55.18.63)
Host is up (0.010s latency).

PORT    STATE SERVICE
443/tcp open  https
| ssl-enum-ciphers: 
|   TLSv1.2: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (ecdh_x25519) - A
|       TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CCM_8 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CCM (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA256 (dh 2048) - A
|       TLS_DHE_RSA_WITH_AES_128_CBC_SHA (dh 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|_  least strength: A

输出是一致的,也是仅支持TSLv1.2。

通过该命令查看阿里云接口支持的SSL协议:

[root@as01 ~]# nmap --script ssl-enum-ciphers -p 443 dypnsapi.aliyuncs.com
Starting Nmap 7.70 ( https://nmap.org ) at 2022-06-28 11:15 CST
Nmap scan report for dypnsapi.aliyuncs.com (106.11.45.35)
Host is up (0.029s latency).

PORT    STATE SERVICE
443/tcp open  https
| ssl-enum-ciphers: 
|   TLSv1.0: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (ecdh_x25519) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|   TLSv1.1: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (ecdh_x25519) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|   TLSv1.2: 
|     ciphers: 
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256 (ecdh_x25519) - A
|       TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 (ecdh_x25519) - A
|       TLS_RSA_WITH_AES_256_CBC_SHA (rsa 2048) - A
|       TLS_RSA_WITH_AES_128_CBC_SHA (rsa 2048) - A
|     compressors: 
|       NULL
|     cipher preference: server
|_  least strength: A

可以看到,阿里云接口支持多个版本的SSL协议。

接下来看一下客户端支持的协议版本,首先看一下JDK的版本:

[root@as01 ~]# java -version
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)

jdk1.8支持的SSL协议版本:

TLSv1.2 (default)
TLSv1.1
TLSv1
SSLv3

来源:Diagnosing TLS, SSL, and HTTPS (oracle.com)

综上,客户端和服务的协议版本是能够匹配的,为什么会报protocol_version的异常还需要进一步探查。

接下来,通过在应用程序的启动命令中添加-Djavax.net.debug=all 打开网络调试日志,如下所示:

java -Xmx4096m -Xms4096m -jar -Djavax.net.debug=all /alidata0/wwwjava/myapp-dir/jar/myapp-name.jar --spring.profiles.active=prod

观察输出日志,发现在调用goeasy接口之前,对第三方系统调用的SSL协议有多个版本,在goeasy接口调用之后,所有其他第三方的接口调用的SSL协议版本都变成了TLSv1,观察握手部分的ClientHello即可:

*** ClientHello, TLSv1
RandomCookie: GMT: 1530541852 bytes = { 25, 106, 142, 179, 195, 87, 163, 223, 105, 170, 57, 91, 102, 15, 218, 48, 52, 167, 231, 83, 190, 177, 54, 27, 232, 111, 11, 140 }
Session ID: {}

继续看goeasy代码,发现其通过环境变量的方式强制设置了SSL协议版本为TSLv1:

HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
if (PUBLISH_URL.startsWith("https://")) {
System.setProperty("https.protocols", "TLSv1");
TrustManager[] tm = {new PubSubX509TrustManager()};
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();
HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
httpsConn.setSSLSocketFactory(ssf);
}

这个方式太暴力了,这也解释了为什么在解决该问题期间,不管是尝试在http客户端上设置SSL协议版本,还是在Springboot启动的时候,通过环境变量设置SSL协议版本,都是无效的原因。

曾尝试在http客户端工具上设置协议版本:

SSLContext ctx = SSLContexts.custom().useProtocol("TLSv1.2").build();
CloseableHttpClient httpClient = HttpClients.custom().setSSLSocketFactory(new SSLConnectionSocketFactory(ctx)).build();

曾尝试在Springboot启动的时候,通过环境变量设置协议版本:

System.setProperty("https.protocols", "TLSv1.2,TLSv1.1,TLSv1.0,SSLv3");

解决方案

找到问题原因,解决方案就好说了,把goeasy的版本从0.3.16升级到0.4.0,问题就解决了。查看升级之后的goeasy代码:

URL url = new URL(PUBLISH_URL);

HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoInput(true);
conn.setDoOutput(true);
conn.setUseCaches(false);
conn.setRequestMethod("POST");
conn.setRequestProperty("Accept-Charset", "utf-8");
conn.setRequestProperty("contentType", "utf-8");
conn.setConnectTimeout(CONNECT_TIMEOUT);
conn.setReadTimeout(READ_TIMEOUT);
if (PUBLISH_URL.startsWith("https://")) {
    TrustManager[] tm = {new PubSubX509TrustManager()};
    SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
    sslContext.init(null, tm, new java.security.SecureRandom());
    SSLSocketFactory ssf = sslContext.getSocketFactory();
    HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
    httpsConn.setSSLSocketFactory(ssf);
}

已经去掉了设置TSLv1的代码。

https://mvnrepository.com/artifact/io.goeasy/goeasy-sdk

背景知识

SSL是Secure Sockets Layer的缩写,TLS是Transport Layer Security的缩写。

SSL v1.0由于安全问题没有公开发布,SSL v2.0于1995年2月发布,但依然有安全问题,直到1996年的SSL v3.0发布,才修正这些问题。

TLS1.0是SSL v3.0的升级版,是更新、更安全的 SSL 版本,目前市面上所有的https都基于TLS。

以下是SSL/TLS的发布历史:

SSL 1.0 – never publicly released due to security issues.
SSL 2.0 – released in 1995. Deprecated in 2011. Has known security issues.
SSL 3.0 – released in 1996. Deprecated in 2015. Has known security issues.
TLS 1.0 – released in 1999 as an upgrade to SSL 3.0. Planned deprecation in 2020.
TLS 1.1 – released in 2006. Planned deprecation in 2020.
TLS 1.2 – released in 2008.
TLS 1.3 – released in 2018.

SSL和TLS这两个术语可以混用。一般在技术交流中会使用TLS,市场宣传中更多的用SSL,如下:

此条目发表在java/j2ee分类目录。将固定链接加入收藏夹。

发表评论

您的电子邮箱地址不会被公开。

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据