淺析Android系統中HTTPS通訊的實現_java

來源:互聯網
上載者:User

前言
最近有一個跟HTTPS相關的問題需要解決,因此花時間學習了一下Android平台HTTPS的使用,同時也看了一些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通訊,就是不加密的通訊,所有的資訊明文傳播,帶來了三大風險:
1. 竊聽風險:第三方可以獲知通訊內容。
2. 篡改風險:第三方可以修改通知內容。
3. 冒充風險:第三方可以冒充他人身份參與通訊。
SSL/TLS協議是為瞭解決這三大風險而設計的,希望達到:
1. 所有資訊都是加密傳輸,第三方無法竊聽。
2. 具有校正機制,一旦被篡改,通訊雙方都會立刻發現。
3. 配備身份認證,防止身份被冒充。
基本的運行過程
SSL/TLS協議的基本思路是採用公開金鑰加密法,也就是說,用戶端先向伺服器端索要公開金鑰,然後用公開金鑰加密資訊,伺服器收到密文後,用自己的私密金鑰解密。但是這裡需要瞭解兩個問題的解決方案。
1. 如何保證公開金鑰不被篡改?
解決方案:將公開金鑰放在數位憑證中。只要認證是可信的,公開金鑰就是可信的。
2. 公開金鑰加密計算量太大,如何減少耗用的時間?
解決方案:每一次對話(session),用戶端和伺服器端都產生一個“對話密鑰”(session key),用它來加密資訊。由於“對話密鑰”是對稱式加密,所以運算速度非常快,而伺服器公開金鑰只用於加密“對話密鑰”本身,這樣就減少了加密運算的消耗時間。
因此,SSL/TLS協議的基本過程是這樣的:
1. 用戶端向伺服器端索要並驗證公開金鑰。
2. 雙方協商產生“對話密鑰”。
3. 雙方採用“對話密鑰”進行加密通訊。
上面過程的前兩布,又稱為“握手階段”。
握手階段的詳細過程

握手階段”涉及四次通訊,需要注意的是,“握手階段”的所有通訊都是明文的。
用戶端發出請求(ClientHello)
首先,用戶端(通常是瀏覽器)先向伺服器發出加密通訊的請求,這被叫做ClientHello請求。在這一步中,用戶端主要向伺服器提供以下資訊:
1. 支援的協議版本,比如TLS 1.0版
2. 一個用戶端產生的隨機數,稍後用於產生“對話密鑰”。
3. 支援的加密方法,比如RSA公開金鑰加密。
4. 支援的壓縮方法。
這裡需要注意的是,用戶端發送的資訊之中不包括伺服器的網域名稱。也就是說,理論上伺服器只能包含一個網站,否則會分不清應用向用戶端提供哪一個網站的數位憑證。這就是為什麼通常一台伺服器只能有一張數位憑證的原因。
伺服器回應(ServerHello)
伺服器收到用戶端請求後,向用戶端發出回應,這叫做ServerHello。伺服器的回應包含以下內容:
1. 確認使用的加密通訊協定版本,比如TLS 1.0版本。如果瀏覽器與伺服器支援的版本不一致,伺服器關閉加密通訊。
2. 一個伺服器產生的隨機數,稍後用於產生“對話密鑰”。
3. 確認使用的加密方法,比如RSA公開金鑰加密。
4. 伺服器憑證。
除了上面這些資訊,如果伺服器需要確認用戶端的身份,就會再包含一項請求,要求用戶端提供“用戶端認證”。比如,金融機構往往只允許認證客戶連入自己的網路,就會向正式客戶提供USB密鑰,裡面就包含了一張用戶端認證。
用戶端回應
用戶端收到伺服器回應以後,首先驗證伺服器憑證。如果認證不是可信機構頒發,或者認證中的網域名稱與實際網域名稱不一致,或者認證已經到期,就會向訪問者顯示一個警告,由其選擇是否還要繼續通訊。
如果認證沒有問題,用戶端就會從認證中取出伺服器的公開金鑰。然後,向伺服器發送下面三項訊息。
1. 一個隨機數。該隨機數用伺服器公開金鑰加密,防止被竊聽。
2. 編碼改變通知,表示隨後的資訊都將用雙方商定的加密方法和密鑰發送。
3. 用戶端握手結束通知,表示用戶端的握手階段已經結束。這一項通常也是前面發送的所有內容的hash值,用來供伺服器校正。
上面第一項隨機數,是整個握手階段出現的第三個隨機數,又稱“pre-master key”。有了它以後,用戶端和伺服器就同時有了三個隨機數,接著雙方就用事先商定的加密方法,各自產生本次會話所用的同一把“工作階段金鑰”。
伺服器的最後回應
伺服器收到用戶端的第三個隨機數pre-master key之後,計算產生本次會話所用的“工作階段金鑰”。然後,向用戶端最後發送下面資訊。
1. 編碼改變通知,表示隨後的資訊都將用雙方商定的加密方法和密鑰發送。
2. 伺服器握手結束通知,表示伺服器的握手階段已經結束。這一項同時也是前面發生的所有內容的hash值,用來供用戶端校正。
握手結束
至此,整個握手階段全部結束。接下來,用戶端與伺服器進入加密通訊,就完全是使用普通的HTTP協議,只不過用“工作階段金鑰”加密內容。

