最近在排除一個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();
}
}
}
參考資料: