最近在寫一個(gè)數(shù)據(jù)安全傳輸?shù)捻?xiàng)目图贸,把遇到的坑以及怎么解決的,都記錄下來乾闰。
1.為什么要用HTTPS落追?
因?yàn)镠TTP協(xié)議是沒有加密的明文傳輸協(xié)議,如果APP采用HTTP傳輸數(shù)據(jù)涯肩,則會(huì)泄露傳輸內(nèi)容淋硝,可能被中間人劫持,修改傳輸?shù)膬?nèi)容宽菜。為了保護(hù)用戶的信息安全谣膳、保護(hù)自己的商業(yè)利益,減少攻擊面铅乡,我們需要保障通信信道的安全继谚,采用開發(fā)方便的HTTPS是比較好的方式,比用私有協(xié)議要好阵幸,省時(shí)省力花履。
2.HTTPS通信原理
HTTPS是HTTP over SSL/TLS芽世,HTTP是應(yīng)用層協(xié)議,TCP是傳輸層協(xié)議诡壁,在應(yīng)用層和傳輸層之間济瓢,增加了一個(gè)安全套接層SSL/TLS:
SSL/TLS層負(fù)責(zé)客戶端和服務(wù)器之間的加解密算法協(xié)商、密鑰交換妹卿、通信連接的建立旺矾,安全連接的建立過程如下所示:
3.如何使用HTTPS
3.1數(shù)字證書、CA與HTTPS
信息安全的基礎(chǔ)依賴密碼學(xué)夺克,密碼學(xué)涉及算法和密鑰箕宙,算法一般是公開的,而密鑰需要得到妥善的保護(hù)铺纽,密鑰如何產(chǎn)生柬帕、分配、使用和回收狡门,這涉及公鑰基礎(chǔ)設(shè)施陷寝。
公鑰基礎(chǔ)設(shè)施(PKI)是一組由硬件、軟件其馏、參與者盼铁、管理政策與流程組成的基礎(chǔ)架構(gòu),其目的在于創(chuàng)造尝偎、管理、分配鹏控、使用致扯、存儲(chǔ)以及撤銷數(shù)字證書。公鑰存儲(chǔ)在數(shù)字證書中当辐,標(biāo)準(zhǔn)的數(shù)字證書一般由可信數(shù)字證書認(rèn)證機(jī)構(gòu)(CA抖僵,根證書頒發(fā)機(jī)構(gòu))簽發(fā),此證書將用戶的身份跟公鑰鏈接在一起缘揪。CA必須保證其簽發(fā)的每個(gè)證書的用戶身份是唯一的耍群。
鏈接關(guān)系(證書鏈)通過注冊(cè)和發(fā)布過程創(chuàng)建,取決于擔(dān)保級(jí)別找筝,鏈接關(guān)系可能由CA的各種軟件或在人為監(jiān)督下完成蹈垢。PKI的確定鏈接關(guān)系的這一角色稱為注冊(cè)管理中心(RA,也稱中級(jí)證書頒發(fā)機(jī)構(gòu)或者中間機(jī)構(gòu))袖裕。RA確保公鑰和個(gè)人身份鏈接曹抬,可以防抵賴。如果沒有RA急鳄,CA的Root 證書遭到破壞或者泄露谤民,由此CA頒發(fā)的其他證書就全部失去了安全性堰酿,所以現(xiàn)在主流的商業(yè)數(shù)字證書機(jī)構(gòu)CA一般都是提供三級(jí)證書,Root 證書簽發(fā)中級(jí)RA證書张足,由RA證書簽發(fā)用戶使用的證書触创。
X509證書鏈,左邊的是CA根證書为牍,中間的是RA中間機(jī)構(gòu)哼绑,右邊的是用戶:
3.2生成自己的CA根證書 , 生成服務(wù)端證書 省略
.............
3.3使用HttpsURLConnection進(jìn)行HTTPS通信
獲取SSLContext
/**
* 獲取SSLContext
* @param context 上下文
* @return SSLContext
*/
private static SSLContext getSSLContext(Context context) {
try {
// 服務(wù)器端需要驗(yàn)證的客戶端證書
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
// 客戶端信任的服務(wù)器端證書
KeyStore trustStore = KeyStore.getInstance(KEY_STORE_TYPE_P12);
InputStream ksIn = context.getResources().getAssets().open(KEY_STORE_CLIENT_PATH);
InputStream tsIn = context.getResources().getAssets().open(KEY_STORE_TRUST_PATH);
try {
keyStore.load(ksIn, KEY_STORE_PASSWORD.toCharArray());
trustStore.load(tsIn, KEY_STORE_TRUST_PASSWORD.toCharArray());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
ksIn.close();
} catch (Exception ignore) {
}
try {
tsIn.close();
} catch (Exception ignore) {
}
}
SSLContext sslContext = SSLContext.getInstance("TLS");
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance("X509");
keyManagerFactory.init(keyStore, KEY_STORE_PASSWORD.toCharArray());
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);
return sslContext;
} catch (Exception e) {
Log.e("tag", e.getMessage(), e);
}
return null;
}
獲取HttpsURLConnection
/**
* @param context 上下文
* @param url 連接url
* @param method 請(qǐng)求方式
* @return HttpsURLConnection
*/
public static HttpsURLConnection getHttpsURLConnection(Context context, String url, String method,
String head_sid,String head_size,String head_dev,String head_protocol_ver,String head_msg_id,String head_md) {
URL u;
HttpsURLConnection connection = null;
try {
SSLContext sslContext = getSSLContext(context);
if (sslContext != null) {
u = new URL(url);
connection = (HttpsURLConnection) u.openConnection();
connection.setRequestMethod(method);//"POST" "GET"
connection.setDoOutput(true);
connection.setDoInput(true);
connection.setUseCaches(false);
//此處代碼和業(yè)務(wù)相關(guān)吵聪,請(qǐng)忽略
connection.setInstanceFollowRedirects(true);
connection.setRequestProperty("sid",head_sid);
connection.setRequestProperty("size",head_size);
connection.setRequestProperty("dev",head_dev);
connection.setRequestProperty("protocol-ver",head_protocol_ver);
connection.setRequestProperty("msg-id",head_msg_id);
connection.setRequestProperty("md",head_md);
connection.setRequestProperty("Content-Type", "application/json");
//此處代碼和業(yè)務(wù)相關(guān)凌那,請(qǐng)忽略
connection.setHostnameVerifier( new HostnameVerifier(){
public boolean verify(String string,SSLSession ssls) {
return true;
}
});
connection.setDefaultHostnameVerifier( new HostnameVerifier(){
public boolean verify(String string,SSLSession ssls) {
return true;
}
});
connection.setSSLSocketFactory(sslContext.getSocketFactory());
connection.setConnectTimeout(30000);
}
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
至此,可以進(jìn)行https的訪問了
4.遇到的問題
4.1.javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found
我是因?yàn)榘褍蓚€(gè)證書的搞反了,還有如果要使用私有CA簽發(fā)的證書吟逝,必須重寫校驗(yàn)證書鏈TrustManager中的方法
4.2.java.io.IOException: Hostname 'your url' was not verified
4.2.1.服務(wù)端https的證書沒有過審
解決方案:忽略ip的驗(yàn)證
HttpsURLConnection.setDefaultHostnameVerifier( new HostnameVerifier(){
public boolean verify(String string,SSLSession ssls) {
return true;
}
});
但是我發(fā)現(xiàn)這樣忽略hostname 的驗(yàn)證帽蝶,并沒有成功,最后我加了下面的代碼 對(duì)服務(wù)器證書域名進(jìn)行強(qiáng)校驗(yàn):才成功
connection.setHostnameVerifier( new HostnameVerifier(){
public boolean verify(String string,SSLSession ssls) {
return true;
}
});
4.2.2.驗(yàn)證證書時(shí)發(fā)現(xiàn)真正請(qǐng)求和服務(wù)器的證書域名不一致块攒。
解決這個(gè)問題有兩個(gè)方法:
1.重新生成服務(wù)器的證書励稳,用真實(shí)的域名信息。
2.在客戶端代碼中增加如下代碼囱井,忽略hostname 的驗(yàn)證驹尼。(僅僅用于測(cè)試階段,不建議用于發(fā)布后的產(chǎn)品中庞呕。)