1. 程式人生 > >網路請求No peer certificate

網路請求No peer certificate

我們在更換伺服器或者轉為https的時候,在進行請求時產生異常

javax.net.ssl.SSLPeerUnverifiedException: No peer certificate
                 W/System.err:     at com.android.org.conscrypt.SSLNullSession.getPeerCertificates(SSLNullSession.java:104)
                 W/System.err:     at org.apache.http.conn.ssl.AbstractVerifier.verify
(AbstractVerifier.java:98) W/System.err: at org.apache.http.conn.ssl.SSLSocketFactory.createSocket(SSLSocketFactory.java:393) W/System.err: at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:189) W/System.err
: at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:169) W/System.err: at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:124) W/System.err: at org.apache.http.impl.client.DefaultRequestDirector
.execute(DefaultRequestDirector.java:365) W/System.err: at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:608) W/System.err: at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:522) W/System.err: at net.tsz.afinal.http.HttpHandler.makeRequestWithRetries(HttpHandler.java:79) W/System.err: at net.tsz.afinal.http.HttpHandler.doInBackground(HttpHandler.java:115) W/System.err: at net.tsz.afinal.core.AsyncTask$2.call(AsyncTask.java:145) W/System.err: at java.util.concurrent.FutureTask.run(FutureTask.java:237) W/System.err: at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1112) W/System.err: at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:587) W/System.err: at java.lang.Thread.run(Thread.java:818)

這個時候查了好幾次,找不到原因,好蛋疼。。。。
後來上谷歌官網查詢
產生原因一:

Protocol    Supported (API Levels)  Enabled by default (API Levels)
SSLv3               1+              1+
TLSv1           1+              1+
TLSv1.1         16+                 20+
TLSv1.2         16+                 20+

在不同的Android版本中,對應的TLS 版本不同,這時候,需要我們去讓後臺查詢伺服器,看是否支援,如果支援,在檢視原生代碼。

解決方法 一:



public class SSL extends SSLSocketFactory {
    private SSLSocketFactory defaultFactory;
    // Android 5.0+ (API level21) provides reasonable default settings
    // but it still allows SSLv3
    // https://developer.android.com/about/versions/android-5.0-changes.html#ssl
    static String protocols[] = null, cipherSuites[] = null;

    static {
        try {
            SSLSocket socket = (SSLSocket) SSLSocketFactory.getDefault().createSocket();
            if (socket != null) {
                /* set reasonable protocol versions */
                // - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
                // - remove all SSL versions (especially SSLv3) because they‘re insecure now
                List<String> protocols = new LinkedList<>();
                for (String protocol : socket.getSupportedProtocols())
                    if (!protocol.toUpperCase().contains("SSL"))
                        protocols.add(protocol);
                SSL.protocols = protocols.toArray(new String[protocols.size()]);
                /* set up reasonable cipher suites */
                if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                    // choose known secure cipher suites
                    List<String> allowedCiphers = Arrays.asList(
                            // TLS 1.2
                            "TLS_RSA_WITH_AES_256_GCM_SHA384",
                            "TLS_RSA_WITH_AES_128_GCM_SHA256",
                            "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
                            "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
                            "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
                            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
                            "TLS_ECHDE_RSA_WITH_AES_128_GCM_SHA256",
                            // maximum interoperability
                            "TLS_RSA_WITH_3DES_EDE_CBC_SHA",
                            "TLS_RSA_WITH_AES_128_CBC_SHA",
                            // additionally
                            "TLS_RSA_WITH_AES_256_CBC_SHA",
                            "TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
                            "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
                            "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
                            "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA");
                    List<String> availableCiphers = Arrays.asList(socket.getSupportedCipherSuites());
                    // take all allowed ciphers that are available and put them into preferredCiphers
                    HashSet<String> preferredCiphers = new HashSet<>(allowedCiphers);
                    preferredCiphers.retainAll(availableCiphers);
                    /* For maximum security, preferredCiphers should *replace* enabled ciphers (thus disabling
                     * ciphers which are enabled by default, but have become unsecure), but I guess for
                     * the security level of DAVdroid and maximum compatibility, disabling of insecure
                     * ciphers should be a server-side task */
                    // add preferred ciphers to enabled ciphers
                    HashSet<String> enabledCiphers = preferredCiphers;
                    enabledCiphers.addAll(new HashSet<>(Arrays.asList(socket.getEnabledCipherSuites())));
                    SSL.cipherSuites = enabledCiphers.toArray(new String[enabledCiphers.size()]);
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public SSL(X509TrustManager tm) {
        try {
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, (tm != null) ? new X509TrustManager[]{tm} : null, null);
            defaultFactory = sslContext.getSocketFactory();
        } catch (GeneralSecurityException e) {
            throw new AssertionError(); // The system has no TLS. Just give up.
        }
    }

    private void upgradeTLS(SSLSocket ssl) {
        // Android 5.0+ (API level21) provides reasonable default settings
        // but it still allows SSLv3
        // https://developer.android.com/about/versions/android-5.0-changes.html#ssl
        if (protocols != null) {
            ssl.setEnabledProtocols(protocols);
        }
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP && cipherSuites != null) {
            ssl.setEnabledCipherSuites(cipherSuites);
        }
    }

    @Override public String[] getDefaultCipherSuites() {
        return cipherSuites;
    }

    @Override public String[] getSupportedCipherSuites() {
        return cipherSuites;
    }

    @Override public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
        Socket ssl = defaultFactory.createSocket(s, host, port, autoClose);
        if (ssl instanceof SSLSocket)
            upgradeTLS((SSLSocket) ssl);
        return ssl;
    }

