2018年11月6日 星期二

HttpClient 連結Https 出現javax.net.ssl.SSLHandshakeException 問題排除


最近在排除一個HTTPS API 連線的問題,當連線時會丟出以下錯誤

javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure
    at sun.security.ssl.Alerts.getSSLException(Unknown Source)
    at sun.security.ssl.Alerts.getSSLException(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.recvAlert(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.readRecord(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(Unknown Source)
    at sun.security.ssl.SSLSocketImpl.writeRecord(Unknown Source)
    at sun.security.ssl.AppOutputStream.write(Unknown Source)
    at java.io.BufferedOutputStream.flushBuffer(Unknown Source)
    at java.io.BufferedOutputStream.flush(Unknown Source)
    at org.apache.commons.httpclient.methods.EntityEnclosingMethod.writeRequestBody(EntityEnclosingMethod.java:506)
    at org.apache.commons.httpclient.HttpMethodBase.writeRequest(HttpMethodBase.java:2114)
    at org.apache.commons.httpclient.HttpMethodBase.execute(HttpMethodBase.java:1096)
    at org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:398)
    at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:171)
    at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
    at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323)


這個在二三年前運行正常,看來應該是目前SSL各Server都修補SSLv3漏洞,目前都僅支援TLS,所以才變成不能使用
經查詢,蠻多人都有遇到此問題,大致問題點是無法進行憑證確認,找了很多方法其實都沒法排除,相關測過的如下,如想要看我的排除法,請直接看第4點即可:
1. 在JVM 啟動時加上  -Dhttps.protocols=TLSv1.2,TLSv1.1,TLSv1.0
首先,該異常發生的時機,是在Client與Server之間進行的Handshake的過程中,Client與Server之間的有效數據傳輸還沒有開始。
原因一:針對上述Handshake的過程,該異常往往發生在第4步,即Client得到服務器的數字證書時,向CA驗證證書有效性時。
Client在試圖向可信CA進行驗證時,發現Server引用的CA,沒有出現在Client的Trust Store中。
原因二:此外,該異常也可能是由於Client與Server所使用的SSL / TLS版本不一致服務器使用的TLS版本高,而Client支持的TLS版本低。
我試了之後沒反應 ,也有照著其它加上 -Djavax.net.debug=all  出來的錯誤訊息和該網友的差不多,但還是不能排除,有另一解法說更新JDK到1.8 但該系統沒全面測過,所以還是作罷。
參考資料:
2. 更新jar  檔
部分網友解釋:是因為JDK中的JCE的安全機制導致錯誤,要去oracle的官網下載對應的JCE包替換JDK中的JCE包。
jce所在地址:%JAVA_HOME%\ jre \ lib \ security裡的local_policy.jar,US_export_policy.jar
JDK7 http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
JDK8 http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
但是我更新後和該網友一樣沒有解決,而他排除的方式是在請求連接之前,加上
System.setProperty(“https.protocols”,“TLSv1.2,TLSv1.1,SSLv3”);
但我試了還是一樣不行
參考資料:
https://blog.csdn.net/gege87417376/article/details/77936507
3.匯入證書簽章
因為turst失敗,所以要將證書匯入到伺服器,先將Server的憑證取回,可利用IE 查看該網站的SSL ,再進行匯出轉匯入,該動作如下:
1.用IE打開需要連接的HTTPS網址,會彈出如下對話框:
2.單擊“查看證書”,在彈出的對話框中選擇“詳細信息”,然後再單擊“複製到文件”,根據提供的嚮導生成待訪問網頁的證書文件
3.嚮導第一步,歡迎界面,直接單擊“下一步”,
4.嚮導第二步,選擇導出的文件格式,默認,單擊“下一步”,
5.嚮導第三步,輸入導出的文件名,輸入後,單擊“下一步”,
6.嚮導第四步,單擊“完成”,完成嚮導
7.最後彈出一個對話框,顯示導出成功
8.用keytool的工具把剛才導出的證書倒入本地keystore.Keytool命令在<java主> \ BIN \下,打開命令行窗口,並到<java主> \ lib \ security中\目錄下,運行下面的命令:
keytool -import -noprompt -keystore cacerts -storepass changeit -alias yourEntry1 -file your.cer
其中參數別名後跟的值是當前證書在密鑰庫中的唯一標識符,但是大小寫不區分;參數文件後跟的是剛才通過IE導出的證書所在的路徑和文件名;如果你想刪除剛才導入到密鑰庫的證書,可以用命令:
keytool -delete -keystore cacerts -storepass changeit -alias yourEntry1
但照做後還是一樣無法排除 ><。
參考資料:
4.最後排除的方式,為採用  httpclient跳過https請求的驗證的方式,實做驗證的方式然後自己加入讓他避掉原本網站要求
我們使用的是httpclient,其版本如下:

<dependency>
  <groupId>commons-httpclient</groupId>
  <artifactId>commons-httpclient</artifactId>
  <version>3.1</version>
</dependency>

接 下來要實作2個class,第一個是MyX509TrustManager(這個方法直接實現X509TrustManager,X509TrustManager在javax.net.ssl.X509TrustManager裡面),這串可以都不用更換即可使用。
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.ssl.X509TrustManager;

