Java爬蟲(chóng)(七)- httpClient進(jìn)階: https 和 證書認(rèn)證(講故事篇)

一、前言

本篇風(fēng)格會(huì)偏向講故事,來(lái)記錄整個(gè)發(fā)現(xiàn)問(wèn)題,解決問(wèn)題的過(guò)程。具體的知識(shí)點(diǎn)總結(jié)放在后一篇。

前段陣子被分配了一個(gè)工單,要求抓取另一個(gè)險(xiǎn)企B的數(shù)據(jù)。想著應(yīng)該不會(huì)比上一家A麻煩了,險(xiǎn)企A抓取數(shù)據(jù)過(guò)程中有幾次請(qǐng)求是跨域的,很多數(shù)據(jù)都是由ajax動(dòng)態(tài)請(qǐng)求到的,要分析js代碼,模擬請(qǐng)求。

稍微觀察了一下險(xiǎn)企B的頁(yè)面源代碼,發(fā)現(xiàn)所有操作除了表單提交,其他都是get請(qǐng)求。而且模擬登錄時(shí)不需要輸驗(yàn)證碼。美滋滋。。就是有2點(diǎn)麻煩的地方:

  • 險(xiǎn)企B是通過(guò)專線訪問(wèn)的,只有借助代理公司的網(wǎng)絡(luò)才能訪問(wèn),公司在代理公司那放了臺(tái)電腦,然后我在公司遠(yuǎn)程連接那臺(tái)電腦進(jìn)行開(kāi)發(fā)的。操作會(huì)有延時(shí),有點(diǎn)卡。
  • 險(xiǎn)企B的網(wǎng)站看起來(lái)很古老,只支持ie8及以下的瀏覽器訪問(wèn),chrome、火狐啥的就更打不開(kāi)了。所以抓包都靠fiddler了,頁(yè)面解析元素定位就只能靠舊版本的ie開(kāi)發(fā)工具,

好吧,雖然不便,但是還是不怎么影響開(kāi)發(fā)過(guò)程。

然后在一開(kāi)始,訪問(wèn)第一個(gè)登錄頁(yè)面的時(shí)候我就被卡住了。我用原來(lái)的工具類發(fā)了一個(gè)get請(qǐng)求去獲取登錄頁(yè)面,結(jié)果報(bào)錯(cuò)了。

二、錯(cuò)誤1

debug:
    Unsupported record version SSLv2Hello
    javax.net.ssl.SSLException: Unsupported record version SSLv2Hello
    at sun.security.ssl.InputRecord.readV3Record(Unknown Source)
    at sun.security.ssl.InputRecord.read(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.startHandshake(Unknown Source)
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.createLayeredSocket(SSLConnectionSocketFactory.java:275)
    at org.apache.http.conn.ssl.SSLConnectionSocketFactory.connectSocket(SSLConnectionSocketFactory.java:254)
    at org.apache.http.impl.conn.HttpClientConnectionOperator.connect(HttpClientConnectionOperator.java:123)
    at org.apache.http.impl.conn.PoolingHttpClientConnectionManager.connect(PoolingHttpClientConnectionManager.java:318)
    at org.apache.http.impl.execchain.MainClientExec.establishRoute(MainClientExec.java:363)
    at org.apache.http.impl.execchain.MainClientExec.execute(MainClientExec.java:219)
    at org.apache.http.impl.execchain.ProtocolExec.execute(ProtocolExec.java:195)
    at org.apache.http.impl.execchain.RetryExec.execute(RetryExec.java:86)
    at org.apache.http.impl.execchain.RedirectExec.execute(RedirectExec.java:108)
    at org.apache.http.impl.client.InternalHttpClient.doExecute(InternalHttpClient.java:184)
    at org.apache.http.impl.client.CloseableHttpClient.execute(CloseableHttpClient.java:82)
    at httpcomponents.httpsTest.main(httpsTest.java:135)

一臉懵逼。直覺(jué)上是遇到了什么麻煩的東西。直接去Stack Overflow上面搜索了。發(fā)現(xiàn)有個(gè)相同錯(cuò)誤的問(wèn)題。

https://stackoverflow.com/questions/26166121/unsupported-record-version-sslv2hello-using-closeablehttpclient

里面的答案大致就是說(shuō),我所要請(qǐng)求的這個(gè)server很古老,居然還支持SSLv2協(xié)議(還用了個(gè)incredibly加強(qiáng)語(yǔ)氣,-_-||)。

2.1 解決方案:

使用SSLConnectionSocketFactory來(lái)強(qiáng)制只允許使用TLSv1協(xié)議。我的代碼如下:

// ssl context
SSLContext sslcontext = SSLContexts.custom().build();
//  ssl socket factory
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[]{"TLSv1"},
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());
// httClient 實(shí)例
CloseableHttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(sslsf)
                //.setDefaultCookieStore(cookieStore)
                // 異常重試機(jī)制 3次 (網(wǎng)絡(luò)層面上的)
                //.setRetryHandler(new DefaultHttpRequestRetryHandler(3,true))
                //.setDefaultRequestConfig(defaultRequestConfig)
                .build();

至于這些安全協(xié)議,在下一章會(huì)總結(jié)。

上述代碼加進(jìn)去之后呢。。之前那個(gè)錯(cuò)誤是解決了。然后又出現(xiàn)了新的錯(cuò)誤。

三、錯(cuò)誤2

