Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android HTTPS詳解

Android HTTPS詳解

編輯:關於Android編程

最近更新

之前寫過一篇使用HttpClient來實現Android平台HTTPS通信的文章,收到很多讀者的私信。悲催的是,私信內容我今天才看見。由於之前是使用HttpClient來實現Android平台的HTTPS通信,但是HttpClient在Android2.2之後就不推薦使用了,所以這裡重寫這篇博客,將所有HTTPS通信代碼改用HttpUrlConnection實現。同時,講解完成後,還會寫一篇文章來講述如何使用Volley來實現HTTPS通信.


HTTPS原理

HTTPS(Hyper Text Transfer Protocol Secure),是一種基於SSL/TLS的HTTP,所有的HTTP數據都是在SSL/TLS協議封裝之上進行傳輸的。HTTPS協議是在HTTP協議的基礎上,添加了SSL/TLS握手以及數據加密傳輸,也屬於應用層協議。所以,研究HTTPS協議原理,最終就是研究SSL/TLS協議。
SSL/TLS協議作用

不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文傳播,帶來了三大風險:

竊聽風險:第三方可以獲知通信內容。篡改風險:第三方可以修改通知內容。冒充風險:第三方可以冒充他人身份參與通信。

SSL/TLS協議是為了解決這三大風險而設計的,希望達到:

所有信息都是加密傳輸,第三方無法竊聽。具有校驗機制,一旦被篡改,通信雙方都會立刻發現。配備身份證書,防止身份被冒充。

基本的運行過程:

SSL/TLS協議的基本思路是采用公鑰加密法,也就是說,客戶端先向服務器端索要公鑰,然後用公鑰加密信息,服務器收到密文後,用自己的私鑰解密。但是這裡需要了解兩個問題的解決方案。

如何保證公鑰不被篡改?
解決方法:將公鑰放在數字證書中。只要證書是可信的,公鑰就是可信的。

公鑰加密計算量太大,如何減少耗用的時間?
解決方法:每一次對話(session),客戶端和服務器端都生成一個“對話密鑰”(session key),用它來加密信息。由於“對話密鑰”是對稱加密,所以運算速度非常快,而服務器公鑰只用於加密“對話密鑰”本身,這樣就減少了加密運算的消耗時間。

因此,SSL/TLS協議的基本過程是這樣的:

客戶端向服務器端索要並驗證公鑰。雙方協商生成“對話密鑰”。雙方采用“對話密鑰”進行加密通信。

上面過程的前兩布,又稱為“握手階段”。

握手階段的詳細過程

“握手階段”涉及四次通信,需要注意的是,“握手階段”的所有通信都是明文的。

客戶端發出請求(ClientHello)

首先,客戶端(通常是浏覽器)先向服務器發出加密通信的請求,這被叫做ClientHello請求。在這一步中,客戶端主要向服務器提供以下信息:

支持的協議版本,比如TLS 1.0版一個客戶端生成的隨機數,稍後用於生成“對話密鑰”。支持的加密方法,比如RSA公鑰加密。支持的壓縮方法。

這裡需要注意的是,客戶端發送的信息之中不包括服務器的域名。也就是說,理論上服務器只能包含一個網站,否則會分不清應用向客戶端提供哪一個網站的數字證書。這就是為什麼通常一台服務器只能有一張數字證書的原因。

服務器回應(ServerHello)

服務器收到客戶端請求後,向客戶端發出回應,這叫做ServerHello。服務器的回應包含以下內容:

確認使用的加密通信協議版本,比如TLS 1.0版本。如果浏覽器與服務器支持的版本不一致,服務器關閉加密通信。一個服務器生成的隨機數,稍後用於生成“對話密鑰”。確認使用的加密方法,比如RSA公鑰加密。服務器證書。

除了上面這些信息,如果服務器需要確認客戶端的身份,就會再包含一項請求,要求客戶端提供“客戶端證書”。比如,金融機構往往只允許認證客戶連入自己的網絡,就會向正式客戶提供USB密鑰,裡面就包含了一張客戶端證書。

客戶端回應

