
麦子学院 2017-09-04 20:21
Android学习之HTTPS的应用详解
回复:0 查看:2840
现在做
Android开发
,或多或少都会接触到一些HTTPS
的网络请求,作为新人,踩坑是避免不了的,本文
将忽略HTTP
和
HTTPS
的一些网络基础知识,单纯的从代码角度浅谈
HTTPS
在
Android
中的应用,针对用
自签名证书进行单项验证的HTTPS
请求
,如果是有CA
或者
RA
签发的证书,
Android
底层会进行证书的相关校验,不必自己实现相关的功能。
一、Android支持HTTPS的核心设计
要想在Android
上实现
HTTPS
通讯,最核心的有两点,
证书校验
和
域名校验
。其中前者是必须的,后者是非必须的。
证书校验
如果要想在Android
上进行
HTTPS
通讯,通常会将证书内置在
APP
内,或者将证书的公钥提取出来,直接以字符串的形式写在类里面。其实这两种方式都差不多,区别就是证书里面不仅仅包含公钥,可能还有一些其他的信息。
由于是自签名的证书,如果是直接发起HTTPS
请求的话,将会出现以下异常信息:
javax.net.ssl.SSLHandshakeException:
java.security.cert.CertPathValidatorException:
Trust
anchor
for
certification
path
not
found.
出现这个异常信息的原因就是自签名证书不被系统所信任,如果出现这个错误,说明HTTPS
请求在
TLS
握手过程中被中断了。
要想实现Android
系统信任服务器自签名的证书,核心思想如下:
1、通过流将服务器自签名证书转换成X.509格式
证书放在Assets
目录,其他目录同理获取输入流
InputStream inputStream = mContext.getAssets().open("jetty_ssl.cer");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate certificate = certificateFactory.generateCertificate(inputStream);
Certificate
就是获取到的
X.509
格式的证书类。
2、通过服务器自签名证书转换的X.509证书生成包含服务端证书的KeyStore对象
获取一个空的KeyStore
对象,所以这里的输入流和密码都为
null
。
String defaultType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(defaultType);
keyStore.load(
null,
null);//
输入流和密码都为
null
将服务器自签名证书生成的X.509
证书添加进
KeyStore
keyStore.setCertificateEntry("certificate",certificate);
3、用包含服务器证书的KeyStore生成一个TrustManager
这种方式生成的TrustManager
里面包含服务器证书强校验机制,也就是说以这种方式生成的
TrustManager
都将使用强校验机制,服务器返回的证书必须匹配。
String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();//
获取默认的
TrustManagerFactory
算法名称
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
trustManagerFactory.init(keyStore);
4、通过TrustManager获取到一个包含信任服务器自签名证书的SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(
null,trustManagerFactory.getTrustManagers(),
null);
到这里就已经到了证书校验的关键所在,SSLContext
包含证书校验的关键信息,它可以通过下面方法获取到
HTTPS
请求的关键参数
SSLSocketFactory
。
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
如果你是使用OkHttp3.0
网络框架,可以使用
sslSocketFactory()
方法将包含服务器证书校验的
SSLSocketFactory
传入
OkHttp3.0
的配置参数中,在发起
HTTPS
前,
OkHttp3.0
框架会去验证证书的合法性。
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0])
.hostnameVerifier(DO_NOT_VERIFY)
.build();
其他网络框架同样有类似SSLSocketFactory
作为参数验证证书合法性的方法,可以自行
Google
相关的资料。
以上证书校验流程仅仅是本地证书合法性校验,服务器本地证书匹配校验过程是在HTTPS
请求发送后系统(其实是
TLS
协议)完成的,如果证书匹配不成功将自动中断网络请求。正常情况下通过以上的校验已经可以正常的进行
HTTPS
通讯了,下面就开始将一些非正常情况下出现的坑该怎么填。
域名校验
通常情况下正确的自签名证书是不必重新域名校验的,需要重新域名校验的情况出现在服务器生成的证书没有包括域名信息,或者干脆服务器没有绑定域名,直接使用的是IP
访问方式。
SSL
连接有两个关键环节。
首先是
验证证书是否来自值得信任的来源
,这个环节自签名证书可以通过自定义TrustManager
来解决,上面通过流的方式获取的
TrustManager
从某种意义上来说就是属于自定义
TrustManager
,在这里多说一句,如果是自定义
TrustManager
,强烈建议开启证书强验证,不要进行弱验证。如果证书弱验证,
SSL
握手环节无法成功交换秘钥,数据其实还是明文传输的。以下是自定义
TrustManager
正确开启证书强校验的方式。
private
class
PreTrustManager
implements
X509TrustManager {
@Override
public
void
checkClientTrusted(X509Certificate[] x509Certificates, String authType)
throws CertificateException {
//
校验客户端证书,用于
HTTPS
双向认证
}
@Override
public
void
checkServerTrusted(X509Certificate[] x509Certificates, String authType)
throws CertificateException {
//
校验服务端证书,实现强校验,强烈不推荐弱校验
if (x509Certificates ==
null) {
throw
new IllegalArgumentException("Check Server X509Certificate[] is null");
}
if (x509Certificates.length < 0) {
throw
new IllegalArgumentException("Check Server X509Certificate[] is empty");
}
for (X509Certificate x509Certificate : x509Certificates) {
//
检测服务端证书签名时候有问题
x509Certificate.checkValidity();
try {
x509Certificate.verify(getX509Certificate().getPublicKey());
}
catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
catch (InvalidKeyException e) {
e.printStackTrace();
}
catch (NoSuchProviderException e) {
e.printStackTrace();
}
catch (SignatureException e) {
e.printStackTrace();
}
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return
new X509Certificate[0];
}
}
private X509Certificate getX509Certificate(){
try {
InputStream inputStream = mContext.getAssets().
open("jetty_ssl.cer");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
X509Certificate certificate = (X509Certificate) certificateFactory.generateCertificate(inputStream);
return certificate;
}
catch (IOException e) {
e.printStackTrace();
}
catch (CertificateException e) {
e.printStackTrace();
}
return null;
}
其次
确保正在通信的服务器提供正确的证书
。如果没有提供,通常会看到类似于下面的异常信息:
javax.net.ssl.SSLPeerUnverifiedException:
Hostname "
你服务器的域名
"
not
verified:
出现此类问题的原因通常是由于服务器证书中配置的域名和客户端请求的域名不一致所导致的。
有两种解决方案,一种是服务器用真实的域名重新生成正确的证书,这种解决方式不做讨论;还有一种是客户端自定义HostnameVerifier
,添加域名校验白名单,同样采用域名强校验方式。
private HostnameVerifier TRUST_HOST_NAME =
new HostnameVerifier() {
@Override
public
boolean
verify(String hostname, SSLSession session) {
//
设置接受的域名集合
if (hostname.equals("
服务器域名
")) {
return
true;
}
return
false;
}
};
以下弱校验方式会导致安全隐患,强烈不推荐。
private HostnameVerifier TRUST_HOST_NAME =
new HostnameVerifier() {
@Override
public
boolean
verify(String hostname, SSLSession session) {
return
true;
}
};
来源:简书