最近項(xiàng)目中,需要使用自簽名的 HTTPS 證書(shū)實(shí)現(xiàn)雙向認(rèn)證誊薄。網(wǎng)上的資料很多履恩,但是存在各種各樣的問(wèn)題,與 iOS 版本呢蔫、ATS 配置 等多方面因素有關(guān)切心。弄好之后先整一份記下來(lái)。完整的內(nèi)容涉及到的內(nèi)容比較多片吊,還是要全面查閱文檔绽昏,本文只記錄最終的結(jié)果,和部分遇到的問(wèn)題俏脊。
本文中的代碼全谤,在 iOS 8.x 和 iOS 9.x 的模擬器中測(cè)試通過(guò),iOS 10.x 模擬器和真機(jī)中測(cè)試通過(guò)爷贫。
一认然、背景知識(shí)
對(duì)于 HTTPS 認(rèn)證,不管是單向還是雙向漫萄,在客戶(hù)端連接到服務(wù)端時(shí)卷员,會(huì)觸發(fā)客戶(hù)端的 Authroization Challenge(沒(méi)找到太合適的翻譯,暫且理解為授權(quán)質(zhì)詢(xún))回調(diào)腾务,在處理 Authroization Challenge 之后毕骡,得到兩個(gè)值:(不知道怎么翻譯,隨便寫(xiě)下)
-
NSURLSessionAuthChallengeDisposition
處置方式-
NSURLSessionAuthChallengeUseCredential
使用指定的憑證,憑證可能為空 -
NSURLSessionAuthChallengePerformDefaultHandling
忽略憑證挺峡,使用默認(rèn)的質(zhì)詢(xún)處理器 -
NSURLSessionAuthChallengeCancelAuthenticationChallenge
整個(gè)請(qǐng)求將被取消; 憑證參數(shù)被忽略葵孤。 -
NSURLSessionAuthChallengeRejectProtectionSpace
這個(gè)挑戰(zhàn)被拒絕,并且應(yīng)該嘗試下一個(gè) Authentication Protection Space橱赠;憑證參數(shù)被忽略尤仍。
-
-
NSURLCredential *
憑證
可以根據(jù)回調(diào)時(shí)傳入的信息,自己調(diào)用相關(guān) API 獲取憑證狭姨,也可以自己偽造
將得到的兩個(gè)值宰啦,作為回調(diào)函數(shù)的結(jié)果回傳給系統(tǒng),以完成 Authroization Challenge饼拍。
以上過(guò)程僅是對(duì) iOS 認(rèn)證過(guò)程的分析赡模,不過(guò)個(gè)人認(rèn)為,網(wǎng)絡(luò)模型是一致的师抄,在不同技術(shù)中即便在實(shí)現(xiàn)細(xì)節(jié)上有所差異漓柑,但總體思路還是大同小異的。
二叨吮、服務(wù)端認(rèn)證
對(duì)于采用通過(guò) CA 購(gòu)買(mǎi)的正式證書(shū)辆布,只要沒(méi)有特別要求,手機(jī)端不需要對(duì) Authroization Challenge 做任何處理茶鉴,就可以直接連接锋玲。
如果是自簽名證書(shū),就需要做一些處理工作涵叮。iOS 8 及其之前的版本比較簡(jiǎn)單惭蹂,而且目前 iOS 8 在市面上的保有量已經(jīng)很少,不做細(xì)致討論割粮。iOS 9+ 之后引入了 ATS盾碗,帶來(lái)的問(wèn)題比較多所以從代碼到配置上都要做相應(yīng)調(diào)整。
1穆刻、白名單方式
步驟一(修改配置 Info.plist):
(1)針對(duì)域名配置
修改 Info.plist置尔,將要訪(fǎng)問(wèn)的域名配置為 NSExceptionAllowsInsecureHTTPLoads
,允許不安全的 HTTP 訪(fǎng)問(wèn):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>yourdomain.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
</dict>
</plist>
這里對(duì)于域名的配置氢伟,類(lèi)似是個(gè)白名單的方式,在白名單中的域名幽歼,配置為允許不安全的 HTTPS 連接朵锣。不安全的證書(shū)原因很多,常見(jiàn)的可能是如下原因?qū)е碌模?/p>
- 其根證書(shū)不被操作系統(tǒng)信任甸私,如:自簽證書(shū)
- 證書(shū)過(guò)期或被吊銷(xiāo)
- 證書(shū)域名與實(shí)際域名不匹配
配置中的 NSIncludesSubdomains
部分诚些,建議把域名寫(xiě)為頂級(jí)域名,然后把 NSIncludesSubdomains
置 為 true
來(lái)包含子域名。
如果是特定的完整域名诬烹,如:www.yourdomain.com
則把 NSIncludesSubdomains
置 為 false
砸烦。后面的說(shuō)法,幾次驗(yàn)證的效果不同绞吁。
如果沒(méi)有特別要求幢痘,建議使用第一種做法,寫(xiě)一級(jí)域名家破,然后包含其子域颜说。
(2)最簡(jiǎn)單粗暴的方法
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
這種方式:
- 允許非安全的 HTTP 請(qǐng)求,iOS 9+ 默認(rèn)是不允許 HTTP 連接的
- 對(duì)于所有域名都可以使用自簽名證書(shū)汰聋,不再需要逐個(gè)指定域名
步驟二(修改代碼):
修改代碼:
// 安全策略
// 同瀏覽器行為脆淹,以操作系統(tǒng)規(guī)則對(duì)服務(wù)器證書(shū)
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
// 不校驗(yàn)域名陪毡,如果需要校驗(yàn)域名,需要采用內(nèi)置證書(shū)的方式
policy.validatesDomainName = NO;
// 允許無(wú)效證書(shū)
policy.allowInvalidCertificates = YES;
// 為 SessionManager 配置安全策略
AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
mgr.securityPolicy = policy;
// 重要!W肀睢!設(shè)置緩存策略褒脯,避免緩存
// AFNetworking 的 GET 方法緩存非常明顯虽缕,一旦成功一次,后面就會(huì)直接使用緩存的結(jié)果女淑,即便網(wǎng)絡(luò)訪(fǎng)問(wèn)失敗瞭郑,也能返回成功數(shù)據(jù),會(huì)對(duì)判斷造成誤導(dǎo)鸭你,所以一定要加上這一句G拧!袱巨!
[mgr.requestSerializer setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
// 發(fā)送請(qǐng)求
[mgr GET:...];
重要:
白名單方式最簡(jiǎn)單阁谆,但是這樣做只建立安全連接,但不會(huì)對(duì)服務(wù)端證書(shū)做校驗(yàn)愉老,比如:不會(huì)校驗(yàn)證書(shū)與域名的一致性场绿。這樣做的問(wèn)題是無(wú)法防御“中間人攻擊”。
2嫉入、內(nèi)置證書(shū)方式
(1)基本實(shí)現(xiàn)
白名單的方案不夠安全焰盗,更為安全的做法:采用內(nèi)置證書(shū)的方式,將用于校驗(yàn)的證書(shū)內(nèi)置在客戶(hù)端咒林,不信任除此之外的證書(shū)熬拒。內(nèi)置的證書(shū),可以是服務(wù)端證書(shū)垫竞,或者是用于頒發(fā)服務(wù)端證書(shū)的 CA 的證書(shū)澎粟。具體要看證書(shū)具體的簽發(fā)方式。
內(nèi)置的方式,是將證書(shū)轉(zhuǎn)為 DER 格式活烙,然后以 .cer
為擴(kuò)展名徐裸,作為資源放到工程中,AFNetworking 就可以自動(dòng)識(shí)別了啸盏。
同時(shí)重贺,代碼要做如下調(diào)整:
// 安全策略
// 改為 AFSSLPinningModeCertificate
AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
// 指定驗(yàn)證域名。如果訪(fǎng)問(wèn)的域名與證書(shū)域名不一致宫补,則不能通過(guò)
// 如果需要做域名校驗(yàn)檬姥,必須使用 Pinned 方式。白名單方式粉怕,不集成證書(shū)健民,無(wú)法校驗(yàn)域名
policy.validatesDomainName = YES;
// 對(duì)于自簽證書(shū),使用這個(gè)選項(xiàng)
policy.allowInvalidCertificates = YES;
// cerData1贫贝、cerData2 為 NSData秉犹,內(nèi)容為 DER 格式證書(shū)
// 證書(shū)可以是 CA 證書(shū),也可以是服務(wù)端部署的證書(shū)稚晚,這一步可選崇堵,AFN 可以自動(dòng)識(shí)別
policy.pinnedCertificates = [NSSet setWithObjects:cerData1, cerData2, nil];
// 為 SessionManager 配置安全策略
AFHTTPSessionManager *mgr = [AFHTTPSessionManager manager];
mgr.securityPolicy = policy;
[mgr.requestSerializer setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
// 發(fā)送請(qǐng)求
[mgr GET:...];
☆☆☆ 默認(rèn)校驗(yàn)規(guī)則
上面說(shuō)的兩種方式,實(shí)際上都是使用了 AFNetworking 的默認(rèn)校驗(yàn)規(guī)則客燕,并且根據(jù)默認(rèn)規(guī)則做了個(gè)簡(jiǎn)單實(shí)現(xiàn)鸳劳。其規(guī)則是這樣的:
AFNetworking 的 AFSecurityPolicy
類(lèi)有如下方法:
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain
涉及到三個(gè)因素:
-
securityPolicy.allowInvalidCertificates
是否允許無(wú)效證書(shū),個(gè)人理解這里所說(shuō)的無(wú)效證書(shū)也搓,是指類(lèi)似瀏覽器校驗(yàn)行為赏廓,操作系統(tǒng)不認(rèn)的證書(shū) -
securityPolicy.validatesDomainName
校驗(yàn)域名 -
securityPolicy.SSLPinningMode
PinningMode
表面看來(lái)有如下規(guī)律:
- 如果要使用自簽名證書(shū),必須指定
allowInvalidCertificates = YES;
傍妒,否則不能通過(guò)幔摸; - 如果
allowInvalidCertificates == YES
并且SSLPinningMode == AFSSLPinningModeNone
,就是說(shuō)只校驗(yàn)服務(wù)端證書(shū)颤练,不管客戶(hù)端內(nèi)置證書(shū)既忆,并指定為允許無(wú)效證書(shū),則可以通過(guò)嗦玖; -
SSLPinningMode == AFSSLPinningModeCertificate 或 AFSSLPinningModePublicKey
則看本地內(nèi)置證書(shū)與服務(wù)端證書(shū)是否匹配 - 使用了
AFSSLPinningModeCertificate
或AFSSLPinningModePublicKey
患雇,會(huì)導(dǎo)致客戶(hù)端沒(méi)有內(nèi)置證書(shū)的網(wǎng)站都不能訪(fǎng)問(wèn),如:https://www.baidu.com
默認(rèn)校驗(yàn)規(guī)則總結(jié):
先約定個(gè)幾個(gè)名詞:
正規(guī)證書(shū) <=> 操作系統(tǒng)認(rèn)可 and (域名一致 or 不校驗(yàn)域名)
有效證書(shū) <=> 正規(guī)證書(shū) or 允許非正規(guī)證書(shū)
無(wú)效證書(shū) <=> 操作系統(tǒng)不認(rèn)可 and 不允許非正規(guī)證書(shū)
- 先檢查
SSLPinningMode
踏揣,如果為AFSSLPinningModeNone
庆亡,檢查證書(shū)是否為有效證書(shū)即為校驗(yàn)結(jié)果; - 如果
SSLPinningMode
為AFSSLPinningModeCertificate
或AFSSLPinningModePublicKey
捞稿,檢查證書(shū),如果為無(wú)效證書(shū),校驗(yàn)結(jié)果不通過(guò)娱局;如果為有效證書(shū)彰亥,后續(xù)則根據(jù)本地集成證書(shū)與服務(wù)端證書(shū)一起校驗(yàn)結(jié)果,作為最終校驗(yàn)結(jié)果衰齐。 -
validatesDomainName
任斋,只是判定因素之一,雖然影響整體校驗(yàn)結(jié)果耻涛,但不影響校驗(yàn)邏輯废酷。如:雖然證書(shū)合法,指定做域名校驗(yàn)抹缕,但是證書(shū)域名與訪(fǎng)問(wèn)域名不一致澈蟆,結(jié)果是不通過(guò)。
這部分的校驗(yàn)卓研,可以參見(jiàn)官方文檔:Overriding TLS Chain Validation Correctly
(2)個(gè)性化處理:指定身份驗(yàn)證質(zhì)詢(xún)
回調(diào)塊
對(duì)于“標(biāo)準(zhǔn)場(chǎng)景”趴俘,達(dá)到可訪(fǎng)問(wèn)的目的,沒(méi)有額外要求奏赘,上述代碼已經(jīng)可以了寥闪。但是對(duì)于需要額外處理的場(chǎng)景,如:失敗的時(shí)候給出對(duì)應(yīng)提示磨淌,需要使用如下方法疲憋,來(lái)指定用于處理授權(quán)質(zhì)詢(xún)
的回調(diào)塊:
// AFURLSessionManager 類(lèi)
// 指定用于處理 身份驗(yàn)證質(zhì)詢(xún) 的回調(diào)塊
– setSessionDidReceiveAuthenticationChallengeBlock:
這部分的實(shí)現(xiàn)詳情可以參見(jiàn) AFNetworking 中 AFURLSessionManager.m
文件里如下方法:
// AFURLSessionManager.m
// 處理身份驗(yàn)證質(zhì)詢(xún)
- URLSession:didReceiveChallenge:completionHandler:
在這個(gè)方法中,會(huì)先查看用戶(hù)是否指定了自己的回調(diào)塊梁只,如果指定了就執(zhí)行用戶(hù)自己的回調(diào)塊缚柳,否則執(zhí)行默認(rèn)實(shí)現(xiàn)。編寫(xiě)自己的回調(diào)方法時(shí)敛纲,可以參考默認(rèn)實(shí)現(xiàn)喂击。
注意:默認(rèn)實(shí)現(xiàn)中,只實(shí)現(xiàn)了服務(wù)端驗(yàn)證淤翔。對(duì)于客戶(hù)端驗(yàn)證部分翰绊,只做了如下處理:
*credential = nil;
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
如果要做客戶(hù)端認(rèn)證,重寫(xiě)這部分代碼即可旁壮,后文中會(huì)提到监嗜。
調(diào)試注意事項(xiàng):
測(cè)試時(shí)有一點(diǎn)需要注意,如果使用 GET
方法抡谐,應(yīng)保證每次都真實(shí)發(fā)送了請(qǐng)求裁奇,而不是使用緩存,避免影響測(cè)試效果麦撵」舫Γ坑袄7尽!
- (推薦)客戶(hù)端處理:Request 的 Header 中音五,指定
Cache-Control
為no-cache
- 處理 URL:為 URL 增加時(shí)間戳
- 服務(wù)端處理:Response 的 Header 中惫撰,指定
Cache-Control
為no-cache
- iOS 端設(shè)置:指定緩存策略(不推薦)
3、UIWebView
(1)使用 AFNetworking
AFNetworking 提供了 UIWebView+AFNetworking
Category躺涝,可以通過(guò)這個(gè)分類(lèi)為 UIWebView 指定 sessionManager
厨钻,并調(diào)用新增加的 - loadRequest:MIMEType:textEncodingName:progress:success:failure:
方法來(lái)進(jìn)行加載。但是在 Cordova 這樣的組件中坚嗜,還會(huì)使用 UIWebView 默認(rèn)的 - loadRequest:
方法夯膀,可以配合 Method Swizzling 解決該問(wèn)題。不過(guò)這樣的話(huà)還是有個(gè)問(wèn)題苍蔬,會(huì)導(dǎo)致 UIWebView 的歷史丟失诱建,無(wú)法執(zhí)行“返回”操作,原因是沒(méi)有使用 UIWebView 自己的方法去訪(fǎng)問(wèn)银室。
(2)使用 NSURLProtocol
目前對(duì)于網(wǎng)絡(luò)認(rèn)證相關(guān)的處理涂佃,效果最好、侵入性最小蜈敢、對(duì)已有代碼邏輯影響最小的辜荠,是 NSURLProtocol 方式。這里有個(gè)用于使 UIWebView 支持客戶(hù)端認(rèn)證的插件抓狭,對(duì)于服務(wù)端認(rèn)證一樣有效伯病,參見(jiàn) https://github.com/mwaylabs/cordova-plugin-client-certificate。
題外話(huà)否过,NSURLProtocol
對(duì)于很多特定場(chǎng)景來(lái)說(shuō)更為有效午笛。比如:曾經(jīng)有項(xiàng)目使用了 HTTP Basic Authorization 認(rèn)證。如果不使用 NSURLProtocol
的方案苗桂,可能會(huì)導(dǎo)致以下兩種情況不能通過(guò)認(rèn)證:
- 302 引發(fā)的跳轉(zhuǎn)不能自動(dòng)帶上認(rèn)證信息
- Web 頁(yè)面上的
<img>
药磺、<script>
標(biāo)簽、CSS煤伟、Ajax 請(qǐng)求不能通過(guò)認(rèn)證
三癌佩、客戶(hù)端認(rèn)證
對(duì)于雙向 HTTPS 認(rèn)證來(lái)說(shuō),服務(wù)端認(rèn)證是基礎(chǔ)便锨。客戶(hù)端認(rèn)證的前提围辙,是先實(shí)現(xiàn)服務(wù)端認(rèn)證,然后在此基礎(chǔ)上做一下補(bǔ)充放案。
在前文中服務(wù)端授權(quán)質(zhì)詢(xún)處理相關(guān)的描述中姚建,在對(duì)應(yīng)位置寫(xiě)客戶(hù)端認(rèn)證的代碼即可≈ㄑ常客戶(hù)端需要集成 PKCS12 格式的證書(shū)文件(由證書(shū)及其私鑰文件合成)掸冤,代碼中內(nèi)置對(duì)應(yīng)密碼厘托。
詳見(jiàn)代碼示例 iOSSSLDemo。
四贩虾、相關(guān)因素及討論
1催烘、證書(shū)加載
如果使用 AFNetworking 的話(huà)沥阱,加載證書(shū)非常簡(jiǎn)單缎罢,只要把格式為 DER
的證書(shū)(擴(kuò)展名一般為 .cer
或 .der
)集成到 Bundle,然后通過(guò)以下代碼來(lái)自動(dòng)加載:
policy.pinnedCertificates = [AFSecurityPolicy certificatesInBundle:[NSBundle mainBundle]];
如果需要在運(yùn)行時(shí)動(dòng)態(tài)加載臨時(shí)獲取的證書(shū)考杉,可以通過(guò)
policy.pinnedCertificates = [NSSet setWithObjects:cerData1, cerData2, nil];
來(lái)實(shí)現(xiàn)策精。其內(nèi)容為證書(shū)的 NSData
組成的 NSSet
。
2崇棠、證書(shū)的校驗(yàn)
@interface NSURLRequest (SSL)
@end
@implementation NSURLRequest (SSL)
+ (BOOL)allowsAnyHTTPSCertificateForHost:(NSString *)host {
return YES;
}
@end
這種使用 Category 的寫(xiě)法咽袜,也導(dǎo)致不會(huì)對(duì)證書(shū)進(jìn)行校驗(yàn)。不過(guò)此方法有兩個(gè)問(wèn)題:
- 不校驗(yàn)證書(shū)枕稀,導(dǎo)致安全級(jí)別降低询刹,容易被“中間人”方式攻擊;
- 此方法為私有方法萎坷,不建議使用凹联。
3、iOS 8
在 iOS 8 中哆档,如果使用 AFNetworking 來(lái)實(shí)現(xiàn)自簽名證書(shū)的認(rèn)證蔽挠,非常簡(jiǎn)單,只要代碼部分按結(jié)論中的描述來(lái)編寫(xiě)即可瓜浸。
有一點(diǎn)不太確定的澳淑,網(wǎng)上的資料說(shuō)必須加載證書(shū),但是實(shí)際測(cè)試插佛,不加也可以杠巡,這個(gè)可能跟 AFSSLPinningMode
有關(guān)系,不過(guò)由于目前 iOS 8 保有量很少了雇寇,所以不再深入了氢拥。
4、iOS 9+
對(duì)于 iOS 9+ 的情況谢床,蘋(píng)果加入了 ATS兄一,所以必須做 iOS 9 的適配 按照結(jié)論中說(shuō)的,修改 ATS 部分的設(shè)置识腿。
5出革、AFSSLPinningMode
AFSSLPinningMode
是安全策略的模式指定。
#import <Security/Security.h>
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
AFSSLPinningModeNone,
//表示不做SSL pinning渡讼,只跟瀏覽器一樣在系統(tǒng)的信任機(jī)構(gòu)列表里驗(yàn)證服務(wù)端返回的證書(shū)骂束。若證書(shū)是信任機(jī)構(gòu)簽發(fā)的就會(huì)通過(guò)耳璧,若是自己服務(wù)器生成的證書(shū),這里是不會(huì)通過(guò)的展箱。
AFSSLPinningModeCertificate,
//表示用證書(shū)綁定方式驗(yàn)證證書(shū)旨枯,需要客戶(hù)端保存有服務(wù)端的證書(shū)拷貝,這里驗(yàn)證分兩步混驰,第一步驗(yàn)證證書(shū)的域名/有效期等信息攀隔,第二步是對(duì)比服務(wù)端返回的證書(shū)跟客戶(hù)端返回的是否一致。
AFSSLPinningModePublicKey,
//這個(gè)模式同樣是用證書(shū)綁定方式驗(yàn)證栖榨,客戶(hù)端要有服務(wù)端的證書(shū)拷貝昆汹,只是驗(yàn)證時(shí)只驗(yàn)證證書(shū)里的公鑰,不驗(yàn)證證書(shū)的有效期等信息婴栽。
};
四满粗、代碼示例
五、參考資料
- iOS 9之適配ATS
- iOS9網(wǎng)絡(luò)適配_ATS:改用更安全的HTTPS
- iOS 自簽名證書(shū) HTTPS 請(qǐng)求(NSURLSession)
- AFSecurityPolicy 類(lèi)解析
- Overriding TLS Chain Validation Correctly
(完)