客戶端收到服務器回應以後,首先驗證服務器證書。如果證書不是可信機構頒發,或者證書中的域名與實際域名不一致,或者證書已經過期,就會向訪問者顯示一個警告,由其選擇是否還要繼續通信。
如果證書沒有問題,客戶端就會從證書中取出服務器的公鑰。然後,向服務器發送下面三項消息。

一個隨機數。該隨機數用服務器公鑰加密,防止被竊聽。編碼改變通知,表示隨後的信息都將用雙方商定的加密方法和密鑰發送。客戶端握手結束通知,表示客戶端的握手階段已經結束。這一項通常也是前面發送的所有內容的hash值,用來供服務器校驗。

上面第一項隨機數,是整個握手階段出現的第三個隨機數,又稱“pre-master key”。有了它以後,客戶端和服務器就同時有了三個隨機數,接著雙方就用事先商定的加密方法,各自生成本次會話所用的同一把“會話密鑰”。

服務器的最後回應

服務器收到客戶端的第三個隨機數pre-master key之後,計算生成本次會話所用的“會話密鑰”。然後,向客戶端最後發送下面信息。

編碼改變通知,表示隨後的信息都將用雙方商定的加密方法和密鑰發送。服務器握手結束通知,表示服務器的握手階段已經結束。這一項同時也是前面發生的所有內容的hash值,用來供客戶端校驗。

握手結束

至此,整個握手階段全部結束。接下來,客戶端與服務器進入加密通信,就完全是使用普通的HTTP協議,只不過用“會話密鑰”加密內容。

服務器基於Nginx搭建HTTPS虛擬站點


Android實現HTTPS通信

之前使用了HttpClient來實現HTTPS通信,而且代碼中有大量無關代碼,自己回顧看起來都特別混亂.所以,這裡只列出HttpUrlConnection實現HTTPS通信的關鍵代碼。

CA認證的數字證書網站

我們以百度的https網址(https://m.baidu.com/)為例,示例源碼如下:

public void startHttpsConnection() {
    HttpsURLConnection httpsURLConnection = null;
    BufferedReader reader = null;
    try {
        URL url = new URL("https://m.baidu.com/");
        httpsURLConnection = (HttpsURLConnection) url.openConnection();
        httpsURLConnection.setConnectTimeout(5000);
        httpsURLConnection.setDoInput(true);
        httpsURLConnection.setUseCaches(false);
        httpsURLConnection.connect();

        reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
        StringBuilder sBuilder = new StringBuilder();
        String line;
        while ((line = reader.readLine()) != null) {
            sBuilder.append(line);
        }
        Log.e("TAG", "Wiki content=" + sBuilder.toString());
    } catch (MalformedURLException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (httpsURLConnection != null) {
            httpsURLConnection.disconnect();
        }

        if (reader != null) {
            try {
                reader.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

由於百度是有CA授權的數字證書,所以這裡我們就是簡單的使用HttpsUrlConnection對其進行訪問,就實現了HTTPS通信。

自簽名的數字證書網站

由於CA認證是需要收費的,所以有些網站為了節約成本,采用自簽名的數字證書,偉大的12306目前依然是這麼干的。如果我們用上述代碼訪問自簽名的網站會有什麼問題呢?
截取一段crash信息如下:

04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err: javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.org.conscrypt.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:409)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.Connection.upgradeToTls(Connection.java:153)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.Connection.connect(Connection.java:114)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:298)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpEngine.sendSocketRequest(HttpEngine.java:259)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:206)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:345)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:89)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.android.okhttp.internal.http.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:161)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.genius.wzy.MainActivity.startHttpsConnection(MainActivity.java:58)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at com.genius.wzy.MainActivity$1.run(MainActivity.java:34)
04-13 14:47:30.539 28776-28816/com.genius.wzy W/System.err:     at java.lang.Thread.run(Thread.java:841)

可以看到,訪問自簽名證書的網站,Android直接會throw SSLHandshakeException,原因就是12306的數字證書不被Android系統的信任。想解決這個問題,有如下幾種方法。

讓HttpsURLConnection信任所有的CA證書

這是網上資源最多也是最不靠譜的解決方案。具體實現方法如下。

