Android HTTPS详解

来源:互联网 发布:沃虎送货单软件 编辑:程序博客网 时间:2024/06/05 18:05

http://blog.csdn.net/wzy_1988/article/details/39288831?utm_source=tuicool&utm_medium=referral

最近有一个跟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服务器。

这里我基于自己的PC搭建了一个测试HTTPS站点,访问https://192.168.1.106/hello.php程序能够返回“Hello World”字符串,接下来,我就讲解一下如果在Android中建立HTTPS通信。

Android实现HTTPS通信

由于各种原因吧,这里使用HttpClicent类讲解一下Android如何建立HTTPS连接。代码demo如下。
MainActivity.java
[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. package com.example.photocrop;  
  2.   
  3. import java.io.BufferedReader;  
  4. import java.io.InputStreamReader;  
  5.   
  6. import org.apache.http.HttpResponse;  
  7. import org.apache.http.HttpStatus;  
  8. import org.apache.http.StatusLine;  
  9. import org.apache.http.client.HttpClient;  
  10. import org.apache.http.client.methods.HttpPost;  
  11. import org.apache.http.client.methods.HttpUriRequest;  
  12.   
  13. import android.app.Activity;  
  14. import android.os.AsyncTask;  
  15. import android.os.Bundle;  
  16. import android.os.AsyncTask.Status;  
  17. import android.text.TextUtils;  
  18. import android.util.Log;  
  19. import android.view.View;  
  20. import android.widget.Button;  
  21. import android.widget.TextView;  
  22.   
  23. public class MainActivity extends Activity {  
  24.     private Button httpsButton;  
  25.     private TextView conTextView;  
  26.   
  27.     private CreateHttpsConnTask httpsTask;  
  28.   
  29.     @Override  
  30.     protected void onCreate(Bundle savedInstanceState) {  
  31.         super.onCreate(savedInstanceState);  
  32.         setContentView(R.layout.activity_main);  
  33.   
  34.         httpsButton = (Button) findViewById(R.id.create_https_button);  
  35.         httpsButton.setOnClickListener(new View.OnClickListener() {  
  36.   
  37.             @Override  
  38.             public void onClick(View v) {  
  39.                 runHttpsConnection();  
  40.             }  
  41.         });  
  42.   
  43.         conTextView = (TextView) findViewById(R.id.content_textview);  
  44.         conTextView.setText("初始为空");  
  45.     }  
  46.   
  47.     private void runHttpsConnection() {  
  48.         if (httpsTask == null || httpsTask.getStatus() == Status.FINISHED) {  
  49.             httpsTask = new CreateHttpsConnTask();  
  50.             httpsTask.execute();  
  51.         }  
  52.     }  
  53.   
  54.     private class CreateHttpsConnTask extends AsyncTask<Void, Void, Void> {  
  55.         private static final String HTTPS_EXAMPLE_URL = "自定义";  
  56.         private StringBuffer sBuffer = new StringBuffer();  
  57.   
  58.         @Override  
  59.         protected Void doInBackground(Void... params) {  
  60.             HttpUriRequest request = new HttpPost(HTTPS_EXAMPLE_URL);  
  61.             HttpClient httpClient = HttpUtils.getHttpsClient();  
  62.             try {  
  63.                 HttpResponse httpResponse = httpClient.execute(request);  
  64.                 if (httpResponse != null) {  
  65.                     StatusLine statusLine = httpResponse.getStatusLine();  
  66.                     if (statusLine != null  
  67.                             && statusLine.getStatusCode() == HttpStatus.SC_OK) {  
  68.                         BufferedReader reader = null;  
  69.                         try {  
  70.                             reader = new BufferedReader(new InputStreamReader(  
  71.                                     httpResponse.getEntity().getContent(),  
  72.                                     "UTF-8"));  
  73.                             String line = null;  
  74.                             while ((line = reader.readLine()) != null) {  
  75.                                 sBuffer.append(line);  
  76.                             }  
  77.   
  78.                         } catch (Exception e) {  
  79.                             Log.e("https", e.getMessage());  
  80.                         } finally {  
  81.                             if (reader != null) {  
  82.                                 reader.close();  
  83.                                 reader = null;  
  84.                             }  
  85.                         }  
  86.                     }  
  87.                 }  
  88.   
  89.             } catch (Exception e) {  
  90.                 Log.e("https", e.getMessage());  
  91.             } finally {  
  92.   
  93.             }  
  94.   
  95.             return null;  
  96.         }  
  97.   
  98.         @Override  
  99.         protected void onPostExecute(Void result) {  
  100.             if (!TextUtils.isEmpty(sBuffer.toString())) {  
  101.                 conTextView.setText(sBuffer.toString());  
  102.             }  
  103.         }  
  104.   
  105.     }  
  106. }  
HttpUtils.java
[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. package com.example.photocrop;  
  2.   
  3. import org.apache.http.HttpVersion;  
  4. import org.apache.http.client.HttpClient;  
  5. import org.apache.http.conn.ClientConnectionManager;  
  6. import org.apache.http.conn.scheme.PlainSocketFactory;  
  7. import org.apache.http.conn.scheme.Scheme;  
  8. import org.apache.http.conn.scheme.SchemeRegistry;  
  9. import org.apache.http.conn.ssl.SSLSocketFactory;  
  10. import org.apache.http.impl.client.DefaultHttpClient;  
  11. import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;  
  12. import org.apache.http.params.BasicHttpParams;  
  13. import org.apache.http.params.HttpProtocolParams;  
  14. import org.apache.http.protocol.HTTP;  
  15.   
  16. import android.content.Context;  
  17.   
  18.   
  19. public class HttpUtils {  
  20.     public static HttpClient getHttpsClient() {  
  21. <span style="white-space:pre">        </span>HttpClient mHttpsClient = new DefaultHttpClient();  
  22. <span style="white-space:pre">        </span>SSLSocketFactory mSSLSocketFactory =  SSLSocketFactory.getSocketFactory();  
  23. <span style="white-space:pre">        </span>if (mSSLSocketFactory != null) {  
  24. <span style="white-space:pre">            </span>Scheme mScheme = new Scheme("https", mSSLSocketFactory, 443);  
  25. <span style="white-space:pre">            </span>mHttpsClient.getConnectionManager().getSchemeRegistry().register(mScheme);  
  26. <span style="white-space:pre">        </span>}  
  27. <span style="white-space:pre">        </span>return mHttpsClient;  
  28.     }  
  29. }  
activity_main.xml
[html] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"  
  2.     xmlns:tools="http://schemas.android.com/tools"  
  3.     android:layout_width="match_parent"  
  4.     android:layout_height="match_parent"  
  5.     android:orientation="vertical">  
  6.   
  7.     <Button  
  8.         android:id="@+id/create_https_button"  
  9.         android:layout_width="match_parent"  
  10.         android:layout_height="wrap_content"  
  11.         android:text="@string/hello_world"  
  12.         android:textSize="16sp" />  
  13.   
  14.     <TextView  
  15.         android:id="@+id/content_textview"  
  16.         android:layout_width="match_parent"  
  17.         android:layout_height="wrap_content"  
  18.         android:gravity="center"  
  19.         android:textSize="16sp" />  
  20.   
  21. </LinearLayout>  
Android使用DefaultHttpClient建立HTTPS连接,关键需要加入对HTTPS的支持:
[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. 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。
MySSLSocketFactory.java实现代码如下:
[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. package com.example.photocrop;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.Socket;  
  5. import java.net.UnknownHostException;  
  6. import java.security.KeyManagementException;  
  7. import java.security.KeyStore;  
  8. import java.security.KeyStoreException;  
  9. import java.security.NoSuchAlgorithmException;  
  10. import java.security.UnrecoverableKeyException;  
  11. import java.security.cert.CertificateException;  
  12. import java.security.cert.X509Certificate;  
  13.   
  14. import javax.net.ssl.SSLContext;  
  15. import javax.net.ssl.TrustManager;  
  16. import javax.net.ssl.X509TrustManager;  
  17. import org.apache.http.conn.ssl.SSLSocketFactory;  
  18.   
  19. public class MySSLSocketFactory extends SSLSocketFactory {  
  20.     SSLContext sslContext = SSLContext.getInstance("TLS");  
  21.   
  22.     public MySSLSocketFactory(KeyStore truststore)  
  23.             throws NoSuchAlgorithmException, KeyManagementException,  
  24.             KeyStoreException, UnrecoverableKeyException {  
  25.         super(truststore);  
  26.         TrustManager tm = new X509TrustManager() {  
  27.   
  28.             @Override  
  29.             public X509Certificate[] getAcceptedIssuers() {  
  30.                 return null;  
  31.             }  
  32.   
  33.             @Override  
  34.             public void checkServerTrusted(X509Certificate[] chain,  
  35.                     String authType) throws CertificateException {  
  36.   
  37.             }  
  38.   
  39.             @Override  
  40.             public void checkClientTrusted(X509Certificate[] chain,  
  41.                     String authType) throws CertificateException {  
  42.   
  43.             }  
  44.         };  
  45.   
  46.         sslContext.init(null, new TrustManager[] { tm }, null);  
  47.     }  
  48.   
  49.     @Override  
  50.     public Socket createSocket() throws IOException {  
  51.         return sslContext.getSocketFactory().createSocket();  
  52.     }  
  53.   
  54.     @Override  
  55.     public Socket createSocket(Socket socket, String host, int port,  
  56.             boolean autoClose) throws IOException, UnknownHostException {  
  57.         return sslContext.getSocketFactory().createSocket(socket, host, port,  
  58.                 autoClose);  
  59.     }  
  60.   
  61.     public static SSLSocketFactory getSocketFactory() {  
  62.         try {  
  63.             KeyStore trustStore = KeyStore.getInstance(KeyStore  
  64.                     .getDefaultType());  
  65.             trustStore.load(null, null);  
  66.             SSLSocketFactory factory = new MySSLSocketFactory(trustStore);  
  67.             return factory;  
  68.         } catch (Exception e) {  
  69.             e.getMessage();  
  70.             return null;  
  71.         }  
  72.     }  
  73. }  
同时,需要修改DefaultHttpClient的register方法,改为自己构建的sslsocket:
[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public static HttpClient getCustomClient() {  
  2.     BasicHttpParams params = new BasicHttpParams();  
  3.     HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);  
  4.     HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);  
  5.     HttpProtocolParams.setUseExpectContinue(params, true);  
  6.       
  7.     SchemeRegistry schReg = new SchemeRegistry();  
  8.     schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));  
  9.     schReg.register(new Scheme("https", MySSLSocketFactory.getSocketFactory(), 443));  
  10.       
  11.     ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);  
  12.       
  13.     return new DefaultHttpClient(connMgr, params);  
  14. }  