Exception in thread "main" javax.net.ssl.SSLHandshakeException: Remote host closed connection during handshake
       at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:946)
       at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1312)
       at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1339)
       at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1323)
       at sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:563)
       at sun.net.www.protocol.https.AbstractDelegateHttpsURLConnection.connect(AbstractDelegateHttpsURLConnection.java:185)
       at sun.net.www.protocol.http.HttpURLConnection.getOutputStream(HttpURLConnection.java:1091)
       at sun.net.www.protocol.https.HttpsURLConnectionImpl.getOutputStream(HttpsURLConnectionImpl.java:250)
       at com.labcorp.efone.vendor.TestATTConnectivity.main(TestATTConnectivity.java:43)
Caused by: java.io.EOFException: SSL peer shut down incorrectly
       at sun.security.ssl.InputRecord.read(InputRecord.java:482)
       at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:927)
       ... 8 more

看樣子好像是握手時(shí)候失敗了。。又滾去Stack Overflow上去搜索了一下,發(fā)現(xiàn)這還是個(gè)很火的問(wèn)題。大家出問(wèn)題的原因都不一樣,也沒(méi)有個(gè)綜合的答案。所以我就不放出來(lái)了,如果有興趣可以自己去看看。

我的這個(gè)問(wèn)題涉及到了 SSL/TLS 的握手和通信過(guò)程中,安全認(rèn)證被分為單向認(rèn)證和雙向認(rèn)證。這里面的知識(shí)點(diǎn)也很多,具體下一篇總結(jié)篇。雙向認(rèn)證就是說(shuō),server也會(huì)要求驗(yàn)證client的證書,而我用Java程序模擬時(shí)沒(méi)有啟用證書,所以導(dǎo)致認(rèn)證階段出錯(cuò),握手失敗。

3.1 解決方法

相關(guān)圖片由于那時(shí)候忘了截圖,我直接引用的是參考資料中的。

1、訪問(wèn)https網(wǎng)站,下載證書

a. 瀏覽器地址欄旁邊會(huì)有一個(gè)鎖的圖標(biāo),點(diǎn)擊那個(gè)鎖,會(huì)有查看證書的按鈕;
b. 將證書信息導(dǎo)出,證書格式有很多種,der、cer等,我保存的是cer格式的

2、利用jdk的toolkey工具,將證書轉(zhuǎn)換成密鑰的形式

命令行或者shell執(zhí)行下列命令:

keytool -import -alias "my alipay cert" -file steven.cert     -keystore my.store,
image

之后還需要設(shè)置密碼,我直接設(shè)置成123456

3、sslContext中載入信用證書


    private static SSLContext sslcontext;
        try {
            sslcontext = SSLContexts.custom()
                    .loadTrustMaterial(new File("D:\\my.keystore"), "123456".toCharArray(),
                            new TrustSelfSignedStrategy())
                    .build();
        } catch (Exception e) {
            e.printStackTrace();
        }
        SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
                sslcontext,
                new String[]{"TLSv1"},
                null,
                SSLConnectionSocketFactory.getDefaultHostnameVerifier());
        httpClient = HttpClients.custom()
                .setSSLSocketFactory(sslsf)
                .setDefaultCookieStore(cookieStore)
                // 異常重試機(jī)制 3次 (網(wǎng)絡(luò)層面上的)
                .setRetryHandler(new DefaultHttpRequestRetryHandler(3,true))
                .setDefaultRequestConfig(defaultRequestConfig)
                .build();

然后就解決了。

3.2 SSLHandshake 階段的另一種報(bào)錯(cuò)

Btw,javax.net.ssl.SSLHandshakeException還有一種常見(jiàn)的錯(cuò)誤:

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed

這個(gè)就是服務(wù)端的證書是不可信的情況。你可以理解為當(dāng)你用瀏覽器訪問(wèn)某個(gè)網(wǎng)站時(shí),頁(yè)面彈出該網(wǎng)站證書不可信的情況。在這里就是當(dāng)然這種錯(cuò)誤我是沒(méi)有遇到。解決方法詳見(jiàn)http://zhuyuehua.iteye.com/blog/1102347

訪問(wèn)https服務(wù)其他的常見(jiàn)錯(cuò)誤

  • java.net.ConnectException: Connection refused: connect 服務(wù)器沒(méi)有啟動(dòng)
  • java.net.SocketException: Software caused connection abort: recv failed
    這是由于服務(wù)端配置的是SSL雙向認(rèn)證,而客戶端發(fā)送數(shù)據(jù)是按照服務(wù)器是單向認(rèn)證時(shí)發(fā)送的,即沒(méi)有將客戶端證書信息一起發(fā)送給服務(wù)端。
  • org.apache.commons.httpclient.NoHttpResponseException 這一般是服務(wù)端防火墻的原因。攔截了客戶端請(qǐng)求。另外,當(dāng)服務(wù)端負(fù)載過(guò)重時(shí),也會(huì)出現(xiàn)此問(wèn)題。將客戶端證書信息一起發(fā)送給服務(wù)端。

參考資料

[1] http://zhuyuehua.iteye.com/blog/1122670

[2] https://blog.csdn.net/wanglha/article/details/51140846

[3] https://hc.apache.org/httpcomponents-client-ga/httpclient/examples/org/apache/http/examples/client/ClientCustomSSL.java

[4] https://blog.csdn.net/liuxiao723846/article/details/52695549

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容