伺服器基於Nginx搭建HTTPS虛擬網站
之前一篇文章詳細介紹了在伺服器端如何產生SSL認證,並基於Nginx搭建HTTPS伺服器,連結:Nginx搭建HTTPS伺服器

Android實現HTTPS通訊
由於各種原因吧,這裡使用HttpClicent類講解一下Android如何建立HTTPS串連。代碼demo如下。
MainActivity.java

   

 package com.example.photocrop;      import java.io.BufferedReader;   import java.io.InputStreamReader;      import org.apache.http.HttpResponse;   import org.apache.http.HttpStatus;   import org.apache.http.StatusLine;   import org.apache.http.client.HttpClient;   import org.apache.http.client.methods.HttpPost;   import org.apache.http.client.methods.HttpUriRequest;      import android.app.Activity;   import android.os.AsyncTask;   import android.os.Bundle;   import android.os.AsyncTask.Status;   import android.text.TextUtils;   import android.util.Log;   import android.view.View;   import android.widget.Button;   import android.widget.TextView;      public class MainActivity extends Activity {     private Button httpsButton;     private TextView conTextView;        private CreateHttpsConnTask httpsTask;        @Override     protected void onCreate(Bundle savedInstanceState) {       super.onCreate(savedInstanceState);       setContentView(R.layout.activity_main);          httpsButton = (Button) findViewById(R.id.create_https_button);       httpsButton.setOnClickListener(new View.OnClickListener() {            @Override         public void onClick(View v) {           runHttpsConnection();         }       });          conTextView = (TextView) findViewById(R.id.content_textview);       conTextView.setText("初始為空白");     }        private void runHttpsConnection() {       if (httpsTask == null || httpsTask.getStatus() == Status.FINISHED) {         httpsTask = new CreateHttpsConnTask();         httpsTask.execute();       }     }        private class CreateHttpsConnTask extends AsyncTask<Void, Void, Void> {       private static final String HTTPS_EXAMPLE_URL = "自訂";       private StringBuffer sBuffer = new StringBuffer();          @Override       protected Void doInBackground(Void... params) {         HttpUriRequest request = new HttpPost(HTTPS_EXAMPLE_URL);         HttpClient httpClient = HttpUtils.getHttpsClient();         try {           HttpResponse httpResponse = httpClient.execute(request);           if (httpResponse != null) {             StatusLine statusLine = httpResponse.getStatusLine();             if (statusLine != null                 && statusLine.getStatusCode() == HttpStatus.SC_OK) {               BufferedReader reader = null;               try {                 reader = new BufferedReader(new InputStreamReader(                     httpResponse.getEntity().getContent(),                     "UTF-8"));                 String line = null;                 while ((line = reader.readLine()) != null) {                   sBuffer.append(line);                 }                  } catch (Exception e) {                 Log.e("https", e.getMessage());               } finally {                 if (reader != null) {                   reader.close();                   reader = null;                 }               }             }           }            } catch (Exception e) {           Log.e("https", e.getMessage());         } finally {            }            return null;       }          @Override       protected void onPostExecute(Void result) {         if (!TextUtils.isEmpty(sBuffer.toString())) {           conTextView.setText(sBuffer.toString());         }       }        }   } 

HttpUtils.java

   