    @Override public Socket createSocket(String host, int port) throws IOException, UnknownHostException {
        Socket ssl = defaultFactory.createSocket(host, port);
        if (ssl instanceof SSLSocket)
            upgradeTLS((SSLSocket) ssl);
        return ssl;
    }

    @Override public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException, UnknownHostException {
        Socket ssl = defaultFactory.createSocket(host, port, localHost, localPort);
        if (ssl instanceof SSLSocket)
            upgradeTLS((SSLSocket) ssl);
        return ssl;
    }

    @Override public Socket createSocket(InetAddress host, int port) throws IOException {
        Socket ssl = defaultFactory.createSocket(host, port);
        if (ssl instanceof SSLSocket)
            upgradeTLS((SSLSocket) ssl);
        return ssl;
    }

    @Override public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
        Socket ssl = defaultFactory.createSocket(address, port, localAddress, localPort);
        if (ssl instanceof SSLSocket)
            upgradeTLS((SSLSocket) ssl);
        return ssl;
    }
}

然後我們只需要給我們的請求設定這個SSLSocketFactory就可以了,我們以okhttp為例,如下:

//定義一個信任所有證書的TrustManager
final X509TrustManager trustAllCert = new X509TrustManager() {
    @Override
    public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) throws CertificateException {
    }

    @Override
    public java.security.cert.X509Certificate[] getAcceptedIssuers() {
        return new java.security.cert.X509Certificate[]{};
    }
};
//設定OkHttpClient
OkHttpClient client = new OkHttpClient.Builder().sslSocketFactory(new SSL(trustAllCert), trustAllCert).build();

產生原因二:
從異常資訊中,我們可以看出網路請求過程中的握手過程中,握手被中斷了

TCP建立連線時需要三次握手,在釋放連線需要四次揮手;例如三次握手的過程如下:

第一次握手:客戶端傳送syn包(syn=j)到伺服器,並進入SYN_SENT狀態,等待伺服器確認;

第二次握手:伺服器收到syn包,並會確認客戶的SYN(ack=j+1),同時自己也傳送一個SYN包(syn=k),即SYN+ACK包,此時伺服器進入SYN_RECV狀態;

第三次握手:客戶端收到伺服器的SYN+ACK包,向伺服器傳送確認包ACK(ack=k+1),此包傳送完畢,客戶端和伺服器進入ESTABLISHED(TCP連線成功)狀態,完成三次握手。

可以看到握手時會在客戶端和伺服器之間傳遞一些TCP頭資訊,比如ACK標誌、SYN標誌以及揮手時的FIN標誌等。

除了以上這些常見的標誌頭資訊,還有另外一些標誌頭資訊,比如推標誌PSH、復位標誌RST等。其中復位標誌RST的作用就是“復位相應的TCP連線”。

另一個可能導致的“Connection reset”的原因是伺服器設定了Socket.setLinger (true, 0)。但我檢查過線上的tomcat配置,是沒有使用該設定的,而且線上的伺服器都使用了nginx進行反向代理,所以並不是該原因導致的。關於該原因上面的oracle文件也談到了並給出瞭解釋。

此外囉嗦一下,另外還有一種比較常見的錯誤“Connection reset by peer”,該錯誤和“Connection reset”是有區別的:

伺服器返回了“RST”時,如果此時客戶端正在從Socket套接字的輸出流中讀資料則會提示Connection reset”;

