Android HTTPS详解

前言

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

MySSLSocketFactory.java实现代码如下:

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,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。

使用自定义证书建立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);
	}

参考文献

[1] http://www.ruanyifeng.com/blog/2014/02/ssl_tls.html

时间: 2024-11-09 09:08:26

Android HTTPS详解的相关文章

转 Android HTTPS详解

目录(?)[-] 前言 HTTPS原理 SSLTLS协议作用 基本的运行过程 握手阶段的详细过程 客户端发出请求ClientHello 服务器回应ServerHello 客户端回应 服务器的最后回应 握手结束 服务器基于Nginx搭建HTTPS虚拟站点 Android实现HTTPS通信 使用自定义证书并忽略验证的HTTPS连接方式 缺陷 使用自定义证书建立HTTPS连接 生成KeyStore 使用自定义KeyStore实现连接 参考文献 前言 最近有一个跟HTTPS相关的问题需要解决,因此花时间

[gitbook] Android框架分析系列之Android Binder详解

请支持作者原创: https://mr-cao.gitbooks.io/android/content/android-binder.html Android Binder详解 Table of Contents 1. binder简介 2. binder的实现 2.1. IBinder类简介 2.2. IInterface类简介 2.3. BpBinder和BBinder简介 2.4. ProcessState和IPCThreadState简介 2.5. ServiceManager简介 2.

android WebView详解,常见漏洞详解和安全源码

这篇博客主要来介绍 WebView 的相关使用方法,常见的几个漏洞,开发中可能遇到的坑和最后解决相应漏洞的源码,以及针对该源码的解析. 由于博客内容长度,这次将分为上下两篇,上篇详解 WebView 的使用,下篇讲述 WebView 的漏洞和坑,以及修复源码的解析. 下篇:android WebView详解,常见漏洞详解和安全源码(下) 转载请注明出处:http://blog.csdn.net/self_study/article/details/54928371. 对技术感兴趣的同鞋加群 54

android WebView详解,常见漏洞详解和安全源码(下)

上篇博客主要分析了 WebView 的详细使用,这篇来分析 WebView 的常见漏洞和使用的坑. 上篇:android WebView详解,常见漏洞详解和安全源码(上) 转载请注明出处:http://blog.csdn.net/self_study/article/details/55046348 对技术感兴趣的同鞋加群 544645972 一起交流. WebView 常见漏洞 WebView 的漏洞也是不少,列举一些常见的漏洞,实时更新,如果有其他的常见漏洞,知会一下我-- WebView

HTTP和HTTPS详解

HTTP和HTTPS详解 转自:http://www.cnblogs.com/ok-lanyan/archive/2012/07/14/2591204.html HTTP是一个属于应用层的面向对象的协议,由于其简捷.快速的方式,适用于分布式超媒体信息系统.它于1990年提出,经过几年的使用与发展,得到不断地完善和扩展.目前在WWW中使用的是HTTP/1.0的第六版,HTTP/1.1的规范化工作正在进行之中,而且HTTP-NG(Next Generation of HTTP)的建议已经提出.HTT

Android slidingmenu详解 优化侧滑

Android slidingmenu 详解 性能优化 转载请注明:   http://blog.csdn.net/aaawqqq 简介 SlidingMenu 是github 上Android开源项目  能用于快速集成 Android 侧滑菜单效果 Slidingmenu 里面可以包含多种组件 fragment  viewpager  listview  等 下载地址: github    : https://github.com/jfeinstein10/SlidingMenu action

使用Ant打包Android应用详解——Ant使用解析

上篇<使用Ant打包Android应用详解>描述了使用Ant打包的流程,但很多步骤并没有说明如此做的原因,本篇将从Ant方面来理解,下一篇从APK生成的流程来说明. APK包的生成是一系列操作的结果,而Ant则是将这一系列操作流程化,提供出定制化的接口,以及可配置的参数供修改,而这些都是通过指定的构建文件来实现.我们就从Ant的打包流程来理解Ant的一些基本用法.当在命令行中执行ant,默认会去解析当前目录的build.xml作为构建文件.下面是个删除部分注视的版本: <project

android动画详解三 动画API概述

· 属性动画与view动画的不同之处 view动画系统提供了仅动画View 对象的能力,所以如果你想动画非View 对象,你就要自己实现代码. view动画系统实际上还被强制仅能对 View 的少数属性进行动画,比如缩放和旋转,而不能对背景色进行. view动画的另一个坏处是它仅修改View的绘制位置,而不是View的实际位置.例如,如果你动画一个移动穿越屏幕,button的绘制位置是正确的,但实际你可以点击它的位置却没有变,所以你必须去实现你自己的逻辑来处理它. 使用属性动画系统时,这个限制被

android矩阵详解

Matrix,中文里叫矩阵,高等数学里有介绍,在图像处理方面,主要是用于平面的缩放.平移.旋转等操作. 在Android里面,Matrix由9个float值构成,是一个3*3的矩阵.最好记住.如下图: 解释一下,上面的sinX和cosX,表示旋转角度的cos值和sin值,注意,旋转角度是按顺时针方向计算的. translateX和translateY表示x和y的平移量.scale是缩放的比例,1是不变,2是表示缩放1/2,这样子. 在android.graphics.Matrix中有对应旋转的函