package com.example.photocrop;      import org.apache.http.HttpVersion;   import org.apache.http.client.HttpClient;   import org.apache.http.conn.ClientConnectionManager;   import org.apache.http.conn.scheme.PlainSocketFactory;   import org.apache.http.conn.scheme.Scheme;   import org.apache.http.conn.scheme.SchemeRegistry;   import org.apache.http.conn.ssl.SSLSocketFactory;   import org.apache.http.impl.client.DefaultHttpClient;   import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;   import org.apache.http.params.BasicHttpParams;   import org.apache.http.params.HttpProtocolParams;   import org.apache.http.protocol.HTTP;      import android.content.Context;         public class HttpUtils {     public static HttpClient getHttpsClient() {       BasicHttpParams params = new BasicHttpParams();       HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);       HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);       HttpProtocolParams.setUseExpectContinue(params, true);              SchemeRegistry schReg = new SchemeRegistry();       schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));       schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));              ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);              return new DefaultHttpClient(connMgr, params);     }          public static HttpClient getCustomClient() {       BasicHttpParams params = new BasicHttpParams();       HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);       HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);       HttpProtocolParams.setUseExpectContinue(params, true);              SchemeRegistry schReg = new SchemeRegistry();       schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));       schReg.register(new Scheme("https", MySSLSocketFactory.getSocketFactory(), 443));              ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);              return new DefaultHttpClient(connMgr, params);     }          public static HttpClient getSpecialKeyStoreClient(Context context) {       BasicHttpParams params = new BasicHttpParams();       HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);       HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);       HttpProtocolParams.setUseExpectContinue(params, true);              SchemeRegistry schReg = new SchemeRegistry();       schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));       schReg.register(new Scheme("https", CustomerSocketFactory.getSocketFactory(context), 443));              ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);              return new DefaultHttpClient(connMgr, params);     }   } 

activity_main.xml

   

 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:tools="http://schemas.android.com/tools"     android:layout_width="match_parent"     android:layout_height="match_parent"     android:orientation="vertical">        <Button       android:id="@+id/create_https_button"       android:layout_width="match_parent"       android:layout_height="wrap_content"       android:text="@string/hello_world"       android:textSize="16sp" />        <TextView       android:id="@+id/content_textview"       android:layout_width="match_parent"       android:layout_height="wrap_content"       android:gravity="center"       android:textSize="16sp" />      </LinearLayout> 

Android使用DefaultHttpClient建立HTTPS串連,關鍵需要加入對HTTPS的支援:

  schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443)); 

加入對HTTPS的支援,就可以有效建立HTTPS串連了,例如“https://www.google.com.hk”了,但是訪問自己基於Nginx搭建的HTTPS伺服器卻不行,因為它使用了不被系統承認的自訂認證,會報出如下問題:No peer certificate。
使用自訂認證並忽略驗證的HTTPS串連方式
解決認證不被系統承認的方法,就是跳過系統校正。要跳過系統校正,就不能再使用系統標準的SSL SocketFactory了,需要自訂一個。然後為了在這個自訂SSL SocketFactory裡跳過校正,還需要自訂一個TrustManager,在其中忽略所有校正,即TrustAll。

   

package com.example.photocrop;      import java.io.IOException;   import java.net.Socket;   import java.net.UnknownHostException;   import java.security.KeyManagementException;   import java.security.KeyStore;   import java.security.KeyStoreException;   import java.security.NoSuchAlgorithmException;   import java.security.UnrecoverableKeyException;   import java.security.cert.CertificateException;   import java.security.cert.X509Certificate;      import javax.net.ssl.SSLContext;   import javax.net.ssl.TrustManager;   import javax.net.ssl.X509TrustManager;   import org.apache.http.conn.ssl.SSLSocketFactory;      public class MySSLSocketFactory extends SSLSocketFactory {     SSLContext sslContext = SSLContext.getInstance("TLS");        public MySSLSocketFactory(KeyStore truststore)         throws NoSuchAlgorithmException, KeyManagementException,         KeyStoreException, UnrecoverableKeyException {       super(truststore);       TrustManager tm = new X509TrustManager() {            @Override         public X509Certificate[] getAcceptedIssuers() {           return null;         }            @Override         public void checkServerTrusted(X509Certificate[] chain,             String authType) throws CertificateException {            }            @Override         public void checkClientTrusted(X509Certificate[] chain,             String authType) throws CertificateException {            }       };          sslContext.init(null, new TrustManager[] { tm }, null);     }        @Override     public Socket createSocket() throws IOException {       return sslContext.getSocketFactory().createSocket();     }        @Override     public Socket createSocket(Socket socket, String host, int port,         boolean autoClose) throws IOException, UnknownHostException {       return sslContext.getSocketFactory().createSocket(socket, host, port,           autoClose);     }        public static SSLSocketFactory getSocketFactory() {       try {         KeyStore trustStore = KeyStore.getInstance(KeyStore             .getDefaultType());         trustStore.load(null, null);         SSLSocketFactory factory = new MySSLSocketFactory(trustStore);         return factory;       } catch (Exception e) {         e.getMessage();         return null;       }     }   } 

