本文作者 ~ 涂耀輝支持作者,請(qǐng)閱讀原創(chuàng) ~ 原文鏈接

寫在開頭:
- 本來這篇內(nèi)容準(zhǔn)備寫在AFNetworking到底做了什么?(三)中的,但是因?yàn)槲蚁朐谌型杲Y(jié)這個(gè)系列,礙于篇幅所限、并且這一塊內(nèi)容獨(dú)立性比較強(qiáng),所以單獨(dú)拎出來,寫成一篇。
- 本文從源碼的角度,去分析AFNetworking對(duì)https的認(rèn)證過程。旨在讓讀者明白我們?nèi)プ鰄ttps請(qǐng)求:
- 如果使用AF,需要做什么。
- 不使用的話,直接用原生NSUrlSession,又需要做什么。
- 當(dāng)我們使用自簽證書的https,又需要注意哪些問題。
單獨(dú)看并不影響閱讀。如果有需要了解更多AF相關(guān)內(nèi)容,可以關(guān)注樓主的系列文章:
- 單獨(dú)看并不影響閱讀。如果有需要了解更多AF相關(guān)內(nèi)容,可以關(guān)注樓主的系列文章:
AFNetworking到底做了什么?
AFNetworking到底做了什么?(二)
那么正文開始了:
簡(jiǎn)單的理解下https:https在http請(qǐng)求的基礎(chǔ)上多加了一個(gè)證書認(rèn)證的流程。認(rèn)證通過之后,數(shù)據(jù)傳輸都是加密進(jìn)行的。關(guān)于https的更多概念,我就不贅述了,網(wǎng)上有大量的文章,小伙伴們可以自行查閱。在這里大概的講講https的認(rèn)證過程吧,如下圖所示:

