目录
背景
https网络协议交互
net.debug查看信息
Apache Httpclient与 Netty的http请求
server_name
错误原因
修复方案
方案一
方案二
背景
通知某个商户时,突然出现大量的https握手失败了,出现received fatal alert: internal_error错误。商户sre那边当时将多个域名绑定在一个主机上,开了SNI(Server Name Indication)。
https网络协议交互
https的握手阶段图,如下:
对于每一步的解释如下:
步骤 1: 客户端通过发送 Client Hello 报文开始 SSL 通信。报文中包含客户端支持的 SSL 的指定版本、加密组件(Cipher Suite)列表(所使用的加密算法及密钥长度等)。
步骤 2: 服务器可进行 SSL 通信时,会以 Server Hello 报文作为应答。和客户端一样,在报文中包含 SSL 版本以及加密组件。服务器的加密组件内容是从接收到的客户端加密组件内筛选出来的。
步骤 3: 之后服务器发送 Certificate 报文。报文中包含公开密钥证书。
步骤 4: 最后服务器发送 Server Hello Done 报文通知客户端,最初阶段的SSL握手协商部分结束。
步骤 5: SSL 第一次握手结束之后,客户端以 Client Key Exchange 报文作为回应。报文中包含通信加密中使用的一种被称为 Pre-master secret 的随机密码串。该报文已用步骤 3 中的公开密钥进行加密。
步骤 6: 接着客户端继续发送 Change Cipher Spec 报文。该报文会提示服务器,在此报文之后的通信会采用 Pre-master secret 密钥加密。
步骤 7: 客户端发送 Finished 报文。该报文包含连接至今全部报文的整体校验值。这次握手协商是否能够成功,要以服务器是否能够正确解密该报文作为判定标准。
步骤 8: 服务器同样发送 Change Cipher Spec 报文。
步骤 9: 服务器同样发送 Finished 报文。
步骤 10: 服务器和客户端的 Finished 报文交换完毕之后,SSL 连接就算建立完成。当然,通信会受到 SSL 的保护。从此处开始进行应用层协议的通信,即发送 HTTP请求。
步骤 11: 应用层协议通信,即发送 HTTP 响应。
步骤 12: 最后由客户端断开连接。断开连接时,发送 close_notify 报文。上图做了一些省略,这步之后再发送 TCP FIN 报文来关闭与 TCP 的通信。在以上流程中,应用层发送数据时会附加一种叫做 MAC(Message Authentication Code)的报文摘要。MAC 能够查知报文是否遭到篡改,从而保护报文的完整性。
net.debug查看信息
了解了如上https的交互内容,那我们可以强行前进了。到底出现在了哪一部分?
在程序启动脚本中我们增加配置项:-djavax.net.debug=all
javax.net.debug=[ssl|all]
If ssl, turns on SSL debugging. If all, turns on SSL debugging with verbose messages.
开启了网络SSL的debugging,我们看到可客户端和服务端交互的更多细节,如下:
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.703 CST|ClientHello.java:567|Produced ClientHello handshake message (
"ClientHello": {
"client version" : "TLSv1.2",
"random" : "F9 D9 EC 33 BD B7 E3 5F B2 0E F8 7B 5E AE 10 F0 FC 6F 89 5A B6 C6 F4 5C 17 EC A4 A6 85 60 CB 75",
"session id" : "",
"cipher suites" : "[TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384(0xC02C), TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256(0xC02B), TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384(0xC030), TLS_RSA_WITH_AES_256_GCM_SHA384(0x009D), TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384(0xC02E), TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384(0xC032), TLS_DHE_RSA_WITH_AES_256_GCM_SHA384(0x009F), TLS_DHE_DSS_WITH_AES_256_GCM_SHA384(0x00A3), TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256(0xC02F), TLS_RSA_WITH_AES_128_GCM_SHA256(0x009C), TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256(0xC02D), TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256(0xC031), TLS_DHE_RSA_WITH_AES_128_GCM_SHA256(0x009E), TLS_DHE_DSS_WITH_AES_128_GCM_SHA256(0x00A2), TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384(0xC024), TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384(0xC028), TLS_RSA_WITH_AES_256_CBC_SHA256(0x003D), TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384(0xC026), TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384(0xC02A), TLS_DHE_RSA_WITH_AES_256_CBC_SHA256(0x006B), TLS_DHE_DSS_WITH_AES_256_CBC_SHA256(0x006A), TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA(0xC00A), TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA(0xC014), TLS_RSA_WITH_AES_256_CBC_SHA(0x0035), TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA(0xC005), TLS_ECDH_RSA_WITH_AES_256_CBC_SHA(0xC00F), TLS_DHE_RSA_WITH_AES_256_CBC_SHA(0x0039), TLS_DHE_DSS_WITH_AES_256_CBC_SHA(0x0038), TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256(0xC023), TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256(0xC027), TLS_RSA_WITH_AES_128_CBC_SHA256(0x003C), TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256(0xC025), TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256(0xC029), TLS_DHE_RSA_WITH_AES_128_CBC_SHA256(0x0067), TLS_DHE_DSS_WITH_AES_128_CBC_SHA256(0x0040), TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA(0xC009), TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA(0xC013), TLS_RSA_WITH_AES_128_CBC_SHA(0x002F), TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA(0xC004), TLS_ECDH_RSA_WITH_AES_128_CBC_SHA(0xC00E), TLS_DHE_RSA_WITH_AES_128_CBC_SHA(0x0033), TLS_DHE_DSS_WITH_AES_128_CBC_SHA(0x0032), TLS_EMPTY_RENEGOTIATION_INFO_SCSV(0x00FF)]",
"compression methods" : "00",
"extensions" : [
"supported_groups (10)": {
"versions": [secp256r1, secp384r1, secp521r1, ffdhe2048, ffdhe3072, ffdhe4096, ffdhe6144, ffdhe8192]
},
"ec_point_formats (11)": {
"formats": [uncompressed]
},
"signature_algorithms (13)": {
"signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
},
"signature_algorithms_cert (50)": {
"signature schemes": [ecdsa_secp256r1_sha256, ecdsa_secp384r1_sha384, ecdsa_secp521r1_sha512, rsa_pss_rsae_sha256, rsa_pss_rsae_sha384, rsa_pss_rsae_sha512, rsa_pss_pss_sha256, rsa_pss_pss_sha384, rsa_pss_pss_sha512, rsa_pkcs1_sha256, rsa_pkcs1_sha384, rsa_pkcs1_sha512, dsa_sha256, ecdsa_sha1, rsa_pkcs1_sha1, dsa_sha1]
},
"extended_master_secret (23)": {
<empty>
},
"supported_versions (43)": {
"versions": [TLSv1.2, TLSv1.1, TLSv1]
}
]
}
)
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.704 CST|SSLEngineOutputRecord.java:505|WRITE: TLS12 handshake, length = 250
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.779 CST|SSLEngineOutputRecord.java:523|Raw write (
0000: 16 03 03 00 FA 01 00 00 F6 03 03 F9 D9 EC 33 BD ..............3.
0010: B7 E3 5F B2 0E F8 7B 5E AE 10 F0 FC 6F 89 5A B6 .._....^....o.Z.
0020: C6 F4 5C 17 EC A4 A6 85 60 CB 75 00 00 56 C0 2C ..\.....`.u..V.,
0030: C0 2B C0 30 00 9D C0 2E C0 32 00 9F 00 A3 C0 2F .+.0.....2...../
0040: 00 9C C0 2D C0 31 00 9E 00 A2 C0 24 C0 28 00 3D ...-.1.....$.(.=
0050: C0 26 C0 2A 00 6B 00 6A C0 0A C0 14 00 35 C0 05 .&.*.k.j.....5..
0060: C0 0F 00 39 00 38 C0 23 C0 27 00 3C C0 25 C0 29 ...9.8.#.'.<.%.)
0070: 00 67 00 40 C0 09 C0 13 00 2F C0 04 C0 0E 00 33 .g.@...../.....3
0080: 00 32 00 FF 01 00 00 77 00 0A 00 12 00 10 00 17 .2.....w........
0090: 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 0B ................
00A0: 00 02 01 00 00 0D 00 22 00 20 04 03 05 03 06 03 .......". ......
00B0: 08 04 08 05 08 06 08 09 08 0A 08 0B 04 01 05 01 ................
00C0: 06 01 04 02 02 03 02 01 02 02 00 32 00 22 00 20 ...........2.".
00D0: 04 03 05 03 06 03 08 04 08 05 08 06 08 09 08 0A ................
00E0: 08 0B 04 01 05 01 06 01 04 02 02 03 02 01 02 02 ................
00F0: 00 17 00 00 00 2B 00 07 06 03 03 03 02 03 01 .....+.........
)
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.791 CST|SSLEngineInputRecord.java:177|Raw read (
0000: 15 03 03 00 02 02 50 ......P
)
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.791 CST|SSLEngineInputRecord.java:214|READ: TLSv1.2 alert, length = 2
javax.net.ssl|FINE|3D|New I/O client worker #4-1|2022-01-23 15:53:10.792 CST|Alert.java:238|Received alert message (
"Alert": {
"level" : "fatal",
"description": "internal_error"
}
)
Apache Httpclient与 Netty的http请求
Apache Httpclient 请求,建立SSL连接时握手正常
CloseableHttpClient httpClient = HttpClients.custom().build();
String url = "https://test.com";
HttpGet httpGet = new HttpGet(url);
HttpResponse response = httpClient.execute(httpGet);
Netty的http异步请求,建立SSL连接时握手失败
private final AsyncHttpClient asyncHttpClient = new AsyncHttpClient(new NettyAsyncHttpProvider(new AsyncHttpClientConfig.Builder().build()));
AsyncHttpClient.BoundRequestBuilder boundRequestBuilder = asyncHttpClient.preparePost(url);
ListenableFuture<Response> future = boundRequestBuilder.execute();
Response response = future.get(5, TimeUnit.SECONDS);
两者的错误日志相比较,Apache httpClient的请求中,扩展字段多了个server_name。
server_name
根据文档TLS扩展字段文档对server_name的描述,如下图片:
如果一台服务器托管多个域,那么很明显server_name是对于每个域的所有者来说,这是必要的,以确保满足他们的安全需要。
因为客户端可以显示不同的server_name。在应用程序协议中,应用程序服务器实现了这一点,必须检查这些名称是相同的,以确保客户端在应用程序协议中没有显示不同的名称。
错误原因
商户那边将多个域名绑定在一个主机上,开了SNI(Server Name Indication)。米币请求建立ssl连接时,没有传送上server name,而商户服务端检查了server_name扩展,则nginx不知道使用哪个server,直接抛出异常。
在Client Hello阶段,通过SNI扩展,将域名信息提前告诉服务器,服务器根据域名取得对应的证书返回给客户端已完成校验过程。
导致没有SNI的原因
- jdk版本过低,1.8的低版本和1.7或者1.6,均有可能,参考https://www.codetd.com/article/9814188
- httpclient,4.3.12这个版本之前的,会有这个bug
- 系统类设置了System.setProperty("jsse.enableSNIExtension", "false");
修复方案
方案一
继续使用 下面依赖的相关异步请求方法
<dependency>
<groupId>com.ning</groupId>
<artifactId>async-http-client</artifactId>
<version>1.7.14</version>
</dependency>
让请求携带上server_name, 创建特定的asyncHttpClient供该商户使用,其他商户依旧保持用之前的aysncHttpClient来调用。
SSLContext sslcontext = SSLContexts.createSystemDefault();
SSLEngine sslEngine = sslcontext.createSSLEngine("test.com", 443);
// 配置sslEngine在握手时使用客户端模式。
sslEngine.setUseClientMode(true);
AsyncHttpClientConfig.Builder builder = new AsyncHttpClientConfig.Builder().setSSLEngineFactory(new SSLEngineFactory() {
@Override
public SSLEngine newSSLEngine() throws GeneralSecurityException {
return sslEngine;
}
});
AsyncHttpClient asyncHttpClient = new AsyncHttpClient(new NettyAsyncHttpProvider(builder.build()));
方案二
用apache的HttpAsyncClient来处理
maven依赖文章来源:https://www.toymoban.com/news/detail-622580.html
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.1.4</version>
</dependency>
相关调用代码如下:文章来源地址https://www.toymoban.com/news/detail-622580.html
CloseableHttpAsyncClient client = HttpAsyncClients.custom()
.setSSLHostnameVerifier(new NoopHostnameVerifier())
.setDefaultRequestConfig(REQUEST_CONFIG)
.build();
HttpPost httpPost = new HttpPost(url);
try {
client.start();
Future<HttpResponse> future = client.execute(httpPost,
new FutureCallback<HttpResponse> {
@Override
public void completed(HttpResponse result) {
}
@Override
public void failed(Exception e) {
}
@Override
public void cancelled() {
}
});
HttpResponse response = future.get(5, TimeUnit.SECONDS);
} catch (Exception e) {
logger.error("httpAysncPost exception, url: {}, message: {}", url, e.getMessage(), e);
}
注意:需要防止HttpAsyncClient资源没关闭引起的内存泄漏(吃过这方面的亏,哭唧唧)。诊断由 Apache HttpAsyncClient 引起的内存泄漏 - Think different - 生活的美好
批量执行完一部分请求后,需要进行client.close(); 不然线程会飙升到几万,然后服务会挂掉。
方案三:
System.setProperty("jsse.enableSNIExtension", "true");
到了这里,关于排查https请求出现received fatal alert: internal_error的问题的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!