同時,需要修改DefaultHttpClient的register方法,改為自己構建的sslsocket:

 

  public static HttpClient getCustomClient() {     BasicHttpParams params = new BasicHttpParams();     HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);     HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);     HttpProtocolParams.setUseExpectContinue(params, true);          SchemeRegistry schReg = new SchemeRegistry();     schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));     schReg.register(new Scheme("https", MySSLSocketFactory.getSocketFactory(), 443));          ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);          return new DefaultHttpClient(connMgr, params);   } 

這樣就可以成功的訪問自己構建的基於Nginx的HTTPS虛擬網站了。
缺陷:
不過,雖然這個方案使用了HTTPS,用戶端和伺服器端的通訊內容得到了加密,嗅探程式無法得到傳輸的內容,但是無法抵擋“中間人攻擊”。例如,在內網配置一個DNS,把目標伺服器網域名稱解析到本地的一個地址,然後在這個地址上使用一個中間伺服器作為代理,它使用一個假的認證與用戶端通訊,然後再由這個Proxy 伺服器作為用戶端串連到實際的伺服器,用真的認證與伺服器通訊。這樣所有的通訊內容都會經過這個代理,而用戶端不會感知,這是由於用戶端不校正伺服器密鑰憑證導致的。

使用自訂認證建立HTTPS串連
為了防止上面方案可能導致的“中間人攻擊”,我們可以下載伺服器端密鑰憑證,然後將密鑰憑證編譯到Android應用中,由應用自己來驗證認證。
產生KeyStore

要驗證自訂認證,首先要把認證編譯到應用中,這需要使用keytool工具生產KeyStore檔案。這裡的認證就是指目標伺服器的公開金鑰,可以從web伺服器配置的.crt檔案或.pem檔案獲得。同時,你需要配置bouncycastle,我下載的是bcprov-jdk16-145.jar,至於配置大家自行google就好了。

  keytool -importcert -v -trustcacerts -alias example -file www.example.com.crt -keystore example.bks -storetype BKS -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath /home/wzy/Downloads/java/jdk1.7.0_60/jre/lib/ext/bcprov-jdk16-145.jar -storepass pw123456 

運行後將顯示認證內容並提示你是否確認,輸入Y斷行符號即可。

生產KeyStore檔案成功後,將其放在app應用的res/raw目錄下即可。
使用自訂KeyStore實現串連
思路和TrushAll差不多,也是需要一個自訂的SSLSokcetFactory,不過因為還需要驗證認證,因此不需要自訂TrustManager了。

   

 package com.example.photocrop;      import java.io.IOException;   import java.io.InputStream;   import java.security.KeyManagementException;   import java.security.KeyStore;   import java.security.KeyStoreException;   import java.security.NoSuchAlgorithmException;   import java.security.UnrecoverableKeyException;      import org.apache.http.conn.ssl.SSLSocketFactory;      import android.content.Context;      public class CustomerSocketFactory extends SSLSocketFactory {        private static final String PASSWD = "pw123456";        public CustomerSocketFactory(KeyStore truststore)         throws NoSuchAlgorithmException, KeyManagementException,         KeyStoreException, UnrecoverableKeyException {       super(truststore);     }        public static SSLSocketFactory getSocketFactory(Context context) {       InputStream input = null;       try {         input = context.getResources().openRawResource(R.raw.example);         KeyStore trustStore = KeyStore.getInstance(KeyStore             .getDefaultType());            trustStore.load(input, PASSWD.toCharArray());            SSLSocketFactory factory = new CustomerSocketFactory(trustStore);            return factory;       } catch (Exception e) {         e.printStackTrace();         return null;       } finally {         if (input != null) {           try {             input.close();           } catch (IOException e) {             e.printStackTrace();           }           input = null;         }       }     }      } 

同時,需要修改DefaultHttpClient的register方法,改為自己構建的sslsocket:

 

  public static HttpClient getSpecialKeyStoreClient(Context context) {     BasicHttpParams params = new BasicHttpParams();     HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);     HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);     HttpProtocolParams.setUseExpectContinue(params, true);          SchemeRegistry schReg = new SchemeRegistry();     schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));     schReg.register(new Scheme("https", CustomerSocketFactory.getSocketFactory(context), 443));          ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);          return new DefaultHttpClient(connMgr, params);   } 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.