目錄
背景
https網(wǎng)絡(luò)協(xié)議交互
net.debug查看信息
Apache Httpclient與 Netty的http請(qǐng)求
?server_name
錯(cuò)誤原因
修復(fù)方案
方案一
方案二
背景
通知某個(gè)商戶(hù)時(shí),突然出現(xiàn)大量的https握手失敗了,出現(xiàn)received fatal alert: internal_error錯(cuò)誤。商戶(hù)sre那邊當(dāng)時(shí)將多個(gè)域名綁定在一個(gè)主機(jī)上,開(kāi)了SNI(Server Name Indication)。
https網(wǎng)絡(luò)協(xié)議交互
https的握手階段圖,如下:
?對(duì)于每一步的解釋如下:
步驟 1: 客戶(hù)端通過(guò)發(fā)送 Client Hello 報(bào)文開(kāi)始 SSL 通信。報(bào)文中包含客戶(hù)端支持的 SSL 的指定版本、加密組件(Cipher Suite)列表(所使用的加密算法及密鑰長(zhǎng)度等)。
步驟 2: 服務(wù)器可進(jìn)行 SSL 通信時(shí),會(huì)以 Server Hello 報(bào)文作為應(yīng)答。和客戶(hù)端一樣,在報(bào)文中包含 SSL 版本以及加密組件。服務(wù)器的加密組件內(nèi)容是從接收到的客戶(hù)端加密組件內(nèi)篩選出來(lái)的。
步驟 3: 之后服務(wù)器發(fā)送 Certificate 報(bào)文。報(bào)文中包含公開(kāi)密鑰證書(shū)。
步驟 4: 最后服務(wù)器發(fā)送 Server Hello Done 報(bào)文通知客戶(hù)端,最初階段的SSL握手協(xié)商部分結(jié)束。
步驟 5: SSL 第一次握手結(jié)束之后,客戶(hù)端以 Client Key Exchange 報(bào)文作為回應(yīng)。報(bào)文中包含通信加密中使用的一種被稱(chēng)為 Pre-master secret 的隨機(jī)密碼串。該報(bào)文已用步驟 3 中的公開(kāi)密鑰進(jìn)行加密。
步驟 6: 接著客戶(hù)端繼續(xù)發(fā)送 Change Cipher Spec 報(bào)文。該報(bào)文會(huì)提示服務(wù)器,在此報(bào)文之后的通信會(huì)采用 Pre-master secret 密鑰加密。
步驟 7: 客戶(hù)端發(fā)送 Finished 報(bào)文。該報(bào)文包含連接至今全部報(bào)文的整體校驗(yàn)值。這次握手協(xié)商是否能夠成功,要以服務(wù)器是否能夠正確解密該報(bào)文作為判定標(biāo)準(zhǔn)。
步驟 8: 服務(wù)器同樣發(fā)送 Change Cipher Spec 報(bào)文。
步驟 9: 服務(wù)器同樣發(fā)送 Finished 報(bào)文。
步驟 10: 服務(wù)器和客戶(hù)端的 Finished 報(bào)文交換完畢之后,SSL 連接就算建立完成。當(dāng)然,通信會(huì)受到 SSL 的保護(hù)。從此處開(kāi)始進(jìn)行應(yīng)用層協(xié)議的通信,即發(fā)送 HTTP請(qǐng)求。
步驟 11: 應(yīng)用層協(xié)議通信,即發(fā)送 HTTP 響應(yīng)。
步驟 12: 最后由客戶(hù)端斷開(kāi)連接。斷開(kāi)連接時(shí),發(fā)送 close_notify 報(bào)文。上圖做了一些省略,這步之后再發(fā)送 TCP FIN 報(bào)文來(lái)關(guān)閉與 TCP 的通信。在以上流程中,應(yīng)用層發(fā)送數(shù)據(jù)時(shí)會(huì)附加一種叫做 MAC(Message Authentication Code)的報(bào)文摘要。MAC 能夠查知報(bào)文是否遭到篡改,從而保護(hù)報(bào)文的完整性。
net.debug查看信息
了解了如上https的交互內(nèi)容,那我們可以強(qiáng)行前進(jìn)了。到底出現(xiàn)在了哪一部分?
在程序啟動(dòng)腳本中我們?cè)黾优渲庙?xiàng):-djavax.net.debug=all
javax.net.debug=[ssl|all]
If ssl, turns on SSL debugging. If all, turns on SSL debugging with verbose messages.
開(kāi)啟了網(wǎng)絡(luò)SSL的debugging,我們看到可客戶(hù)端和服務(wù)端交互的更多細(xì)節(jié),如下:
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請(qǐng)求
Apache Httpclient 請(qǐng)求,建立SSL連接時(shí)握手正常
?
CloseableHttpClient httpClient = HttpClients.custom().build();
String url = "https://test.com";
HttpGet httpGet = new HttpGet(url);
HttpResponse response = httpClient.execute(httpGet);
?
Netty的http異步請(qǐng)求,建立SSL連接時(shí)握手失敗
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);
兩者的錯(cuò)誤日志相比較,Apache httpClient的請(qǐng)求中,擴(kuò)展字段多了個(gè)server_name。
?server_name
根據(jù)文檔TLS擴(kuò)展字段文檔對(duì)server_name的描述,如下圖片:
?如果一臺(tái)服務(wù)器托管多個(gè)域,那么很明顯server_name是對(duì)于每個(gè)域的所有者來(lái)說(shuō),這是必要的,以確保滿(mǎn)足他們的安全需要。
因?yàn)榭蛻?hù)端可以顯示不同的server_name。在應(yīng)用程序協(xié)議中,應(yīng)用程序服務(wù)器實(shí)現(xiàn)了這一點(diǎn),必須檢查這些名稱(chēng)是相同的,以確??蛻?hù)端在應(yīng)用程序協(xié)議中沒(méi)有顯示不同的名稱(chēng)。
?
錯(cuò)誤原因
商戶(hù)那邊將多個(gè)域名綁定在一個(gè)主機(jī)上,開(kāi)了SNI(Server Name Indication)。米幣請(qǐng)求建立ssl連接時(shí),沒(méi)有傳送上server name,而商戶(hù)服務(wù)端檢查了server_name擴(kuò)展,則nginx不知道使用哪個(gè)server,直接拋出異常。
在Client Hello階段,通過(guò)SNI擴(kuò)展,將域名信息提前告訴服務(wù)器,服務(wù)器根據(jù)域名取得對(duì)應(yīng)的證書(shū)返回給客戶(hù)端已完成校驗(yàn)過(guò)程。
導(dǎo)致沒(méi)有SNI的原因
- jdk版本過(guò)低,1.8的低版本和1.7或者1.6,均有可能,參考https://www.codetd.com/article/9814188
- httpclient,4.3.12這個(gè)版本之前的,會(huì)有這個(gè)bug
- 系統(tǒng)類(lèi)設(shè)置了System.setProperty("jsse.enableSNIExtension", "false");
修復(fù)方案
方案一
繼續(xù)使用 下面依賴(lài)的相關(guān)異步請(qǐng)求方法
<dependency>
<groupId>com.ning</groupId>
<artifactId>async-http-client</artifactId>
<version>1.7.14</version>
</dependency>
讓請(qǐng)求攜帶上server_name, 創(chuàng)建特定的asyncHttpClient供該商戶(hù)使用,其他商戶(hù)依舊保持用之前的aysncHttpClient來(lái)調(diào)用。
SSLContext sslcontext = SSLContexts.createSystemDefault();
SSLEngine sslEngine = sslcontext.createSSLEngine("test.com", 443);
// 配置sslEngine在握手時(shí)使用客戶(hù)端模式。
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來(lái)處理
maven依賴(lài)文章來(lái)源:http://www.zghlxwxcb.cn/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>
相關(guān)調(diào)用代碼如下:文章來(lái)源地址http://www.zghlxwxcb.cn/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資源沒(méi)關(guān)閉引起的內(nèi)存泄漏(吃過(guò)這方面的虧,哭唧唧)。診斷由 Apache HttpAsyncClient 引起的內(nèi)存泄漏 - Think different - 生活的美好
批量執(zhí)行完一部分請(qǐng)求后,需要進(jìn)行client.close(); 不然線程會(huì)飆升到幾萬(wàn),然后服務(wù)會(huì)掛掉。 ?
方案三:
System.setProperty("jsse.enableSNIExtension", "true");
到了這里,關(guān)于排查https請(qǐng)求出現(xiàn)received fatal alert: internal_error的問(wèn)題的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!