Step1. 實現X509TrustManager接口,在接口實現中跳過客戶端和服務器端認證。

public class TrustAllCertsManager implements X509TrustManager {
    @Override
    public void checkClientTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        // Do nothing -> accept any certificates
    }

    @Override
    public void checkServerTrusted(X509Certificate[] chain, String authType)
            throws CertificateException {
        // Do nothing -> accept any certificates
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}

Step2. 實現HostnameVerifier接口,不進行url和服務器主機名的驗證。

public class VerifyEverythingHostnameVerifier implements HostnameVerifier {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

Step3. 基於上面實現的TrustAllCertsManager修改HttpsURLConnection類的默認SSL socket factory。

TrustManager[] trustManager = new TrustManager[] {new TrustEverythingTrustManager()};
SSLContext sslContext = null;
try {
    sslContext = SSLContext.getInstance("SSL");
    sslContext.init(null, trustManager, new java.security.SecureRandom());
} catch (NoSuchAlgorithmException e) {
    // do nothing
}catch (KeyManagementException e) {
    // do nothing
}
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());

Setp4. 實例化HttpsUrlConnection,並設置HostnameVerifier為上面實現的VerifyEverythingHostnameVerifier。

httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setHostnameVerifier(new VerifyEverythingHostnameVerifier());

上述四個步驟,就可以讓你無障礙的訪問自簽名的HTTPS網站了,例如12306。但是,這種方式雖然簡單,但是會導致嚴重的安全問題,例如臭名昭著的中間人攻擊

中間人攻擊

雖然上述方案使用了HTTPS,客戶端和服務器端的通信內容得到了加密,嗅探程序無法得到傳輸的內容,但是無法抵擋“中間人攻擊”。例如,在內網配置一個DNS,把目標服務器域名解析到本地的一個地址,然後在這個地址上使用一個中間服務器作為代理,它使用一個假的證書與客戶端通訊,然後再由這個代理服務器作為客戶端連接到實際的服務器,用真的證書與服務器通訊。這樣所有的通訊內容都會經過這個代理,而客戶端不會感知,這是由於客戶端不校驗服務器公鑰證書導致的。

所以,千萬不要在生產代碼中使用上述方法解決HTTPS無法連接的問題。

讓HttpsURLConnection信任指定的CA證書

為了防止上面方案可能導致的“中間人攻擊”,我們可以事先下載服務器端公鑰證書,然後將公鑰證書編譯到Android應用中,由應用自己來驗證證書。也就是我們來教會HttpsUrlConnection來認識特定的自簽名網站。還是以12306網站為例。

Step2. 將下載的證書放到應用的assets目錄下.

app->src->main->assets->srca.cer
(ps:使用Android Studio的同學需要特別注意默認asserts目錄的位置)。

Setp3. 構造特定的TrustManager[]數組.

private TrustManager[] createTrustManager() {
    BufferedInputStream cerInputStream = null;
    try {
        // 獲取客戶端存放的服務器公鑰證書
        cerInputStream = new BufferedInputStream(getAssets().open("srca.cer"));
        // 根據公鑰證書生成Certificate對象
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        Certificate ca = cf.generateCertificate(cerInputStream);
        Log.e("TAG", "ca=" + ((X509Certificate) ca).getSubjectDN());

        // 生成包含當前CA證書的keystore
        KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
        keyStore.load(null, null);
        keyStore.setCertificateEntry("ca", ca);

        // 使用包含指定CA證書的keystore生成TrustManager[]數組
        String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
        tmf.init(keyStore);
        return tmf.getTrustManagers();
    } catch (CertificateException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (KeyStoreException e) {
        e.printStackTrace();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    } finally {
        if (cerInputStream != null) {
            try {
                cerInputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    return null;
}

Step4. 初始化SSLContext.

SSLContext sc = SSLContext.getInstance("SSL");
TrustManager[] trustManagers = createTrustManager();
if (trustManagers == null) {
    Log.e("TAG", "tmf create failed!");
    return;
}
sc.init(null, trustManagers, new SecureRandom());
URL url = new URL("https://kyfw.12306.cn/otn/login/init");
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

 

   
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved