1. 程式人生 > >Android使用https與伺服器互動的正確姿勢

Android使用https與伺服器互動的正確姿勢

HTTPS 使用 SSL 在客戶端和伺服器之間進行加密通訊,錯誤地使用 SSL ,將會導致其它人能夠攔截網路上的應用資料。

使用一個包含公鑰及與其匹配的私鑰的證書配置伺服器,作為 SSL 客戶端與伺服器握手的一部分,伺服器將通過使用公鑰加密簽署其證書來證明自己具有私鑰。

主機平臺一般包含其信任的知名 CA 的列表。從 Android 4.2 開始,Android 包含在每個版本中更新的 100 多個 CA。CA 具有一個證書和一個私鑰,為伺服器發放證書時,CA 使用其私鑰簽署伺服器證書。然後,客戶端可以驗證該伺服器是否具有平臺已知的 CA 發放的證書。

如果擁有一個知名 CA 發放證書的伺服器,那麼可以用以下程式碼直接發起 HTTPS 請求

URL url = new URL("https://www.cnblogs.com");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

對!就是這麼簡單。 Android 會對驗證證書和主機名做處理,你不用考慮這些細節。

如果驗證伺服器證書出現 SSLHandshakeException 異常,那麼原因可能是頒發伺服器證書的 CA 是 Android 系統未知的,或者是自簽署的伺服器證書。

為了解決證書驗證失敗的問題,我們可以使用自定義的 TrustManager 使 HttpsURLConnection 信任特定的 CA 。

// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));
Certificate ca;
try {
    ca = cf.generateCertificate(caInput);
    System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} finally {
    caInput.close();
}

// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);

// Tell the URLConnection to use a SocketFactory from our SSLContext
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =
    (HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

InputStream 獲取一個特定的 CA,用該 CA 建立 KeyStore,然後用後者建立和初始化 TrustManagerTrustManager 是系統用於從伺服器驗證證書的工具,可以使用一個或多個 CA 從 KeyStore 建立,而建立的 TrustManager 將僅信任這些 CA。

很多網站和部落格介紹一種非常糟糕的解決方案來通過驗證

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{new X509TrustManager() {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
        
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {

    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}}, null);
URLConnection urlConnection = url.openConnection();
urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

使用一個沒有任何作用的 TrustManager。這樣做等同於沒加密通訊,因為任何人都可以在公共 WLAN 熱點下,使用偽裝成伺服器的代理髮送資料,通過 DNS 欺騙攻擊使用者。然後,攻擊者可以記錄密碼和其他個人資料。此方法之所以有效是因為攻擊者可以生成一個證書,且沒有可以真正驗證證書是否來自值得信任的來源的 TrustManager,從而使你的應用可與任何人通訊。