伺服器返回了“RST”時,如果此時客戶端正在往Socket套接字的輸入流中寫資料則會提示“Connection reset by peer”。

首先是出錯了重試:這種方案可以簡單防止“Connection reset”錯誤,然後如果服務不是“冪等”的則不能使用該方法;比如提交訂單操作就不是冪等的,如果使用重試則可能造成重複提單。

然後是客戶端和伺服器統一使用TCP長連線:客戶端使用TCP長連線很容易配置(直接設定HttpClient就好),而伺服器配置長連線就比較麻煩了,就拿tomcat來說,需要設定tomcat的maxKeepAliveRequests、connectionTimeout等引數。另外如果使用了nginx進行反向代理或負載均衡,此時也需要配置nginx以支援長連線(nginx預設是對客戶端使用長連線,對伺服器使用短連線)。

使用長連線可以避免每次建立TCP連線的三次握手而節約一定的時間,但是我這邊由於是內網,客戶端和伺服器的3次握手很快,大約只需1ms。ping一下大約0.93ms(一次往返);三次握手也是一次往返(第三次握手不用返回)。根據80/20原理,1ms可以忽略不計;又考慮到長連線的擴充套件性不如短連線好、修改nginx和tomcat的配置代價很大(所有後臺服務都需要修改);所以這裡並沒有使用長連線。ping伺服器的時間如下圖:

最後的解決方案是客戶端和伺服器統一使用TCP短連線:我這邊正是這麼幹的,而使用短連線既不用改nginx配置,也不用改tomcat配置,只需在使用HttpClient時使用http1.0協議並增加http請求的header資訊(Connection: Close),原始碼如下:

httpGet.setProtocolVersion(HttpVersion.HTTP_1_0);
httpGet.addHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_CLOSE);

原因三:

理解這個問題,我試圖找到連線背後的原因復位和我想出了以下原因:

在遠端主機上的對等應用程式突然停止,重新啟動主機,主機或遠端網路介面被禁用,或遠端主機使用硬關閉。
也可能導致這個錯誤,如果一個連線被打破,由於保持活動檢測到故障,而一個或多個操作正在進行中。此時,正在進行的操作失敗和後續操作將失敗。Network dropped connection on reset(On Windows(WSAENETRESET))Connection reset by peer(On Windows(WSAECONNRESET))
如果目標伺服器是由防火牆,這是真正在大多數情況下的保護,生存時間(TTL)或與該埠相關聯的超時強行關閉空閒在給定的超時連線。這是我們關心的事
解析度:

在伺服器端的事件,如突然停止服務,重新啟動,禁用網路介面不能以任何方式處理。
在伺服器端,配置防火牆與較高的生存時間(TTL)或超時值,如3600秒的指定埠。
客戶可以“嘗試”保持網路活躍,以避免或減少Connection reset by peer。
一般情況下要去的網路流量保持連線活著,問題/異常沒有經常看到。的WiFi有至少機會Connection reset by peer。
與所述行動網路的2G,3G和4G,其中該分組資料傳送是間歇的和依賴於行動網路的可用性,它可能無法重置在伺服器側和結果到的TTL計時器Connection reset by peer。
下面是項建議對各種論壇設定來解決問題

ConnectionTimeout:只有在建立連線超時使用。如果主機需要時間來連線這更高的價值,使客戶端等待連線。
SoTimeout:插座超時它說在其內的資料分組被接收到考慮將該連線視為active.If沒有在規定時間內接收到的資料的最大時間,連線被假定為停滯/斷。
Linger:多達什麼時候插座不應該當資料正在排隊等待發送,並且關閉套接字函式被呼叫的插座上關閉。
TcpNoDelay:是否要禁用儲存和積累的TCP資料包緩衝區,並送他們一旦達到閾值?設定為true,將跳過TCP緩衝,使每個請求立即傳送。在網路中可以變慢通過增加由於更小和更頻繁的分組傳輸的網路流量而引起的。
所以沒有上述引數有助於保持網路活著,因而是無效的。

我發現一個設定,可以幫助解決這是這個函式的問題

setKeepAlive(true)
setSoKeepalive(HttpParams params, enableKeepalive=”true”)
我怎麼解決我的問題?

設定 HttpConnectionParams.setSoKeepAlive(params, true)
抓住SSLException和檢查的異常訊息Connection reset by peer
如果發現異常,儲存下載/閱讀進度,並建立一個新的連線。
如果可能的話繼續下載/閱讀否則重新啟動下載