这样就可以成功的访问自己构建的基于Nginx的HTTPS虚拟站点了。

缺陷:

不过,虽然这个方案使用了HTTPS,客户端和服务器端的通信内容得到了加密,嗅探程序无法得到传输的内容,但是无法抵挡“中间人攻击”。例如,在内网配置一个DNS,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。

使用自定义证书建立HTTPS连接

为了防止上面方案可能导致的“中间人攻击”,我们可以下载服务器端公钥证书,然后将公钥证书编译到Android应用中,由应用自己来验证证书。

生成KeyStore

要验证自定义证书,首先要把证书编译到应用中,这需要使用keytool工具生产KeyStore文件。这里的证书就是指目标服务器的公钥,可以从web服务器配置的.crt文件或.pem文件获得。同时,你需要配置bouncycastle,我下载的是bcprov-jdk16-145.jar,至于配置大家自行google就好了。

[plain] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. 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了。
[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. package com.example.photocrop;  
  2.   
  3. import java.io.IOException;  
  4. import java.io.InputStream;  
  5. import java.security.KeyManagementException;  
  6. import java.security.KeyStore;  
  7. import java.security.KeyStoreException;  
  8. import java.security.NoSuchAlgorithmException;  
  9. import java.security.UnrecoverableKeyException;  
  10.   
  11. import org.apache.http.conn.ssl.SSLSocketFactory;  
  12.   
  13. import android.content.Context;  
  14.   
  15. public class CustomerSocketFactory extends SSLSocketFactory {  
  16.   
  17.     private static final String PASSWD = "pw123456";  
  18.   
  19.     public CustomerSocketFactory(KeyStore truststore)  
  20.             throws NoSuchAlgorithmException, KeyManagementException,  
  21.             KeyStoreException, UnrecoverableKeyException {  
  22.         super(truststore);  
  23.     }  
  24.   
  25.     public static SSLSocketFactory getSocketFactory(Context context) {  
  26.         InputStream input = null;  
  27.         try {  
  28.             input = context.getResources().openRawResource(R.raw.example);  
  29.             KeyStore trustStore = KeyStore.getInstance(KeyStore  
  30.                     .getDefaultType());  
  31.   
  32.             trustStore.load(input, PASSWD.toCharArray());  
  33.   
  34.             SSLSocketFactory factory = new CustomerSocketFactory(trustStore);  
  35.   
  36.             return factory;  
  37.         } catch (Exception e) {  
  38.             e.printStackTrace();  
  39.             return null;  
  40.         } finally {  
  41.             if (input != null) {  
  42.                 try {  
  43.                     input.close();  
  44.                 } catch (IOException e) {  
  45.                     e.printStackTrace();  
  46.                 }  
  47.                 input = null;  
  48.             }  
  49.         }  
  50.     }  
  51.   
  52. }  
同时,需要修改DefaultHttpClient的register方法,改为自己构建的sslsocket:
[java] view plain copy
 print?在CODE上查看代码片派生到我的代码片
  1. public static HttpClient getSpecialKeyStoreClient(Context context) {  
  2.     BasicHttpParams params = new BasicHttpParams();  
  3.     HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);  
  4.     HttpProtocolParams.setContentCharset(params, HTTP.DEFAULT_CONTENT_CHARSET);  
  5.     HttpProtocolParams.setUseExpectContinue(params, true);  
  6.       
  7.     SchemeRegistry schReg = new SchemeRegistry();  
  8.     schReg.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));  
  9.     schReg.register(new Scheme("https", CustomerSocketFactory.getSocketFactory(context), 443));  
  10.       
  11.     ClientConnectionManager connMgr = new ThreadSafeClientConnManager(params, schReg);  
  12.       
  13.     return new DefaultHttpClient(connMgr, params);  
  14. }  

参考文献

[1] http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html
0 0
原创粉丝点击