public class MyX509TrustManager implements X509TrustManager {
    /* (non-Javadoc)
    * @see javax.net.ssl.X509TrustManager#checkClientTrusted(java.security.cert.X509Certificate[], java.lang.String)
    */
    public void checkClientTrusted(X509Certificate[] arg0, String arg1)
            throws CertificateException {

    }
    /* (non-Javadoc)
     * @see javax.net.ssl.X509TrustManager#checkServerTrusted(java.security.cert.X509Certificate[], java.lang.String)
     */
    public void checkServerTrusted(X509Certificate[] arg0, String arg1)
            throws CertificateException {

    }
    /* (non-Javadoc)
     * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers()
     */
    public X509Certificate[] getAcceptedIssuers() {
        return null;
    }
}
接下來實作MySecureProtocolSocketFactory ,這個我調整了getInstance("SSL"),原本他是採行SSL,但現行伺服端加密方法都因資安考量,只限TLSv1 以上的才可以trust,所以記得要改一下,不然是不能排除問題的,目前在網路上類似的參考案例都是 getInstance("SSL"),所以用了大多會無法排除,要特別注意。
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import org.apache.commons.httpclient.ConnectTimeoutException;
import org.apache.commons.httpclient.HttpClientError;
import org.apache.commons.httpclient.params.HttpConnectionParams;
import org.apache.commons.httpclient.protocol.ControllerThreadSocketFactory;
import org.apache.commons.httpclient.protocol.SecureProtocolSocketFactory;

public class MySecureProtocolSocketFactory implements SecureProtocolSocketFactory {

    //這裡新增一個屬性,主要目的就是來獲取ssl跳過驗證
    private SSLContext sslContext = null;
    /**
     * Constructor for MySecureProtocolSocketFactory.
     */
    public MySecureProtocolSocketFactory() {
    }
    /**
     * 這個建立一個獲取SSLContext的方法,匯入MyX509TrustManager進行初始化
     * @return
     */
    private static SSLContext createEasySSLContext() {
        try {
            SSLContext context = SSLContext.getInstance("TLSv1");
            context.init(null, new TrustManager[] { new MyX509TrustManager() },
                    null);
            return context;
        } catch (Exception e) {
            throw new HttpClientError(e.toString());
        }
    }

    /**
     * 判斷獲取SSLContext
     * @return
     */
    private SSLContext getSSLContext() {
        if (this.sslContext == null) {
            this.sslContext = createEasySSLContext();
        }
        return this.sslContext;
    }
    //後面的方法基本上就是帶入相關引數就可以了
    /*
     * (non-Javadoc)
     *
     * @see org.apache.commons.httpclient.protocol.ProtocolSocketFactory#createSocket(java.lang.String,
     *      int, java.net.InetAddress, int)
     */
    public Socket createSocket(String host, int port, InetAddress clientHost,int clientPort) throws IOException, UnknownHostException {
        return getSSLContext().getSocketFactory().createSocket(host, port,clientHost, clientPort);
    }

    /*
     * (non-Javadoc)
     *
     * @see org.apache.commons.httpclient.protocol.ProtocolSocketFactory#createSocket(java.lang.String,
     *      int, java.net.InetAddress, int,
     *      org.apache.commons.httpclient.params.HttpConnectionParams)
     */
    public Socket createSocket(final String host, final int port,final InetAddress localAddress, final int localPort,
                               final HttpConnectionParams params) throws IOException,UnknownHostException, ConnectTimeoutException {
        if (params == null) {
            throw new IllegalArgumentException("Parameters may not be null");
        }
        int timeout = params.getConnectionTimeout();
        if (timeout == 0) {
            return createSocket(host, port, localAddress, localPort);
        } else {
            return ControllerThreadSocketFactory.createSocket(this, host, port,localAddress, localPort, timeout);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see SecureProtocolSocketFactory#createSocket(java.lang.String,int)
     */
    public Socket createSocket(String host, int port) throws IOException,UnknownHostException {
        return getSSLContext().getSocketFactory().createSocket(host, port);
    }

    /*
     * (non-Javadoc)
     *
     * @see SecureProtocolSocketFactory#createSocket(java.net.Socket,java.lang.String,int,boolean)
     */
    public Socket createSocket(Socket socket, String host, int port,boolean autoClose) throws IOException, UnknownHostException {
        return getSSLContext().getSocketFactory().createSocket(socket, host,port, autoClose);
    }
}
最後,我們在使用前加上即可避掉驗證
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.httpclient.protocol.ProtocolSocketFactory;
/*
 * 利用HttpClient進行post請求的工具類
 */
public class HttpClientUtil {

    public static String doGet(String url) throws Exception {
        //宣告
        ProtocolSocketFactory fcty = new MySecureProtocolSocketFactory();
        //加入相關的https請求方式
        Protocol.registerProtocol("https", new Protocol("https", fcty, 443));
        //傳送請求即可
        org.apache.commons.httpclient.HttpClient httpclient = new org.apache.commons.httpclient.HttpClient();
        GetMethod httpget = new GetMethod(url);
        System.out.println("======url:" + url);
        try {
            httpclient.executeMethod(httpget);
            return httpget.getResponseBodyAsString();
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new Exception(ex.getMessage());
        } finally {
            httpget.releaseConnection();
        }
    }

}
參考資料:

沒有留言:

張貼留言