現(xiàn)在做
Android開(kāi)發(fā)
,或多或少都會(huì)接觸到一些HTTPS
的網(wǎng)絡(luò)請(qǐng)求,作為新人,踩坑是避免不了的,本文
將忽略HTTP
和
HTTPS
的一些網(wǎng)絡(luò)基礎(chǔ)知識(shí),單純的從代碼角度淺談
HTTPS
在
Android
中的應(yīng)用,針對(duì)用
自簽名證書(shū)進(jìn)行單項(xiàng)驗(yàn)證的HTTPS
請(qǐng)求
,如果是有CA
或者
RA
簽發(fā)的證書(shū),
Android
底層會(huì)進(jìn)行證書(shū)的相關(guān)校驗(yàn),不必自己實(shí)現(xiàn)相關(guān)的功能。
一、Android支持HTTPS的核心設(shè)計(jì)
要想在Android
上實(shí)現(xiàn)
HTTPS
通訊,最核心的有兩點(diǎn),
證書(shū)校驗(yàn)
和
域名校驗(yàn)
。其中前者是必須的,后者是非必須的。
證書(shū)校驗(yàn)
如果要想在Android
上進(jìn)行
HTTPS
通訊,通常會(huì)將證書(shū)內(nèi)置在
APP
內(nèi),或者將證書(shū)的公鑰提取出來(lái),直接以字符串的形式寫(xiě)在類(lèi)里面。其實(shí)這兩種方式都差不多,區(qū)別就是證書(shū)里面不僅僅包含公鑰,可能還有一些其他的信息。
由于是自簽名的證書(shū),如果是直接發(fā)起HTTPS
請(qǐng)求的話,將會(huì)出現(xiàn)以下異常信息:
javax.net.ssl.SSLHandshakeException:
java.security.cert.CertPathValidatorException:
Trust
anchor
for
certification
path
not
found.
出現(xiàn)這個(gè)異常信息的原因就是自簽名證書(shū)不被系統(tǒng)所信任,如果出現(xiàn)這個(gè)錯(cuò)誤,說(shuō)明HTTPS
請(qǐng)求在
TLS
握手過(guò)程中被中斷了。
要想實(shí)現(xiàn)Android
系統(tǒng)信任服務(wù)器自簽名的證書(shū),核心思想如下:
1、通過(guò)流將服務(wù)器自簽名證書(shū)轉(zhuǎn)換成X.509格式
證書(shū)放在Assets
目錄,其他目錄同理獲取輸入流
InputStream inputStream = mContext.getAssets().open("jetty_ssl.cer");
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Certificate certificate = certificateFactory.generateCertificate(inputStream);
Certificate
就是獲取到的
X.509
格式的證書(shū)類(lèi)。
2、通過(guò)服務(wù)器自簽名證書(shū)轉(zhuǎn)換的X.509證書(shū)生成包含服務(wù)端證書(shū)的KeyStore對(duì)象
獲取一個(gè)空的KeyStore
對(duì)象,所以這里的輸入流和密碼都為
null
。
String defaultType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(defaultType);
keyStore.load(
null,
null);//
輸入流和密碼都為
null
將服務(wù)器自簽名證書(shū)生成的X.509
證書(shū)添加進(jìn)
KeyStore
keyStore.setCertificateEntry("certificate",certificate);
3、用包含服務(wù)器證書(shū)的KeyStore生成一個(gè)TrustManager
這種方式生成的TrustManager
里面包含服務(wù)器證書(shū)強(qiáng)校驗(yàn)機(jī)制,也就是說(shuō)以這種方式生成的
TrustManager
都將使用強(qiáng)校驗(yàn)機(jī)制,服務(wù)器返回的證書(shū)必須匹配。
String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();//
獲取默認(rèn)的
TrustManagerFactory
算法名稱
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
trustManagerFactory.init(keyStore);
4、通過(guò)TrustManager獲取到一個(gè)包含信任服務(wù)器自簽名證書(shū)的SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(
null,trustManagerFactory.getTrustManagers(),
null);
到這里就已經(jīng)到了證書(shū)校驗(yàn)的關(guān)鍵所在,SSLContext
包含證書(shū)校驗(yàn)的關(guān)鍵信息,它可以通過(guò)下面方法獲取到
HTTPS
請(qǐng)求的關(guān)鍵參數(shù)
SSLSocketFactory
。
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
如果你是使用OkHttp3.0
網(wǎng)絡(luò)框架,可以使用
sslSocketFactory()
方法將包含服務(wù)器證書(shū)校驗(yàn)的
SSLSocketFactory
傳入
OkHttp3.0
的配置參數(shù)中,在發(fā)起
HTTPS
前,
OkHttp3.0
框架會(huì)去驗(yàn)證證書(shū)的合法性。
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.sslSocketFactory(socketFactory, (X509TrustManager) trustManagers[0])
.hostnameVerifier(DO_NOT_VERIFY)
.build();
其他網(wǎng)絡(luò)框架同樣有類(lèi)似SSLSocketFactory
作為參數(shù)驗(yàn)證證書(shū)合法性的方法,可以自行
Google
相關(guān)的資料。
以上證書(shū)校驗(yàn)流程僅僅是本地證書(shū)合法性校驗(yàn),服務(wù)器本地證書(shū)匹配校驗(yàn)過(guò)程是在HTTPS
請(qǐng)求發(fā)送后系統(tǒng)(其實(shí)是
TLS
協(xié)議)完成的,如果證書(shū)匹配不成功將自動(dòng)中斷網(wǎng)絡(luò)請(qǐng)求。正常情況下通過(guò)以上的校驗(yàn)已經(jīng)可以正常的進(jìn)行
HTTPS
通訊了,下面就開(kāi)始將一些非正常情況下出現(xiàn)的坑該怎么填。
域名校驗(yàn)
通常情況下正確的自簽名證書(shū)是不必重新域名校驗(yàn)的,需要重新域名校驗(yàn)的情況出現(xiàn)在服務(wù)器生成的證書(shū)沒(méi)有包括域名信息,或者干脆服務(wù)器沒(méi)有綁定域名,直接使用的是IP
訪問(wèn)方式。
SSL
連接有兩個(gè)關(guān)鍵環(huán)節(jié)。
首先是
驗(yàn)證證書(shū)是否來(lái)自值得信任的來(lái)源
,這個(gè)環(huán)節(jié)自簽名證書(shū)可以通過(guò)自定義TrustManager
來(lái)解決,上面通過(guò)流的方式獲取的
TrustManager
從某種意義上來(lái)說(shuō)就是屬于自定義
TrustManager
,在這里多說(shuō)一句,如果是自定義
TrustManager
,強(qiáng)烈建議開(kāi)啟證書(shū)強(qiáng)驗(yàn)證,不要進(jìn)行弱驗(yàn)證。如果證書(shū)弱驗(yàn)證,
SSL
握手環(huán)節(jié)無(wú)法成功交換秘鑰,數(shù)據(jù)其實(shí)還是明文傳輸?shù)?。以下是自定義
TrustManager
正確開(kāi)啟證書(shū)強(qiáng)校驗(yàn)的方式。
private
class
PreTrustManager
implements
X509TrustManager {
@Override
public
void
checkClientTrusted(X509Certificate[] x509Certificates, String authType)
throws CertificateException {
//
校驗(yàn)客戶端證書(shū),用于
HTTPS
雙向認(rèn)證
}
@Override
public
void
checkServerTrusted(X509Certificate[] x509Certificates, String authType)
throws CertificateException {
//
校驗(yàn)服務(wù)端證書(shū),實(shí)現(xiàn)強(qiáng)校驗(yàn),強(qiáng)烈不推薦弱校驗(yàn)
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) {
//
檢測(cè)服務(wù)端證書(shū)簽名時(shí)候有問(wèn)題
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;
}
其次
確保正在通信的服務(wù)器提供正確的證書(shū)
。如果沒(méi)有提供,通常會(huì)看到類(lèi)似于下面的異常信息:
javax.net.ssl.SSLPeerUnverifiedException:
Hostname "
你服務(wù)器的域名
"
not
verified:
出現(xiàn)此類(lèi)問(wèn)題的原因通常是由于服務(wù)器證書(shū)中配置的域名和客戶端請(qǐng)求的域名不一致所導(dǎo)致的。
有兩種解決方案,一種是服務(wù)器用真實(shí)的域名重新生成正確的證書(shū),這種解決方式不做討論;還有一種是客戶端自定義HostnameVerifier
,添加域名校驗(yàn)白名單,同樣采用域名強(qiáng)校驗(yàn)方式。
private HostnameVerifier TRUST_HOST_NAME =
new HostnameVerifier() {
@Override
public
boolean
verify(String hostname, SSLSession session) {
//
設(shè)置接受的域名集合
if (hostname.equals("
服務(wù)器域名
")) {
return
true;
}
return
false;
}
};
以下弱校驗(yàn)方式會(huì)導(dǎo)致安全隱患,強(qiáng)烈不推薦。
private HostnameVerifier TRUST_HOST_NAME =
new HostnameVerifier() {
@Override
public
boolean
verify(String hostname, SSLSession session) {
return
true;
}
};
來(lái)源:簡(jiǎn)書(shū)