1. 客戶端發(fā)起HTTPS請(qǐng)求
這個(gè)沒什么好說的,就是用戶在瀏覽器里輸入一個(gè)https網(wǎng)址,然后連接到server的443端口。
2. 服務(wù)端的配置
采用HTTPS協(xié)議的服務(wù)器必須要有一套數(shù)字證書,可以自己制作,也可以向組織申請(qǐng)。區(qū)別就是自己頒發(fā)的證書需要客戶端驗(yàn)證通過,才可以繼續(xù)訪問,而使用受信任的公司申請(qǐng)的證書則不會(huì)彈出提示頁面。這套證書其實(shí)就是一對(duì)公鑰和私鑰。如果對(duì)公鑰和私鑰不太理解,可以想象成一把鑰匙和一個(gè)鎖頭,只是全世界只有你一個(gè)人有這把鑰匙,你可以把鎖頭給別人,別人可以用這個(gè)鎖把重要的東西鎖起來,然后發(fā)給你,因?yàn)橹挥心阋粋€(gè)人有這把鑰匙,所以只有你才能看到被這把鎖鎖起來的東西。
3. 傳送證書
這個(gè)證書其實(shí)就是公鑰,只是包含了很多信息,如證書的頒發(fā)機(jī)構(gòu),過期時(shí)間等等。4. 客戶端解析證書
這部分工作是有客戶端的TLS/SSL來完成的,首先會(huì)驗(yàn)證公鑰是否有效,比如頒發(fā)機(jī)構(gòu),過期時(shí)間等等,如果發(fā)現(xiàn)異常,則會(huì)彈出一個(gè)警告框,提示證書存在問題。如果證書沒有問題,那么就生成一個(gè)隨機(jī)值。然后用證書對(duì)該隨機(jī)值進(jìn)行加密。就好像上面說的,把隨機(jī)值用鎖頭鎖起來,這樣除非有鑰匙,不然看不到被鎖住的內(nèi)容。
5. 傳送加密信息
這部分傳送的是用證書加密后的隨機(jī)值,目的就是讓服務(wù)端得到這個(gè)隨機(jī)值,以后客戶端和服務(wù)端的通信就可以通過這個(gè)隨機(jī)值來進(jìn)行加密解密了。
6. 服務(wù)段解密信息
服務(wù)端用私鑰解密后,得到了客戶端傳過來的隨機(jī)值(私鑰),然后把內(nèi)容通過該值進(jìn)行對(duì)稱加密。所謂對(duì)稱加密就是,將信息和私鑰通過某種算法混合在一起,這樣除非知道私鑰,不然無法獲取內(nèi)容,而正好客戶端和服務(wù)端都知道這個(gè)私鑰,所以只要加密算法夠彪悍,私鑰夠復(fù)雜,數(shù)據(jù)就夠安全。
7. 傳輸加密后的信息
這部分信息是服務(wù)段用私鑰加密后的信息,可以在客戶端被還原。
8. 客戶端解密信息
客戶端用之前生成的私鑰解密服務(wù)段傳過來的信息,于是獲取了解密后的內(nèi)容。整個(gè)過程第三方即使監(jiān)聽到了數(shù)據(jù),也束手無策。
這就是整個(gè)https驗(yàn)證的流程了。簡(jiǎn)單總結(jié)一下:
- 就是用戶發(fā)起請(qǐng)求,服務(wù)器響應(yīng)后返回一個(gè)證書,證書中包含一些基本信息和公鑰。
- 用戶拿到證書后,去驗(yàn)證這個(gè)證書是否合法,不合法,則請(qǐng)求終止。
- 合法則生成一個(gè)隨機(jī)數(shù),作為對(duì)稱加密的密鑰,用服務(wù)器返回的公鑰對(duì)這個(gè)隨機(jī)數(shù)加密。然后返回給服務(wù)器。
- 服務(wù)器拿到加密后的隨機(jī)數(shù),利用私鑰解密,然后再用解密后的隨機(jī)數(shù)(對(duì)稱密鑰),把需要返回的數(shù)據(jù)加密,加密完成后數(shù)據(jù)傳輸給用戶。
- 最后用戶拿到加密的數(shù)據(jù),用一開始的那個(gè)隨機(jī)數(shù)(對(duì)稱密鑰),進(jìn)行數(shù)據(jù)解密。整個(gè)過程完成。
當(dāng)然這僅僅是一個(gè)單向認(rèn)證,https還會(huì)有雙向認(rèn)證,相對(duì)于單向認(rèn)證也很簡(jiǎn)單。僅僅多了服務(wù)端驗(yàn)證客戶端這一步。感興趣的可以看看這篇:Https單向認(rèn)證和雙向認(rèn)證。
了解了https認(rèn)證流程后,接下來我們來講講AFSecurityPolicy這個(gè)類,AF就是用這個(gè)類來滿足我們各種https認(rèn)證需求。
在這之前我們來看看AF用來做https認(rèn)證的代理:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
//挑戰(zhàn)處理類型為 默認(rèn)
/*
NSURLSessionAuthChallengePerformDefaultHandling:默認(rèn)方式處理
NSURLSessionAuthChallengeUseCredential:使用指定的證書
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑戰(zhàn)
*/
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
// sessionDidReceiveAuthenticationChallenge是自定義方法,用來如何應(yīng)對(duì)服務(wù)器端的認(rèn)證挑戰(zhàn)
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
// 此處服務(wù)器要求客戶端的接收認(rèn)證挑戰(zhàn)方法是NSURLAuthenticationMethodServerTrust
// 也就是說服務(wù)器端需要客戶端返回一個(gè)根據(jù)認(rèn)證挑戰(zhàn)的保護(hù)空間提供的信任(即challenge.protectionSpace.serverTrust)產(chǎn)生的挑戰(zhàn)證書。
// 而這個(gè)證書就需要使用credentialForTrust:來創(chuàng)建一個(gè)NSURLCredential對(duì)象
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 基于客戶端的安全策略來決定是否信任該服務(wù)器,不信任的話,也就沒必要響應(yīng)挑戰(zhàn)
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 創(chuàng)建挑戰(zhàn)證書(注:挑戰(zhàn)方式為UseCredential和PerformDefaultHandling都需要新建挑戰(zhàn)證書)
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 確定挑戰(zhàn)的方式
if (credential) {
//證書挑戰(zhàn) 設(shè)計(jì)policy,none,則跑到這里
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
//取消挑戰(zhàn)
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
//默認(rèn)挑戰(zhàn)方式
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
//完成挑戰(zhàn)
if (completionHandler) {
completionHandler(disposition, credential);
}
}
更多的這個(gè)方法的細(xì)節(jié)問題,可以看注釋,或者查閱樓主之前的相關(guān)文章,都有去講到這個(gè)代理方法。在這里我們大概的講講這個(gè)方法做了什么:
1)首先指定了https為默認(rèn)的認(rèn)證方式。
2)判斷有沒有自定義Block:sessionDidReceiveAuthenticationChallenge,有的話,使用我們自定義Block,生成一個(gè)認(rèn)證方式,并且可以給credential賦值,即我們需要接受認(rèn)證的證書。然后直接調(diào)用completionHandler,去根據(jù)這兩個(gè)參數(shù),執(zhí)行系統(tǒng)的認(rèn)證。至于這個(gè)系統(tǒng)的認(rèn)證到底做了什么,可以看文章最后,這里暫且略過。
3)如果沒有自定義Block,我們判斷如果服務(wù)端的認(rèn)證方法要求是NSURLAuthenticationMethodServerTrust
,則只需要驗(yàn)證服務(wù)端證書是否安全(即https的單向認(rèn)證,這是AF默認(rèn)處理的認(rèn)證方式,其他的認(rèn)證方式,只能由我們自定義Block的實(shí)現(xiàn))
4)接著我們就執(zhí)行了AFSecurityPolicy相關(guān)的一個(gè)方法,做了一個(gè)AF內(nèi)部的一個(gè)https認(rèn)證:
[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host])
AF默認(rèn)的處理是,如果這行返回NO、說明AF內(nèi)部認(rèn)證失敗,則取消https認(rèn)證,即取消請(qǐng)求。返回YES則進(jìn)入if塊,用服務(wù)器返回的一個(gè)serverTrust去生成了一個(gè)認(rèn)證證書。(注:這個(gè)serverTrust是服務(wù)器傳過來的,里面包含了服務(wù)器的證書信息,是用來我們本地客戶端去驗(yàn)證該證書是否合法用的,后面會(huì)更詳細(xì)的去講這個(gè)參數(shù))然后如果有證書,則用證書認(rèn)證方式,否則還是用默認(rèn)的驗(yàn)證方式。最后調(diào)用completionHandler傳遞認(rèn)證方式和要認(rèn)證的證書,去做系統(tǒng)根證書驗(yàn)證。
- 總結(jié)一下這里
securityPolicy存在的作用就是,使得在系統(tǒng)底層自己去驗(yàn)證之前,AF可以先去驗(yàn)證服務(wù)端的證書。如果通不過,則直接越過系統(tǒng)的驗(yàn)證,取消https的網(wǎng)絡(luò)請(qǐng)求。否則,繼續(xù)去走系統(tǒng)根證書的驗(yàn)證。
接下來我們看看AFSecurityPolicy 內(nèi)部是如果做https認(rèn)證的:
如下方式,我們可以創(chuàng)建一個(gè)securityPolicy:
AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];
內(nèi)部創(chuàng)建:
+ (instancetype)defaultPolicy {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
return securityPolicy;
}
默認(rèn)指定了一個(gè)SSLPinningMode模式為AFSSLPinningModeNone。
對(duì)于AFSecurityPolicy,一共有4個(gè)重要的屬性:
//https驗(yàn)證模式
@property (readonly, nonatomic, assign) AFSSLPinningMode SSLPinningMode;
//可以去匹配服務(wù)端證書驗(yàn)證的證書
@property (nonatomic, strong, nullable) NSSet <NSData *> *pinnedCertificates;
//是否支持非法的證書(例如自簽名證書)
@property (nonatomic, assign) BOOL allowInvalidCertificates;
//是否去驗(yàn)證證書域名是否匹配
@property (nonatomic, assign) BOOL validatesDomainName;
它們的作用我添加在注釋里了,第一條就是AFSSLPinningMode, 共提供了3種驗(yàn)證方式:
typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
//不驗(yàn)證
AFSSLPinningModeNone,
//只驗(yàn)證公鑰
AFSSLPinningModePublicKey,
//驗(yàn)證證書
AFSSLPinningModeCertificate,
};
我們接著回到代理https認(rèn)證的這行代碼上:
[self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host];
我們傳了兩個(gè)參數(shù)進(jìn)去,一個(gè)是SecTrustRef類型的serverTrust,這是什么呢?我們看到蘋果的文檔介紹如下:CFType used for performing X.509 certificate trust evaluations.
大概意思是用于執(zhí)行X。509證書信任評(píng)估,
再講簡(jiǎn)單點(diǎn),其實(shí)就是一個(gè)容器,裝了服務(wù)器端需要驗(yàn)證的證書的基本信息、公鑰等等,不僅如此,它還可以裝一些評(píng)估策略,還有客戶端的錨點(diǎn)證書,這個(gè)客戶端的證書,可以用來和服務(wù)端的證書去匹配驗(yàn)證的。
- 除此之外還把服務(wù)器域名傳了過去。
我們來到這個(gè)方法,代碼如下:
//驗(yàn)證服務(wù)端是否值得信任
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
//判斷矛盾的條件
//判斷有域名,且允許自建證書,需要驗(yàn)證域名,
//因?yàn)橐?yàn)證域名,所以必須不能是后者兩種:AFSSLPinningModeNone或者添加到項(xiàng)目里的證書為0個(gè)。
if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
// https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
// According to the docs, you should only trust your provided certs for evaluation.
// Pinned certificates are added to the trust. Without pinned certificates,
// there is nothing to evaluate against.
//
// From Apple Docs:
// "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
// Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
//不受信任,返回
return NO;
}
//用來裝驗(yàn)證策略
NSMutableArray *policies = [NSMutableArray array];
//要驗(yàn)證域名
if (self.validatesDomainName) {
// 如果需要驗(yàn)證domain,那么就使用SecPolicyCreateSSL函數(shù)創(chuàng)建驗(yàn)證策略,其中第一個(gè)參數(shù)為true表示驗(yàn)證整個(gè)SSL證書鏈,第二個(gè)參數(shù)傳入domain,用于判斷整個(gè)證書鏈上葉子節(jié)點(diǎn)表示的那個(gè)domain是否和此處傳入domain一致
//添加驗(yàn)證策略
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
// 如果不需要驗(yàn)證domain,就使用默認(rèn)的BasicX509驗(yàn)證策略
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
//serverTrust:X。509服務(wù)器的證書信任。
// 為serverTrust設(shè)置驗(yàn)證策略,即告訴客戶端如何驗(yàn)證serverTrust
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
//有驗(yàn)證策略了,可以去驗(yàn)證了。如果是AFSSLPinningModeNone,是自簽名,直接返回可信任,否則不是自簽名的就去系統(tǒng)根證書里去找是否有匹配的證書。
if (self.SSLPinningMode == AFSSLPinningModeNone) {
//如果支持自簽名,直接返回YES,不允許才去判斷第二個(gè)條件,判斷serverTrust是否有效
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
}
//如果驗(yàn)證無效AFServerTrustIsValid,而且allowInvalidCertificates不允許自簽,返回NO
else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
return NO;
}
//判斷SSLPinningMode
switch (self.SSLPinningMode) {
// 理論上,上面那個(gè)部分已經(jīng)解決了self.SSLPinningMode)為AFSSLPinningModeNone)等情況,所以此處再遇到,就直接返回NO
case AFSSLPinningModeNone:
default:
return NO;
//驗(yàn)證證書類型
case AFSSLPinningModeCertificate: {
NSMutableArray *pinnedCertificates = [NSMutableArray array];
//把證書data,用系統(tǒng)api轉(zhuǎn)成 SecCertificateRef 類型的數(shù)據(jù),SecCertificateCreateWithData函數(shù)對(duì)原先的pinnedCertificates做一些處理,保證返回的證書都是DER編碼的X.509證書
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 將pinnedCertificates設(shè)置成需要參與驗(yàn)證的Anchor Certificate(錨點(diǎn)證書,通過SecTrustSetAnchorCertificates設(shè)置了參與校驗(yàn)錨點(diǎn)證書之后,假如驗(yàn)證的數(shù)字證書是這個(gè)錨點(diǎn)證書的子節(jié)點(diǎn),即驗(yàn)證的數(shù)字證書是由錨點(diǎn)證書對(duì)應(yīng)CA或子CA簽發(fā)的,或是該證書本身,則信任該證書),具體就是調(diào)用SecTrustEvaluate來驗(yàn)證。
//serverTrust是服務(wù)器來的驗(yàn)證,有需要被驗(yàn)證的證書。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//自簽在之前是驗(yàn)證通過不了的,在這一步,把我們自己設(shè)置的證書加進(jìn)去之后,就能驗(yàn)證成功了。
//再去調(diào)用之前的serverTrust去驗(yàn)證該證書是否有效,有可能:經(jīng)過這個(gè)方法過濾后,serverTrust里面的pinnedCertificates被篩選到只有信任的那一個(gè)證書
if (!AFServerTrustIsValid(serverTrust)) {
return NO;
}
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
//注意,這個(gè)方法和我們之前的錨點(diǎn)證書沒關(guān)系了,是去從我們需要被驗(yàn)證的服務(wù)端證書,去拿證書鏈。
// 服務(wù)器端的證書鏈,注意此處返回的證書鏈順序是從葉節(jié)點(diǎn)到根節(jié)點(diǎn)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
//reverseObjectEnumerator逆序
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
//如果我們的證書中,有一個(gè)和它證書鏈中的證書匹配的,就返回YES
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
//沒有匹配的
return NO;
}
//公鑰驗(yàn)證 AFSSLPinningModePublicKey模式同樣是用證書綁定(SSL Pinning)方式驗(yàn)證,客戶端要有服務(wù)端的證書拷貝,只是驗(yàn)證時(shí)只驗(yàn)證證書里的公鑰,不驗(yàn)證證書的有效期等信息。只要公鑰是正確的,就能保證通信不會(huì)被竊聽,因?yàn)橹虚g人沒有私鑰,無法解開通過公鑰加密的數(shù)據(jù)。
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
// 從serverTrust中取出服務(wù)器端傳過來的所有可用的證書,并依次得到相應(yīng)的公鑰
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
//遍歷服務(wù)端公鑰
for (id trustChainPublicKey in publicKeys) {
//遍歷本地公鑰
for (id pinnedPublicKey in self.pinnedPublicKeys) {
//判斷如果相同 trustedPublicKeyCount+1
if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
trustedPublicKeyCount += 1;
}
}
}
return trustedPublicKeyCount > 0;
}
}
return NO;
}
代碼的注釋很多,這一塊確實(shí)比枯澀,大家可以參照著源碼一起看,加深理解。
- 這個(gè)方法是``AFSecurityPolicy``最核心的方法,其他的都是為了配合這個(gè)方法。這個(gè)方法完成了服務(wù)端的證書的信任評(píng)估。我們總結(jié)一下這個(gè)方法做了什么(細(xì)節(jié)可以看注釋):
1. 根據(jù)模式,如果是``AFSSLPinningModeNone``,則肯定是返回YES,不論是自簽還是公信機(jī)構(gòu)的證書。
2. 如果是``AFSSLPinningModeCertificate``,則從``serverTrust``中去獲取證書鏈,然后和我們一開始初始化設(shè)置的證書集合``self.pinnedCertificates``去匹配,**如果有一對(duì)能匹配成功的,就返回YES,否則NO。**看到這可能有小伙伴要問了,什么是證書鏈?下面這段是我從百科上摘來的:
``證書鏈由兩個(gè)環(huán)節(jié)組成—信任錨(CA 證書)環(huán)節(jié)和已簽名證書環(huán)節(jié)。自我簽名的證書僅有一個(gè)環(huán)節(jié)的長(zhǎng)度—信任錨環(huán)節(jié)就是已簽名證書本身。``
簡(jiǎn)單來說,證書鏈就是就是根證書,和根據(jù)根證書簽名派發(fā)得到的證書。
3. 如果是``AFSSLPinningModePublicKey``公鑰驗(yàn)證,則和第二步一樣還是從``serverTrust``,獲取證書鏈每一個(gè)證書的公鑰,放到數(shù)組中。和我們的``self.pinnedPublicKeys``,去配對(duì),如果有一個(gè)相同的,就返回YES,否則NO。至于這個(gè)``self.pinnedPublicKeys``,初始化的地方如下:
//設(shè)置證書數(shù)組
- (void)setPinnedCertificates:(NSSet *)pinnedCertificates {
_pinnedCertificates = pinnedCertificates;
//獲取對(duì)應(yīng)公鑰集合
if (self.pinnedCertificates) {
//創(chuàng)建公鑰集合
NSMutableSet *mutablePinnedPublicKeys = [NSMutableSet setWithCapacity:[self.pinnedCertificates count]];
//從證書中拿到公鑰。
for (NSData *certificate in self.pinnedCertificates) {
id publicKey = AFPublicKeyForCertificate(certificate);
if (!publicKey) {
continue;
}
[mutablePinnedPublicKeys addObject:publicKey];
}
self.pinnedPublicKeys = [NSSet setWithSet:mutablePinnedPublicKeys];
} else {
self.pinnedPublicKeys = nil;
}
}
AF復(fù)寫了設(shè)置證書的set方法,并同時(shí)把證書中每個(gè)公鑰放在了self.pinnedPublicKeys中。
這個(gè)方法中關(guān)聯(lián)了一系列的函數(shù),我在這邊按照調(diào)用順序一一列出來(有些是系統(tǒng)函數(shù),不在這里列出,會(huì)在下文集體描述作用):
函數(shù)一:**AFServerTrustIsValid**
//判斷serverTrust是否有效
static BOOL AFServerTrustIsValid(SecTrustRef serverTrust) {
//默認(rèn)無效
BOOL isValid = NO;
//用來裝驗(yàn)證結(jié)果,枚舉
SecTrustResultType result;
//__Require_noErr_Quiet 用來判斷前者是0還是非0,如果0則表示沒錯(cuò),就跳到后面的表達(dá)式所在位置去執(zhí)行,否則表示有錯(cuò)就繼續(xù)往下執(zhí)行。
//SecTrustEvaluate系統(tǒng)評(píng)估證書的是否可信的函數(shù),去系統(tǒng)根目錄找,然后把結(jié)果賦值給result。評(píng)估結(jié)果匹配,返回0,否則出錯(cuò)返回非0
//do while 0 ,只執(zhí)行一次,為啥要這樣寫....
__Require_noErr_Quiet(SecTrustEvaluate(serverTrust, &result), _out);
//評(píng)估沒出錯(cuò)走掉這,只有兩種結(jié)果能設(shè)置為有效,isValid= 1
//當(dāng)result為kSecTrustResultUnspecified(此標(biāo)志表示serverTrust評(píng)估成功,此證書也被暗中信任了,但是用戶并沒有顯示地決定信任該證書)。
//或者當(dāng)result為kSecTrustResultProceed(此標(biāo)志表示評(píng)估成功,和上面不同的是該評(píng)估得到了用戶認(rèn)可),這兩者取其一就可以認(rèn)為對(duì)serverTrust評(píng)估成功
isValid = (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
//out函數(shù)塊,如果為SecTrustEvaluate,返回非0,則評(píng)估出錯(cuò),則isValid為NO
_out:
return isValid;
}
- 這個(gè)方法用來驗(yàn)證serverTrust是否有效,其中主要是交由系統(tǒng)APISecTrustEvaluate來驗(yàn)證的,它驗(yàn)證完之后會(huì)返回一個(gè)SecTrustResultType枚舉類型的result,然后我們根據(jù)這個(gè)result去判斷是否證書是否有效。
- 其中比較有意思的是,它調(diào)用了一個(gè)系統(tǒng)定義的宏函數(shù)__Require_noErr_Quiet,函數(shù)定義如下:
ifndef __Require_noErr_Quiet
define __Require_noErr_Quiet(errorCode, exceptionLabel) \
do \
{ \
if ( __builtin_expect(0 != (errorCode), 0) ) \
{ \
goto exceptionLabel; \
} \
} while ( 0 )
endif
這個(gè)函數(shù)主要作用就是,判斷``errorCode``是否為0,不為0則,程序用goto跳到exceptionLabel位置去執(zhí)行。這個(gè)``exceptionLabel``就是一個(gè)代碼位置標(biāo)識(shí),類似上面的``_out``。
說它有意思的地方是在于,它用了一個(gè)``do...while(0)``循環(huán),循環(huán)條件為0,也就是只執(zhí)行一次循環(huán)就結(jié)束。對(duì)這么做的原因,樓主百思不得其解...看來系統(tǒng)原生API更是高深莫測(cè)...經(jīng)冰霜大神的提醒,這么做是為了適配早期的API??!
**函數(shù)二、三(兩個(gè)函數(shù)類似,所以放在一起):獲取``serverTrust``證書鏈證書,獲取``serverTrust``證書鏈公鑰**
//獲取證書鏈
static NSArray * AFCertificateTrustChainForServerTrust(SecTrustRef serverTrust) {
//使用SecTrustGetCertificateCount函數(shù)獲取到serverTrust中需要評(píng)估的證書鏈中的證書數(shù)目,并保存到certificateCount中
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
//創(chuàng)建數(shù)組
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
//// 使用SecTrustGetCertificateAtIndex函數(shù)獲取到證書鏈中的每個(gè)證書,并添加到trustChain中,最后返回trustChain
for (CFIndex i = 0; i < certificateCount; i++) {
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
[trustChain addObject:(__bridge_transfer NSData *)SecCertificateCopyData(certificate)];
}
return [NSArray arrayWithArray:trustChain];
}
// 從serverTrust中取出服務(wù)器端傳過來的所有可用的證書,并依次得到相應(yīng)的公鑰
static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
// 接下來的一小段代碼和上面AFCertificateTrustChainForServerTrust函數(shù)的作用基本一致,都是為了獲取到serverTrust中證書鏈上的所有證書,并依次遍歷,取出公鑰。
//安全策略
SecPolicyRef policy = SecPolicyCreateBasicX509();
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
//遍歷serverTrust里證書的證書鏈。
for (CFIndex i = 0; i < certificateCount; i++) {
//從證書鏈取證書
SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);
//數(shù)組
SecCertificateRef someCertificates[] = {certificate};
//CF數(shù)組
CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);
SecTrustRef trust;
// 根據(jù)給定的certificates和policy來生成一個(gè)trust對(duì)象
//不成功跳到 _out。
__Require_noErr_Quiet(SecTrustCreateWithCertificates(certificates, policy, &trust), _out);
SecTrustResultType result;
// 使用SecTrustEvaluate來評(píng)估上面構(gòu)建的trust
//評(píng)估失敗跳到 _out
__Require_noErr_Quiet(SecTrustEvaluate(trust, &result), _out);
// 如果該trust符合X.509證書格式,那么先使用SecTrustCopyPublicKey獲取到trust的公鑰,再將此公鑰添加到trustChain中
[trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];
_out:
//釋放資源
if (trust) {
CFRelease(trust);
}
if (certificates) {
CFRelease(certificates);
}
continue;
}
CFRelease(policy);
// 返回對(duì)應(yīng)的一組公鑰
return [NSArray arrayWithArray:trustChain];
}
兩個(gè)方法功能類似,都是調(diào)用了一些系統(tǒng)的API,利用For循環(huán),獲取證書鏈上每一個(gè)證書或者公鑰。具體內(nèi)容看源碼很好理解。唯一需要注意的是,這個(gè)獲取的證書排序,是從證書鏈的葉節(jié)點(diǎn),到根節(jié)點(diǎn)的。
**函數(shù)四:判斷公鑰是否相同**
//判斷兩個(gè)公鑰是否相同
static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {
if TARGET_OS_IOS || TARGET_OS_WATCH || TARGET_OS_TV
//iOS 判斷二者地址
return [(__bridge id)key1 isEqual:(__bridge id)key2];
else
return [AFSecKeyGetData(key1) isEqual:AFSecKeyGetData(key2)];
endif
}
方法適配了各種運(yùn)行環(huán)境,做了匹配的判斷。
**接下來列出驗(yàn)證過程中調(diào)用過得系統(tǒng)原生函數(shù):**
//1.創(chuàng)建一個(gè)驗(yàn)證SSL的策略,兩個(gè)參數(shù),第一個(gè)參數(shù)true則表示驗(yàn)證整個(gè)證書鏈
//第二個(gè)參數(shù)傳入domain,用于判斷整個(gè)證書鏈上葉子節(jié)點(diǎn)表示的那個(gè)domain是否和此處傳入domain一致
SecPolicyCreateSSL(<#Boolean server#>, <#CFStringRef _Nullable hostname#>)
SecPolicyCreateBasicX509();
//2.默認(rèn)的BasicX509驗(yàn)證策略,不驗(yàn)證域名。
SecPolicyCreateBasicX509();
//3.為serverTrust設(shè)置驗(yàn)證策略,即告訴客戶端如何驗(yàn)證serverTrust
SecTrustSetPolicies(<#SecTrustRef _Nonnull trust#>, <#CFTypeRef _Nonnull policies#>)
//4.驗(yàn)證serverTrust,并且把驗(yàn)證結(jié)果返回給第二參數(shù) result
SecTrustEvaluate(<#SecTrustRef _Nonnull trust#>, <#SecTrustResultType * _Nullable result#>)
//5.判斷前者errorCode是否為0,為0則跳到exceptionLabel處執(zhí)行代碼
__Require_noErr(<#errorCode#>, <#exceptionLabel#>)
//6.根據(jù)證書data,去創(chuàng)建SecCertificateRef類型的數(shù)據(jù)。
SecCertificateCreateWithData(<#CFAllocatorRef _Nullable allocator#>, <#CFDataRef _Nonnull data#>)
//7.給serverTrust設(shè)置錨點(diǎn)證書,即如果以后再次去驗(yàn)證serverTrust,會(huì)從錨點(diǎn)證書去找是否匹配。
SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
//8.拿到證書鏈中的證書個(gè)數(shù)
CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
//9.去取得證書鏈中對(duì)應(yīng)下標(biāo)的證書。
SecTrustGetCertificateAtIndex(serverTrust, i)
//10.根據(jù)證書獲取公鑰。
SecTrustCopyPublicKey(trust)
其功能如注釋,大家可以對(duì)比著源碼,去加以理解~

可能看到這,又有些小伙伴迷糊了,講了這么多,**那如果做https請(qǐng)求,真正需要我們自己做的到底是什么呢?**這里來解答一下,分為以下兩種情況:
1. 如果你用的是付費(fèi)的公信機(jī)構(gòu)頒發(fā)的證書,標(biāo)準(zhǔn)的https,**那么無論你用的是AF還是NSUrlSession,什么都不用做,代理方法也不用實(shí)現(xiàn)。**你的網(wǎng)絡(luò)請(qǐng)求就能正常完成。
2. 如果你用的是自簽名的證書:
- 首先你需要在plist文件中,設(shè)置可以返回不安全的請(qǐng)求(關(guān)閉該域名的ATS)。
- 其次,如果是NSUrlSesion,那么需要在代理方法實(shí)現(xiàn)如下:
-
(void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
__block NSURLCredential *credential = nil;credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// 確定挑戰(zhàn)的方式
if (credential) {
//證書挑戰(zhàn) 則跑到這里
disposition = NSURLSessionAuthChallengeUseCredential;
}
//完成挑戰(zhàn)
if (completionHandler) {
completionHandler(disposition, credential);
}
}
其實(shí)上述就是AF的相對(duì)于自簽證書的實(shí)現(xiàn)的簡(jiǎn)化版。
如果是AF,你則需要設(shè)置policy:
//允許自簽名證書,必須的
policy.allowInvalidCertificates = YES;
//是否驗(yàn)證域名的CN字段
//不是必須的,但是如果寫YES,則必須導(dǎo)入證書。
policy.validatesDomainName = NO;
當(dāng)然還可以根據(jù)需求,你可以去驗(yàn)證證書或者公鑰,前提是,你把自簽的服務(wù)端證書,或者自簽的CA根證書導(dǎo)入到項(xiàng)目中:

并且如下設(shè)置證書:
NSString *certFilePath = [[NSBundle mainBundle] pathForResource:@"AFUse_server.cer" ofType:nil];
NSData *certData = [NSData dataWithContentsOfFile:certFilePath];
NSSet *certSet = [NSSet setWithObjects:certData,certData, nil];
policy.pinnedCertificates = certSet;
這樣你就可以使用AF的不同AFSSLPinningMode
去驗(yàn)證了。
最后總結(jié)一下,**AF**之于**https**到底做了什么:
- **AF可以讓你在系統(tǒng)驗(yàn)證證書之前,就去自主驗(yàn)證。**然后如果自己驗(yàn)證不正確,直接取消網(wǎng)絡(luò)請(qǐng)求。否則驗(yàn)證通過則繼續(xù)進(jìn)行系統(tǒng)驗(yàn)證。
- 講到這,順便提一下,系統(tǒng)驗(yàn)證的流程:
- 系統(tǒng)的驗(yàn)證,首先是去系統(tǒng)的根證書找,看是否有能匹配服務(wù)端的證書,如果匹配,則驗(yàn)證成功,返回https的安全數(shù)據(jù)。
- 如果不匹配則去判斷ATS是否關(guān)閉,如果關(guān)閉,則返回https不安全連接的數(shù)據(jù)。如果開啟ATS,則拒絕這個(gè)請(qǐng)求,請(qǐng)求失敗。
總之一句話:**AF的驗(yàn)證方式不是必須的,但是對(duì)有特殊驗(yàn)證需求的用戶確是必要的**。
寫在結(jié)尾:
- 看完之后,有些小伙伴可能還是會(huì)比較迷惑,建議還是不清楚的小伙伴,可以自己生成一個(gè)自簽名的證書或者用百度地址等做請(qǐng)求,然后設(shè)置AFSecurityPolicy
不同參數(shù),打斷點(diǎn),一步步的看AF是如何去調(diào)用函數(shù)作證書驗(yàn)證的。相信這樣能加深你的理解。
- 最后關(guān)于自簽名證書的問題,等2017年1月1日,也沒多久了...一個(gè)月不到。除非有特殊原因說明,否則已經(jīng)無法審核通過了。詳細(xì)的可以看看這篇文章:[iOS 10 適配 ATS(app支持https通過App Store審核)](http://www.itdecent.cn/p/36ddc5b009a7)。
- 最后的最后,希望大家能點(diǎn)個(gè)贊,關(guān)注一下~(樓主看到贊和關(guān)注會(huì)很開心...) 有什么不同意見或者建議可以評(píng)論或者簡(jiǎn)信我~萬一有人轉(zhuǎn)載,麻煩注明出處,謝謝~~
**后續(xù)文章:**
[AFNetworking之UIKit擴(kuò)展與緩存實(shí)現(xiàn)](http://www.itdecent.cn/p/4ffeb1ba3046)
[AFNetworking到底做了什么?(終)](http://www.itdecent.cn/p/7ed7c0be15b4)