Https概述
为何需要HTTPS
HTTP协议是没有加密的明文传输协议,如果APP采用HTTP传输数据,则会泄露传输内容,可能被中间人劫持,修改传输的内容。如下图所示就是典型的APP HTTP通信被运营商劫持修改,插入广告:
上图是在我的住处,用WiFi打开某APP,页面底部出现了一个拆红包的广告,点开以后是一个安装APP的页面,如果我用联通的4G网络打开,就不会出现这种情况,说明小区运营商劫持了HTTP通信,往APP的通信中加入了自己的推广内容,还有一些低俗的推广广告,这很影响用户体验。一些别有用心的人通过搭建公共WiFi,进行流量劫持、嗅探,可以获得通过HTTP传输的敏感信息。
为了保护用户的信息安全、保护自己的商业利益,减少攻击面,我们需要保障通信信道的安全,采用开发方便的HTTPS是比较好的方式,比用私有协议要好,省时省力。但是如果HTTPS使用不当,就很难起到应有的保护效果。
HTTPS原理
HTTPS(Hyper Text Transfer Protocol Secure),是一种基于SSL/TLS的HTTP,所有的HTTPS数据都是在SSL/TLS协议封装之上进行传输的,端口为443。HTTPS协议是在HTTP协议的基础上,添加了SSL/TLS握手以及数据加密传输,也属于应用层协议。所以,研究HTTPS协议原理,最终就是研究SSL/TLS协议。
SSL/TLS协议作用
不使用SSL/TLS的HTTP通信,就是不加密的通信,所有的信息明文传播,带来了三大风险:
- 窃听风险:第三方可以获知通信内容。
- 篡改风险:第三方可以修改通知内容。
- 冒充风险:第三方可以冒充他人身份参与通信。
SSL/TLS协议是为了解决这三大风险而设计的,希望达到:
- 所有信息都是加密传输,第三方无法窃听。
- 具有校验机制,一旦被篡改,通信双方都会立刻发现。
- 配备身份证书,防止身份被冒充。
基本的运行过程
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。但是这里需要了解两个问题的解决方案。
(1)如何保证公钥不被篡改?
解决方法:将公钥放在数字证书中。只要证书是可信的,公钥就是可信的。
(2)公钥加密计算量太大,如何减少耗用的时间?
解决方法:每一次对话(session),客户端和服务器端都生成一个“对话密钥”(session key),用它来加密信息。由于“对话密钥”是对称加密,所以运算速度非常快,而服务器公钥只用于加密“对话密钥”本身,这样就减少了加密运算的消耗时间。
因此,SSL/TLS协议的基本过程是这样的:
- 客户端向服务器端索要并验证公钥。
- 双方协商生成“对话密钥”。
- 双方采用“对话密钥”进行加密通信。
上面过程的前两步,又称为“握手阶段”。
握手阶段的详细过程
“握手阶段”涉及四次通信,需要注意的是,“握手阶段”的所有通信都是明文的。
(1)客户端发出请求(ClientHello)
首先,客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。在这一步中,客户端主要向服务器提供以下信息:
- 支持的协议版本,比如TLS 1.0版。
- 一个客户端生成的随机数,稍后用于生成“对话密钥”。
- 支持的加密方法,比如RSA公钥加密。
- 支持的压缩方法。
这里需要注意的是,客户端发送的信息之中不包括服务器的域名。也就是说,理论上服务器只能包含一个网站,否则会分不清应用向客户端提供哪一个网站的数字证书。这就是为什么通常一台服务器只能有一张数字证书的原因。
(2)服务器回应(ServerHello)
服务器收到客户端请求后,向客户端发出回应,这叫做ServerHello。服务器的回应包含以下内容:
- 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。
- 一个服务器生成的随机数,稍后用于生成“对话密钥”。
- 确认使用的加密方法,比如RSA公钥加密。
- 服务器证书。
除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供“客户端证书”。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。
(3)客户端回应(ClientCertificate)
客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁发,或者证书中的域名与实际域名不一致,或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。
如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项消息。
- 一个随机数。该随机数用服务器公钥加密,防止被窃听。
- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
- 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项通常也是前面发送的所有内容的hash值,用来供服务器校验。
上面第一项随机数,是整个握手阶段出现的第三个随机数,又称“pre-master key”。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把“会话密钥”。
(4)服务器的最后回应(ServerCertificate)
服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的“会话密钥”。然后,向客户端最后发送下面信息。
- 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
- 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发生的所有内容的hash值,用来供客户端校验。
至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用“会话密钥”加密内容。
客户端访问CA认证的https网站
根据上面的流程,我们可以看到服务器端会有一个证书,在交互过程中客户端需要去验证证书的合法性,对于CA机构颁发的证书当然我们会直接认为合法。对于自己造的证书,那么我们就需要去校验合法性了,也就是说我们需要让客户端去信任这个证书才可以畅通的进行通信。
这里我们举一个CA认证网站的例子:https://www.baidu.com
private void getDatas() {
AsyncHttpClient client = new AsyncHttpClient();
client.get("https://www.baidu.com", new TextHttpResponseHandler() {
@Override
public void onSuccess(int statusCode, Header[] headers, String responseBody) {
showlog("onSuccess reponse=" + responseBody);
}
@Override
public void onFailure(int statusCode, Header[] headers, String responseBody, Throwable error) {
showlog("onFailure error="+error.toString());
}
});
}
运行,可以看到以上程序会正确走到onSuccess并获取responseBody的值。
我们再举一个自签名(非CA认证)网站的例子,就是12306网站了,https://kyfw.12306.cn/otn/,我们修改一下请求url,运行:
onFailure error=javax.net.ssl.SSLHandshakeException:
java.security.cert.CertPathValidatorException:Trust anchor for certification path not found.
果然,对于自签名网站,由于证书没有被校验而不被信任,会报error得不到正确的结果。
对于自己造的证书,我们需要去校验合法性,也就是说我们需要让客户端去信任这个证书才可以畅通的进行通信。那么如何访问自签名网站呢?看下面。
客户端访问自签名https网站
本节同样以12306为例子来说明如何去访问自签名证书的网站。一般有两种方式实现,一是信任所有的证书,也就是跳过证书合法性校验这一步骤,对于这种做法肯定是有风险的;二是校验证书,证书合法才能访问。
使用HttpClient
我们首先写一个例子,使用HttpClient来访问CA认证的https网站。
public class MainActivity extends AppCompatActivity {
private TextView text;
private CreateHttpsClientConnTask httpsClientConnTask;
String https_url = "https://www.baidu.com";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
text = (TextView) findViewById(R.id.text);
runHttpsClient();
}
private void runHttpsClient() {
if (httpsClientConnTask == null || httpsClientConnTask.getStatus() == AsyncTask.Status.FINISHED) {
httpsClientConnTask = new CreateHttpsClientConnTask();
httpsClientConnTask.execute();
}
}
private class CreateHttpsClientConnTask extends AsyncTask<Void, Void, Void> {
private StringBuffer sBuffer = new StringBuffer();
@Override
protected Void doInBackground(Void... params) {
HttpUriRequest request = new HttpGet(https_url);
HttpClient httpClient = HttpUtils.getHttpsClient(); //访问普通的CA认证的https网站
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())) {
text.setText(sBuffer.toString());
}
}
}
private void showlog(String info) {
System.out.print("Watson " + info + "\n");
}
}
工具类:
public class HttpUtils {
//普通的CA认证的https网站
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();
//设置http和https支持
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 getMyHttpsClient() {
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 getCustomerKeyStoreClient(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);
}
}
对于工具类我们暂且看方法getHttpsClient(),Android使用DefaultHttpClient建立HTTPS连接,关键需要加入对HTTPS的支持:
schReg.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));
加入对HTTPS的支持,就可以有效的建立HTTPS连接了,例如“https://www.baidu.com”,效果如下:
但是现在访问自签名https网站却还不行,例如“https://kyfw.12306.cn/otn/”,因为它使用了不被系统承认的自定义证书,会报出如下问题:No peer certificate。
信任所有证书:
解决证书不被系统承认的方法,就是跳过系统校验。要跳过系统校验,就不能再使用系统标准的SSLSocketFactory了,需要自定义一个。然后为了在这个自定义的SSLSocketFactory里跳过校验,还需要自定义一个X509TrustManager(Android采用的是X509验证),建立我们的验证规则,在其中忽略所有校验,即TrustAll。
public class MySSLSocketFactory extends SSLSocketFactory {
SSLContext sslContext = SSLContext.getInstance("TLS");
public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
// Android 采用X509的证书信息机制
TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
// Create a trust manager that does not validate certificate chains
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
};
// Install the all-trusting trust manager
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);
//factory.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //允许所有主机的验证
return factory;
} catch (Exception e) {
e.getMessage();
return null;
}
}
}
同时,需要修改DefaultHttpClient的register方法,改为自己构建的sslsocket:
public static HttpClient getMyHttpsClient() {
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);
}
这样就可以成功的访问自签名https网站了。
我们把请求url改成 https://kyfw.12306.cn/otn/,运行结果:
缺陷:
不过,虽然这个方案使用了HTTPS,客户端和服务器端的通信内容得到了加密,嗅探程序无法得到传输的内容,但是无法抵挡“中间人攻击”。例如,在内网配置一个DNS,把目标服务器域名解析到本地的一个地址,然后在这个地址上使用一个中间服务器作为代理,它使用一个假的证书与客户端通讯,然后再由这个代理服务器作为客户端连接到实际的服务器,用真的证书与服务器通讯。这样所有的通讯内容都会经过这个代理,而客户端不会感知,这是由于客户端不校验服务器公钥证书导致的。
校验证书:
为了防止上面方案可能导致的“中间人攻击”,我们可以下载服务器端公钥证书,然后将公钥证书编译到Android应用中,由应用自己来验证证书。
导出12306证书:
12306页面—>右击属性
证书–>详细信息—>复制到文件
选择存储格式(这里我选的是DER编码二进制 X.509)—>选择存储路径、文件名(srca.cer)
至此,证书文件就成功导出了,我们将srca.cer文件放到项目的Assert目录下,以便后面使用。
方法一:
使用自定义KeyStore实现连接,思路和TrushAll差不多,也是需要一个自定义的SSLSokcetFactory,不过因为还需要验证证书,因此不需要自定义TrustManager了。
public class CustomerSocketFactory extends SSLSocketFactory {
public CustomerSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
}
public static SSLSocketFactory getSocketFactory(Context context) {
InputStream input = null;
try {
input = context.getAssets().open("srca.cer");// 下载的证书放到项目中的assets目录中
CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
Certificate cer = cerFactory.generateCertificate(input);
KeyStore trustStore = KeyStore.getInstance("PKCS12", "BC");
trustStore.load(null, null);
trustStore.setCertificateEntry("trust", cer);
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 getCustomerKeyStoreClient(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);
}
方法二:
其实校验证书还有第二种方法:
有些人可能觉得把证书copy到assets目录下不舒服,其实我们还可以将证书中的内容提取出来,写成字符串常量,这样就不需要证书跟着app去打包了。
那么如何提取srca.cer证书的内容呢?命令:
keytool -printcert -rfc -file srca.cer
这样就可以提取到证书的内容了。有关keytool命令的使用可以参考这篇文章:http://blog.chinaunix.net/uid-17102734-id-2830223.html。
我们重新改写CustomerSocketFactory类:
public class CustomerSocketFactory extends SSLSocketFactory {
private static String CER_12306 = "-----BEGIN CERTIFICATE-----\n" +
"MIICsTCCAhqgAwIBAgIIODtw6bZEH1kwDQYJKoZIhvcNAQEFBQAwRzELMAkGA1UEBhMCQ04xKTAn\n" +
"BgNVBAoTIFNpbm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MQ0wCwYDVQQDEwRTUkNBMB4X\n" +
"DTE0MDUyNjAxNDQzNloXDTE5MDUyNTAxNDQzNlowazELMAkGA1UEBhMCQ04xKTAnBgNVBAoTIFNp\n" +
"bm9yYWlsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRkwFwYDVQQLHhCUwY3vW6JiN2cNUqFOLV/D\n" +
"MRYwFAYDVQQDEw1reWZ3LjEyMzA2LmNuMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8Cxlz\n" +
"+V/4KkUk8YTxVxzii7xp2gZPWuuVBiwQ6iwL98it75WNGiYCUasDXy3O8wY+PtZFvgEKkpHqQ1U6\n" +
"uemiHStthUS1xTBsU/TuXF6AHc+oduP6zCGKcUnHRAksRb8BGSgzBA/X3B9CUKnYa9YA2EBIYccr\n" +
"zIh6aRAjDHbvYQIDAQABo4GBMH8wHwYDVR0jBBgwFoAUeV62d7fiUoND7cdRiExjhSwAQ1gwEQYJ\n" +
"YIZIAYb4QgEBBAQDAgbAMAsGA1UdDwQEAwIC/DAdBgNVHQ4EFgQUj/0m74jhq993ItPCldNHYLJ8\n" +
"84MwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMA0GCSqGSIb3DQEBBQUAA4GBAEXeoTkv\n" +
"UVSeQzAxFIvqfC5jvBuApczonn+Zici+50Jcu17JjqZ0zEjn4HsNHm56n8iEbmOcf13fBil0aj4A\n" +
"Qz9hGbjmvQSufaB6//LM1jVe/OSVAKB4C9NUdY5PNs7HDzdLfkQjjDehCADa1DH+TP3879N5zFoW\n" +
"DgejQ5iFsAh0\n" +
"-----END CERTIFICATE-----";
public CustomerSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
super(truststore);
}
public static SSLSocketFactory getSocketFactory(Context context) {
InputStream input = null;
try {
// input = context.getAssets().open("srca.cer");//下载的证书放到项目中的assets目录中
input = new ByteArrayInputStream(CER_12306.getBytes("UTF-8")); //证书内容存放在String中
CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
Certificate cer = cerFactory.generateCertificate(input);
KeyStore trustStore = KeyStore.getInstance("PKCS12", "BC");
trustStore.load(null, null);
trustStore.setCertificateEntry("trust", cer);
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;
}
}
}
}
使用HttpsURLConnection
信任所有证书:
private void runHttpsURLConnection() {
if (httpsUrlConnTask == null || httpsUrlConnTask.getStatus() == AsyncTask.Status.FINISHED) {
httpsUrlConnTask = new CreateHttpsUrlConnTask();
httpsUrlConnTask.execute();
}
}
private class CreateHttpsUrlConnTask extends AsyncTask<Void, Void, Void> {
private StringBuffer sBuffer = new StringBuffer();
@Override
protected Void doInBackground(Void... params) {
try {
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 sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{tm}, null);
URL url = new URL(https_url);
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
// httpsURLConnection.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //允许所有主机的验证
httpsURLConnection.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//未校验服务器证书的域名是否符合
return true;
}
};
httpsURLConnection.setHostnameVerifier(hostnameVerifier);
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
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());
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (!TextUtils.isEmpty(sBuffer.toString())) {
text.setText(sBuffer.toString());
}
}
}
校验证书:
方法一:
真正实现TrustManger的checkServerTrusted(),对服务器证书域名进行强校验或者真正实现HostnameVerifier的verify()方法。
private class CreateHttpsUrlConnTask extends AsyncTask<Void, Void, Void> {
private StringBuffer sBuffer = new StringBuffer();
@Override
protected Void doInBackground(Void... params) {
try {
InputStream input = getAssets().open("srca.cer");
CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
//其中serverCert是APP中预埋的服务器端公钥证书
final X509Certificate serverCert = (X509Certificate) cerFactory.generateCertificate(input);
TrustManager tm = new X509TrustManager() {
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String authType) throws CertificateException {
if (x509Certificates == null) {
throw new IllegalArgumentException("check server x509Certificates is null");
}
if (x509Certificates.length < 0) {
throw new IllegalArgumentException("check server x509Certificates is empty");
}
for (X509Certificate cert: x509Certificates) {
//检查服务器端证书签名是否有问题
cert.checkValidity();
try {
//和APP预埋证书作对比
cert.verify(serverCert.getPublicKey());
} catch (Exception e) {
showlog(e.toString());
}
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{tm}, null);
URL url = new URL(https_url);
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
// httpsURLConnection.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //允许所有主机的验证
httpsURLConnection.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//校验服务器证书的域名是否符合
// HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
// Boolean result = hv.verify("*/otn/", sslSession);
// return result;
return true;
}
};
httpsURLConnection.setHostnameVerifier(hostnameVerifier);
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
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());
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (!TextUtils.isEmpty(sBuffer.toString())) {
text.setText(sBuffer.toString());
}
}
}
方法二:
上面只是一种写法,还有另外一种写法证书锁定,直接用预埋的证书来生成TrustManger,过程如下:
private void runHttpsURLConnection2() {
if (httpsUrlConnTask2 == null || httpsUrlConnTask2.getStatus() == AsyncTask.Status.FINISHED) {
httpsUrlConnTask2 = new CreateHttpsUrlConnTask2();
httpsUrlConnTask2.execute();
}
}
private class CreateHttpsUrlConnTask2 extends AsyncTask<Void, Void, Void> {
private StringBuffer sBuffer = new StringBuffer();
@Override
protected Void doInBackground(Void... params) {
try {
//以X.509格式获取证书
InputStream input = getAssets().open("srca.cer");
CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
Certificate cert = cerFactory.generateCertificate(input);
//生成一个包含服务器证书的keystore
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("cert", cert);
//用包含服务器端证书的KeyStore生成一个TrustManager
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm);
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
URL url = new URL(https_url);
HttpsURLConnection httpsURLConnection = (HttpsURLConnection) url.openConnection();
httpsURLConnection.setSSLSocketFactory(sslContext.getSocketFactory());
// httpsURLConnection.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //允许所有主机的验证
httpsURLConnection.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//校验服务器证书的域名是否符合
// HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
// Boolean result = hv.verify("*/otn/", sslSession);
// return result;
return true;
}
};
httpsURLConnection.setHostnameVerifier(hostnameVerifier);
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(httpsURLConnection.getInputStream()));
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());
}
return null;
}
@Override
protected void onPostExecute(Void result) {
if (!TextUtils.isEmpty(sBuffer.toString())) {
text.setText(sBuffer.toString());
}
}
}
使用OKHttp3.0
除了使用Android系统提供的HttpClient和HttpsURLconnection进行https通信,还有其他的第三方库可以使用,以OKhttp3.0为例:
/** OkHttp请求Https网站 */
private void runHttpsOkHttp() {
try {
//以X.509格式获取证书
InputStream input = getAssets().open("srca.cer");
CertificateFactory cerFactory = CertificateFactory.getInstance("X.509");
Certificate cert = cerFactory.generateCertificate(input);
//生成一个包含服务器证书的keystore
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("cert", cert);
//用包含服务器端证书的KeyStore生成一个TrustManager
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm);
trustManagerFactory.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagerFactory.getTrustManagers(), null);
final HostnameVerifier hostnameVerifier = new HostnameVerifier() {
@Override
public boolean verify(String s, SSLSession sslSession) {
//不校验服务器端证书域名
return true;
}
};
OkHttpClient mOkHttpClient = new OkHttpClient().newBuilder().hostnameVerifier(hostnameVerifier)
.sslSocketFactory(sslContext.getSocketFactory()).build();
Request request = new Request.Builder()
.url(https_url)
.build();
Call call = mOkHttpClient.newCall(request);
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, final IOException e) {
showlog(e.getMessage());
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onResponse(Call call, Response response) throws IOException {
final String str = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
text.setText(str);
}
});
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
WebView加载https页面
目前很多应用都用webview加载H5页面,如果服务端采用的是可信CA颁发的证书,在 webView.setWebViewClient(webviewClient) 时重载 WebViewClient的onReceivedSslError() ,如果出现证书错误,直接调用handler.proceed()会忽略错误继续加载证书有问题的页面,如果调用handler.cancel()可以终止加载证书有问题的页面,证书出现问题了,可以提示用户风险,让用户选择加载与否,如果是需要安全级别比较高,可以直接终止页面加载,提示用户网络环境有风险:
...
web.setWebViewClient(webViewClient);
...
/** WebView加载Https网站 */
private void loadHttpsUrl() {
web.loadUrl(https_url);
}
private WebViewClient webViewClient = new WebViewClient() {
@Override
public void onReceivedSslError(WebView view, final SslErrorHandler handler, SslError error) {
final AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
showlog("error toString()="+error.toString());
showlog("error getPrimaryError()="+error.getPrimaryError());
SslCertificate sslCertificate = error.getCertificate();
showlog("sslCertificate toString()="+sslCertificate.toString());
switch(error.getPrimaryError()) {
case SslError.SSL_DATE_INVALID:
showlog(SslError.SSL_DATE_INVALID+" ssl date invalid");
break;
case SslError.SSL_EXPIRED:
showlog(SslError.SSL_EXPIRED+" ssl expired");
break;
case SslError.SSL_IDMISMATCH:
showlog(SslError.SSL_IDMISMATCH+" ssl id mismatch");
break;
case SslError.SSL_INVALID:
showlog(SslError.SSL_EXPIRED+" ssl invalid");
break;
case SslError.SSL_UNTRUSTED:
showlog(SslError.SSL_UNTRUSTED+" ssl untrusted");
break;
case SslError.SSL_MAX_ERROR:
showlog(SslError.SSL_MAX_ERROR+" ssl max error");
break;
}
builder.setTitle("SSL证书错误");
builder.setMessage("SSL错误码"+error.getPrimaryError());
builder.setPositiveButton("继续", new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialogInterface, int i) {
handler.proceed(); //接受证书
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialogInterface, int i) {
handler.cancel(); //默认的处理方式,WebView变成空白页
}
});
final AlertDialog dialog = builder.create();
dialog.show();
}
};
不建议直接用handler.proceed(),聚安全的应用安全扫描器会扫出来直接调用handler.proceed()的情况。
如果webview加载https需要强校验服务端证书,可以在 onPageStarted() 中用 HttpsURLConnection 强校验证书的方式来校验服务端证书,如果校验不通过停止加载网页。当然这样会拖慢网页的加载速度,需要进一步优化,具体优化的办法不在本次讨论范围,这里也不详细讲解了。
注:其实本文只是介绍了客户端如何去成功访问https站点,关于tomcat下如何使用自签名证书部署服务器,以及极少数应用需要双向证书验证(比如银行、金融类app)这两个问题不多做阐述,读者可以参考hongyang的这篇文章:Android Https相关完全解析 当OkHttp遇到Https。