深入了解HTTPS及在iOS系統中适配HTTPS類型網絡請求
一、引言
本篇部落客要讨論如何在用戶端與服務端之間進行HTTPS網絡傳輸,為了深入了解網絡傳輸的基礎原理,更加靈活的校驗證書,部落格的前半部分也将介紹一些HTTPS網絡傳輸原理。當然,文章中有不正和疏漏之處,還望朋友不吝指正,感謝!
二、HTTP與HTTPS
我們都知道,HTTP是一種常用的網絡傳輸協定,它是基于TCP的一種應用層協定,應用層是什麼樣的一個概念,通過下面這張示意圖可以很好的了解:

HTTP協定的網絡傳輸十分常見,例如網易的首頁http://www.163.com/。HTTP類型的網絡傳輸使用十分友善,但是其在安全性上卻有很大問題,列舉如下:
1.HTTP協定在傳輸資料時是明文的,任何人通過一個簡單的抓包工具,就可以截獲到所有傳輸資料。
2.HTTP協定在傳輸資料時無法保證資料的完整,在截獲到明文資料後,很容易就可以将其篡改,這也是一些網頁總是被植入惡意廣告的原因。
3.HTTP協定在傳輸資料時無法保證真實性,這也是最恐怖的一點。誤入了域名欺騙的釣魚網站,極容易對使用者帶來财産損失。
基于上面3點安全性的考慮,一種更加安全的網絡傳輸協定勢必要推行,那就是HTTPS。
要了解HTTPS協定,首先需要明白什麼是SSL/TLS。SSL全稱“Secure Sockets Layer”,意思為安全套接層。其實由網景公司為了解決HTTP傳輸協定在安全方面的缺陷而設計的。後來被标準化,更名為TLS,全稱“Transport Layer Security”,意思為傳輸層安全協定。
那麼現在就好了解了,其實HTTPS就是将HTTP協定與TLS協定組合起來,在不改變HTTP協定原設計的基礎上,為其添加安全性校驗并對傳輸的資料進行加密。那麼TLS究竟在網絡傳輸的那一層進行了處理了,下圖可以很好的表示:
三、證書
通過前面所介紹,我們知道HTTPS主要是為了解決3個問題:資料加密、資料完整、資料真實。那麼下一步就是如何解決這些問題,資料加密在發送資料前依賴SSL層對資料進行加密,資料完整與真實性則要靠另一種關鍵技術:數字證書。
通過一個小例子可以很容易的了解證書的作用,這個例子的來源是<程式設計随想>的作者,我這裡暫且借用一下:A公司的a到B公司辦事,為了證明a确實是A公司的職員而不是商業間諜,A公司會為a提供一個帶有公章的證明,當B公司看到這個證明時,就可以信任辦事員a。對比網絡傳輸,這個證明就是證書,證書可以保證這個網站的真實性。我們繼續往後分析,當B公司與越來越多的公司進行商業合作時,就又有新的問題出現了,比如C公司的c來B公司辦事,就需要拿C公司帶公章的證明,D公司的d來B公司辦事就需要拿D公司帶公章的證明...這樣一來,B公司要存放好多公司的公章和證明的模闆,才能夠完成校驗。這樣未免也太麻煩了,對應到網絡傳輸中,用戶端就是B公司,各個網站都有自己的證書檔案,這樣用戶端需要安裝信任大量的證書,為了解決這樣的問題,就有了第三方CA機構。第三方CA機構是由大家公認信任的機構,例如R公司為第三方信任機構,其業務是為其他公司提供公章證明,這樣一來,B公司隻要保有這個R公司的公章證明副本,其他A,C,D公司的辦事員也隻需要從R公司申請到一個公章證明就可以到B公司來交流業務了。
CA的全稱是“Certificate Authority”,意為證書授權中心。大部分CA機構頒發的證書都是需要付費的,CA機構頒發的證書一般都是根證書,根證書也比較容易了解,首先證書是有鍊式信任關系的,例如Y證書是由CA機構頒發的根證書,由這個Y證書還可以建立出許多子證書,子證書可以繼續建立子證書,隻要根證書是受信任的,其下所有的子證書都是受信任的,如下圖:
在Chrome浏覽器位址欄左邊可以檢視證書資訊,如下:
點選證書資訊,可以看到完整的證書鍊,如下圖:
從圖中可以看到,根證書是由CA機構VerSign公司頒發的。此處還可以看到目前證書是否有效以及過期時間,如果證書無效則說明此網頁資訊有可能被篡改過,使用者在通路時就要小心了。
除了CA機構可以簽發證書外,個人其實也是可以建立證書的,當然個人建立的證書也是不被信任的,我們姑且把這類證書叫做自簽名證書,如果用自簽名證書搭建了HTTPS的服務,則用戶端需要安裝對應的證書信任,才可以進行此服務的通路。後面我們會進一步讨論自簽名證書的使用。
四、搭建一個本地的HTTPS服務
使用Node.js可以快速的搭建前端服務,我們這裡使借助Express架構來搭建本地的HTTPS服務,用于測試我們後邊将要進行HTTPS通訊。Express搭建搭建項目模闆的過程在以前的一篇部落格中有詳細的介紹,這裡就不再重複了。
根據前面所述,搭建HTTPS服務需要有證書憑證,兩種證書我們可以選擇,一種是CA機構簽發的證書,還有一種是我們自己制作的自簽名證書,在Mac電腦上打開鑰匙串通路應用,打開其中的證書助理,如下圖所示:
選擇其中的為您自己建立證書選項,如下圖:
在之後的界面中,輸入證書的名稱,選擇證書類型,如下圖所示:
上面,我把證書的名字建立成了珲少,身份類型選擇的是自簽名的根證書,證書類型選擇SSL伺服器,之後點選建立即可完成證書的建立。
建立完成後,在鑰匙串通路的登入證書中,可以看到已經有了珲少這個自簽名的證書,如下圖:
在證書上點選右鍵,選擇導出選項,名字我将其取名為huishao,檔案類型要選擇.p12,如下圖所示:
點選存儲後,需要設定一個通路密碼,這個密碼将來将用于從.p12檔案中獲驗證書和密鑰,如下圖所示:
之後,系統有可能會讓你再次輸入一個密碼,将入下圖所示,注意,這裡需要輸入的是系統的登入密碼:
完成上面操作後,我們已經将一個.p12檔案導出到了桌面。那麼這個.p12檔案到底是個什麼東西呢,它和證書之間又有什麼關系呢,其實.p12檔案一個複合檔案,其中包裝了私鑰與證書資訊,使用OpenSSL工具可以将其中的資訊進行提取,搭建一個HTTPS的伺服器需要兩個檔案,分别問證書檔案和私鑰檔案,下面我們來從.p12檔案中提取這些需要的檔案。
打開終端,cd到huishao.p12檔案所在的目錄下,使用如下指令可以将.p12檔案中的私鑰分解出來:
openssl pkcs12 -in huishao.p12 -nocerts -out privateKey.pem -nodes
之間會要求輸入導出.p12檔案時所設定的密碼。
使用如下指令将.p12檔案中的證書分解出來:
openssl pkcs12 -in huishao.p12 -nokeys -out cert.pem -nodes
之間也會要求輸入導出.p12檔案時所設定的密碼。完成上面兩部操作後,可以看到目前檔案夾下多了兩個檔案,分别為cert.pem與privateKey.pem,他們分别是證書檔案與密鑰檔案,将他們拷貝到Express項目的bin檔案夾下,使得Express項目的結構看起來如下圖所示:
下面我們來配置Express項目。
在生成好的Express項目中的www檔案的末尾添加如下代碼:
/*
HTTPS
*/
var fs = require('fs');
var https = require('https');
/*
密鑰檔案
*/
var privatekey = fs.readFileSync('./privateKey.pem', 'utf8');
/*
證書檔案
*/
var certificate = fs.readFileSync('./cert.pem', 'utf8');
var options={key:privatekey, cert:certificate};
var serverHttps = https.createServer(options, app);
/*
綁定端口
*/
serverHttps.listen(8080,function () {
console.log('Https server listening on port ' + 8080);
});
用終端在bin檔案夾下運作 node www,效果如下:
在浏覽器打開:https://localhost:8080/users,如果伺服器搭建成功,Chrome中會出現如下效果:
點選進階,點選其中的繼續通路,可以正常擷取到伺服器傳回的資料。到此,我們的HTTPS服務就搭建成功了。
五、iOS開發中通過配置info.plist檔案來允許HTTP協定類型的通訊
前面扯了太多,終于提到重點部分了。Apple在iOS9中就已經漏出一些強制HTTPS通訊的端倪,隻是給了開發者一些過渡,在iOS10及以後的稽核機制中,Apple對于強制HTTPS的推動将會越來越強,如何讓自己的應用程式盡快的适配HTTPS相關的标準,是iOS開發者必須面對的任務。
通過前面的分析我們了解,CA機構簽發的證書是被預設信任的,這就是說,如果你的公司比較有錢,願意花錢從CA機構申請一個付費的證書,那麼很幸運,你的iOS工程是不需要做任何修改的,這些CA機構簽發的證書是預設受信任的,是以你可以直接在程式中進行HTTPS類型的請求,所需要修改的隻是将請求url改成https開頭。但是另一種情況,無論出于什麼原因,你的背景服務用的是自簽名的證書,就想我們上面搭建的HTTPS服務一樣,如果在不做任何處理的情況下在項目中通路這樣的服務,就會出現問題了,原因是我們自己建立的自簽名證書是不受信任的,系統預設拒絕了請求,示例如下:
-(void)normalHttps{
NSURLRequest * req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://localhost:8080/users"]];
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
[[session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@,%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding],error);
}] resume];
}
運作工程後可以看到,并沒有擷取到相關資料,Xcode提示為:
NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)
好了,那麼我們先不管HTTPS的問題,如果我們直接對HTTP協定的服務進行請求,會不會有問題呢,将代碼修改如下:
-(void)normalHttps{
NSURLRequest * req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:3000/users"]];
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:[NSOperationQueue mainQueue]];
[[session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@,%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding],error);
}] resume];
}
需要注意:Express在進行項目模闆的建立時,會預設幫我們綁定一個3000端口的HTTP服務。
運作工程後,可以發現HTTP協定的請求也無法通路,報錯如下:
App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
其意思大緻是說應用程式傳輸安全要求強制使用HTTPS類型的服務,但是開發者可以通過配置info.plsit檔案來回避這一政策。這就是我們這節的重點,通過檔案配置的方式來跳過應用安全傳輸協定。
在iOS9之後,開發者可以在Info.plist檔案中添加如下鍵:NSAppTransportSecurity。這個鍵用來配置APP傳輸安全的相關政策,是字典類型,其中可以設定的鍵有五個,如下:
NSAllowsArbitraryLoads:布爾值,預設為NO,設定為YES則代表除了NSExceptionDomains中設定的域名外,其他所有請求的協定類型都不受限制,也就是說可以支援HTTP類型的請求,這個鍵的作用域是全局的,App内所有的請求都受影響,但是如果開發者設定為了YES,在送出稽核時需要說明原因。
NSAllowsArbitraryLoadsForMedia:布爾值,預設為NO,設定為YES的話,則應用程式内所有的媒體資料的加載将不受協定類型的限制,同樣如果開發者設定為了YES,則在送出稽核時需要說明原因。
NSAllowsArbitraryLoadsInWebContent:布爾值,預設為NO。如果設定為YES,則應用程式内所有WebView的請求加載不受協定類型的限制,開發者設定為了YES,則在送出稽核時需要說明原因。
NSAllowsLocalNetworking:布爾值,預設為NO,如果設定為YES,則在加載本地資源時不受安全傳輸協定的限制。
NSExceptionDomains:字典,其主要對某些特殊域名做限制。其中結構可以表示如下:
NSAppTransportSecurity : Dictionary {
NSAllowsArbitraryLoads : Boolean
NSAllowsArbitraryLoadsForMedia : Boolean
NSAllowsArbitraryLoadsInWebContent : Boolean
NSAllowsLocalNetworking : Boolean
//對某些域名做特殊限制
NSExceptionDomains : Dictionary {
<domain-name-string> : Dictionary {
NSIncludesSubdomains : Boolean
NSExceptionAllowsInsecureHTTPLoads : Boolean
NSExceptionMinimumTLSVersion : String
NSExceptionRequiresForwardSecrecy : Boolean // Default value is YES
NSRequiresCertificateTransparency : Boolean
}
}
}
NSIncludesSubdomains:布爾值,這個鍵的作用是設定此域名下的所有子域名是否采用和父域名相同的配置。
NSExceptionAllowsInsecureHTTPLoads:布爾值,設定是否允許此域名使用自簽名的證書進行請求,預設為NO,如果設定為YES,則在送出時需要說明原因。
NSExceptionMinimumTLSVersion:設定所使用的TLS版本。
NSExceptionRequiresForwardSecret:設定為NO,則不允許向前加密方式。
NSRequiresCertificateTransparency:如果設定為YES,則服務端的證書要有有效的透明時間戳。
六、iOS中使用自簽名的證書進行HTTPS請求校驗
通過Info.plist檔案我們是可以繞過安全傳輸協定的,但是不幸的是,從文檔上看,無論開發者通過哪種方式來繞過安全傳輸協定,Apple都要求開發者在提審時提供合适的理由,這就是說:如果你使用了HTTP協定的請求,沒有充足理由的話,你的App有很大的可能被稽核拒絕。是以,更加保險的一種方式是将所有的服務都換成HTTPS協定的,如果有CA憑證,當然完事大吉,如果沒有,我們也可以通過驗證自簽名證書的方式來适配HTTPS協定。
在進行HTTPS請求時,服務端會先将證書檔案傳回給用戶端,如果用戶端的證書信任清單中包含這個證書,則此請求可以正常進行,如果沒有,則請求會被拒絕。是以,在iOS中适配自簽名證書的HTTPS請求實際上就是将這個自簽名的證書安裝進用戶端的信任清單。iOS中需要使用的證書是der格式的,可以使用如下指令将pem格式的證書轉換成der格式的證書:
openssl x509 -inform PEM -in cert.pem -outform DER -out cert.der
将生成的cert檔案添加進工程中,修改請求如下:
-(void)normalHttps{
NSURLRequest * req = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://localhost:8080/users"]];
NSURLSessionConfiguration * config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession * session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
NSURLSessionTask * task = [session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSLog(@"%@,%@",[[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding],error);
}];
[task resume];
}
除此之外,需要實作一個SURLSessionDelegate的協定方法如下:
- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
NSLog(@"證書認證");
//先判斷證書是否有效
if ([[[challenge protectionSpace] authenticationMethod] isEqualToString: NSURLAuthenticationMethodServerTrust]) {
//證書驗證請求
SecTrustRef serverTrust = [[challenge protectionSpace] serverTrust];
/**
* 導入多張CA憑證(Certification Authority,支援SSL證書以及自簽名的CA)
*/
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"cert" ofType:@"der"];//自簽名證書
NSData* caCert = [NSData dataWithContentsOfFile:cerPath];
//可以添加多張證書
NSArray *caArray = @[caCert];
//驗證規則
NSMutableArray *policies = [NSMutableArray array];
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
NSMutableArray *pinnedCertificates = [NSMutableArray array];
//進行自簽名證書的添加
for (NSData *certificateData in caArray) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
SecTrustResultType result = -1;
//通過本地導入的證書來驗證伺服器的證書是否可信
SecTrustEvaluate(serverTrust, &result);
NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
completionHandler(NSURLSessionAuthChallengeUseCredential,credential);
return [[challenge sender] useCredential: credential
forAuthenticationChallenge: challenge];
}
}