1. 程式人生 > >Java爬蟲(七)- httpClient進階: https 和 證書認證(講故事篇)

Java爬蟲(七)- httpClient進階: https 和 證書認證(講故事篇)

一、前言

本篇風格會偏向講故事,來記錄整個發現問題,解決問題的過程。具體的知識點總結放在後一篇。

前段陣子被分配了一個工單,要求抓取另一個險企B的資料。想著應該不會比上一家A麻煩了,險企A抓取資料過程中有幾次請求是跨域的,很多資料都是由ajax動態請求到的,要分析js程式碼,模擬請求。

稍微觀察了一下險企B的頁面原始碼,發現所有操作除了表單提交,其他都是get請求。而且模擬登入時不需要輸驗證碼。美滋滋。。就是有2點麻煩的地方:

  • 險企B是通過專線訪問的,只有藉助代理公司的網路才能訪問,公司在代理公司那放了臺電腦,然後我在公司遠端連線那臺電腦進行開發的。操作會有延時,有點卡。
  • 險企B的網站看起來很古老,只支援ie8及以下的瀏覽器訪問,chrome、火狐啥的就更打不開了。所以抓包都靠fiddler了,頁面解析元素定位就只能靠舊版本的ie開發工具,

好吧,雖然不便,但是還是不怎麼影響開發過程。

然後在一開始,訪問第一個登入頁面的時候我就被卡住了。我用原來的工具類發了一個get請求去獲取登入頁面,結果報錯了。

二、錯誤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)

一臉懵逼。直覺上是遇到了什麼麻煩的東西。直接去Stack Overflow上面搜尋了。發現有個相同錯誤的問題。

裡面的答案大致就是說,我所要請求的這個server很古老,居然還支援SSLv2協議(還用了個incredibly加強語氣,-_-||)。

2.1 解決方案:

使用SSLConnectionSocketFactory來強制只允許使用TLSv1協議。我的程式碼如下:

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

至於這些安全協議,在下一章會總結。

上述程式碼加進去之後呢。。之前那個錯誤是解決了。然後又出現了新的錯誤。

三、錯誤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

看樣子好像是握手時候失敗了。。又滾去Stack Overflow上去搜索了一下,發現這還是個很火的問題。大家出問題的原因都不一樣,也沒有個綜合的答案。所以我就不放出來了,如果有興趣可以自己去看看。

我的這個問題涉及到了 SSL/TLS 的握手和通訊過程中,安全認證被分為單向認證和雙向認證。這裡面的知識點也很多,具體下一篇總結篇。雙向認證就是說,server也會要求驗證client的證書,而我用Java程式模擬時沒有啟用證書,所以導致認證階段出錯,握手失敗。

3.1 解決方法

相關圖片由於那時候忘了截圖,我直接引用的是參考資料中的。

1、訪問https網站,下載證書

a. 瀏覽器位址列旁邊會有一個鎖的圖示,點選那個鎖,會有檢視證書的按鈕;
b. 將證書資訊匯出,證書格式有很多種,der、cer等,我儲存的是cer格式的

2、利用jdk的toolkey工具,將證書轉換成金鑰的形式

命令列或者shell執行下列命令:

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

之後還需要設定密碼,我直接設定成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)
                // 異常重試機制 3次 (網路層面上的)
                .setRetryHandler(new DefaultHttpRequestRetryHandler(3,true))
                .setDefaultRequestConfig(defaultRequestConfig)
                .build();

然後就解決了。

3.2 SSLHandshake 階段的另一種報錯

Btw,javax.net.ssl.SSLHandshakeException還有一種常見的錯誤:

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

這個就是服務端的證書是不可信的情況。你可以理解為當你用瀏覽器訪問某個網站時,頁面彈出該網站證書不可信的情況。在這裡就是當然這種錯誤我是沒有遇到。解決方法詳見http://zhuyuehua.iteye.com/blog/1102347

訪問https服務其他的常見錯誤

  • java.net.ConnectException: Connection refused: connect 伺服器沒有啟動
  • java.net.SocketException: Software caused connection abort: recv failed
    這是由於服務端配置的是SSL雙向認證,而客戶端傳送資料是按照伺服器是單向認證時傳送的,即沒有將客戶端證書資訊一起傳送給服務端。
  • org.apache.commons.httpclient.NoHttpResponseException 這一般是服務端防火牆的原因。攔截了客戶端請求。另外,當服務端負載過重時,也會出現此問題。將客戶端證書資訊一起傳送給服務端。

參考資料