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
,然後用後者創建和初始化 TrustManager
。TrustManager
是系統用於從服務器驗證證書的工具,可以使用一個或多個 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
,從而使你的應用可與任何人通信。
Android使用https與服務器交互的正確姿勢