這一篇主要介紹使用AFN如何訪問HTTPS網站以及這些作法的實現原理,還有介紹AFN的網絡狀態監測部分AFNetworkReachabilityManager,這個模塊會和蘋果官方推薦的Reachability框架作一個對比。git
本文全部的代碼都運行在iOS9.2的模擬器上,而且在info.plist對ATS作了適配:設置容許非法的加載Allow Arbitrary Loads爲YES。
不要認爲在info.plist添加NSAppTransportSecurity
> NSAllowsArbitraryLoads
爲YES
就覺得弄懂iOS9網絡適配了,有關具體細節問題請看南峯子的這篇文章App Transport Security(ATS)github
介於iOS有關HTTPS訪問的認證過程代碼並非特別常用,本文會用大量的篇幅介紹HTTPS認證的過程,並會經過系統的NSURLSession完成一些認證相關的代碼,畢竟AFN就是使用了這些代碼來實現對HTTPS網站的訪問支持的。web
不一樣於普通的HTTP請求,當訪問一個HTTPS的網站時,瀏覽器會幫咱們不少隱藏的工做,這實際上是SSL通道創建的三次握手過程:
1.發起請求。
首先當輸入完https網址敲擊回車以後,瀏覽器首先向服務器發送一個須要訪問的請求,這個請求中包含着瀏覽器SSL 協議的版本號,加密算法的種類,產生的隨機數,以及其餘服務器和客戶端之間通信所須要的各類信息。
2.服務端返回證書。
服務器向客戶端傳送SSL 協議的版本號,加密算法的種類,隨機數以及其餘相關信息,同時服務器還將向客戶端傳送本身的證書,這些信息被保存在客戶端被稱做'被保護空間'的地方。這裏最關鍵的就是證書信息。
3.瀏覽器驗證證書信息。
瀏覽器利用服務器傳過來的信息驗證服務器的合法性,服務器的合法性包括:證書是否過時,發行服務器證書的CA 是否可靠,發行者證書的公鑰可否正確解開服務器證書的「發行者的數字簽名」,服務器證書上的域名是否和服務器的實際域名相匹配。
若是合法性驗證沒有經過,通信將斷開;若是合法性驗證經過,將繼續進行第四步。
4.客戶端向服務器發送「預主密碼」。
瀏覽器隨機產生一個用於後面通信的「對稱密碼」,而後用服務器的公鑰(服務器的公鑰從步驟②中的服務器的證書中得到)對其加密,而後將加密後的「預主密碼」傳給服務器。算法
4.1.若是服務器要求客戶的身份認證(在握手過程當中爲可選),用戶不光要傳給服務器「預主密碼」,還需創建一個隨機數而後對其進行數據簽名,將這個含有簽名的隨機數和客戶本身的證書也傳給服務器。數組
4.2.若是不須要,則只將「預主密碼」傳給服務器,並直接進行第6步。
5.服務端身份驗證(須要才進行)。
若是服務器要求客戶的身份認證,服務器必須檢驗客戶證書和簽名隨機數的合法性,具體的合法性驗證過程包括:客戶的證書使用日期是否有效,爲客戶提供證書的CA 是否可靠,發行CA 的公鑰可否正確解開客戶證書的發行CA 的數字簽名,檢查客戶的證書是否在證書廢止列表(CRL)中。
檢驗若是沒有經過,通信馬上中斷;
若是驗證經過,進行下一步。
6.瀏覽器、服務端各自生成通話密碼。
服務器將用本身的私鑰解開加密的「預主密碼」,而後執行一系列步驟來產生主通信密碼(客戶端也將經過一樣的方法產生相同的主通信密碼)。
7.約定通話密碼。
服務器和客戶端用相同的主通信密碼即「通話密碼」,一個對稱密鑰用於SSL 協議的安全數據通信的加解密通信。同時在SSL 通信過程當中還要完成數據通信的完整性,防止數據通信中的任何變化。
8.瀏覽器通知服務器已準備就緒。
客戶端向服務器端發出信息,指明後面的數據通信將使用的步驟⑦中的主密碼爲對稱密鑰,同時通知服務器客戶端的握手過程結束。
9.服務端通知瀏覽器已準備就緒。
服務器向客戶端發出信息,指明後面的數據通信將使用的步驟⑦中的主密碼爲對稱密鑰,同時通知客戶端服務器端的握手過程結束。
10.開始數據通信。
SSL 的握手部分結束,SSL安全通道創建完成,開始進行數據通信開始,通信過程當中客戶和服務器開始使用相同的對稱密鑰。
若是以https://www.baidu.com爲例,這時候已經表現爲baidu的主頁打開了,可是SSL加密通道在下次請求的時候不用再次創建。xcode
對於訪問的過程當中,一般會在第3步出現問題,以12306的購票頁面爲例:
當進行到第3步的時候,瀏覽器驗證爲:發行服務器證書的CA是不可靠的,能夠在Chrome的地址欄中點擊被打了紅叉的鎖來查看這個頁面的證書頒發機構,
咱們能夠搜索到這個命名爲'SRCA'的機構其實是‘中鐵認證中心’也就是12306本身的認證系統,它是用了本身的認證系統給本身頒發了一個SSL加密證書,而Chrome怎麼會承認它呢。順便看了一下百度的證書:
這是一個由美國Symantec Trust Network組織頒發的證書,是一個比較權威的證書頒發機構,幾乎在全部的瀏覽器中都是承認的。而baidu使用的證書是這個機構的根證書的子證書,而之因此瀏覽器能承認它,是由於根證書經過webtrust國際認證,並已經內置到各大瀏覽器如谷歌,火狐,微軟等系統中。
那麼這畢竟只是瀏覽器默認的一種認證方式,畢竟咱們仍是須要訪問12306的,這裏就要改變一下第3步驗證的結果,在瀏覽器中,咱們能夠手動選擇信任,而後繼續向下進行。
這樣就能訪問這些網站了。瀏覽器
與瀏覽器的驗證過程類似,iOS的HTTPS驗證過程也要走相似的步驟,不過不用擔憂的是,不少過程咱們也不須要處理,只須要處理好第3步就好了,當咱們進行訪問一個HTTPS網站時,當走到第二步的時候,也就是服務器返回證書時,須要咱們在本地本身完成證書信任的過程,若是使用session建立的task進行網絡訪問,這時候就會進入到- URLSession:didReceiveChallenge:completionHandler:
這個代理方法中,這時候已經完成了HTTPS訪問的第二步,session會讓咱們在這個方法中完成第3步的過程。這個方法的參數有以下的解釋:安全
參數 | 解釋 |
---|---|
challenge | 一個包含了受權請求的對象 |
completionHandler | 你的代理方法必定會調用的一個handler. 它的參數是 disposition—描述challenge如何被處理的幾個常量中的一個 credential—若是disposition是NSURLSessionAuthChallengeUseCredential,credential是受權驗證時會被使用到的憑據,其餘狀況爲NULL. |
challenge參數須要另外說明的是challenge
是一個NSURLAuthenticationChallenge
對象,表明着進行https請求進行時,服務端發送過來的質詢,當接收到質詢以後就要開始進行客戶端的驗證了。服務器
這個對象中最重要的屬性就是protectionSpace
它表明着對須要驗證的受保護空間的驗證,是一個NSURLProtectionSpace
類型的對象。NSURLProtectionSpace對象包含請求的主機host、端口號port、代理類型proxyType、使用的協議protocol、服務端要求客戶端對其驗證的方法authenticationMethod等重要的信息,還有表明着服務器SSL傳輸狀態的SecTrustRef
類型的屬性serverTrust,不過當且僅當authenticationMethod爲NSURLAuthenticationMethodServerTrust這個屬性值纔不爲Nil.網絡
這裏還要說明一下服務端指定的驗證方法的類型,驗證方法的類型有不少種,這裏再也不一一列舉,咱們一般會見到這樣幾種類型:
NSURLAuthenticationMethodHTTPBasic NSURLAuthenticationMethodHTTPDigest NSURLAuthenticationMethodNTLM NSURLAuthenticationMethodClientCertificate NSURLAuthenticationMethodServerTrust
其中HTTP Basic、HTTP Digest與NTLM認證都是基於用戶名/密碼的認證,ClientCertificate(客戶端證書)認證要求從客戶端上傳證書。客戶端須要按照服務端指定的認證方法進行認證,不然可能會按照錯誤處理。例如使用HTTP Basic方式,客戶端須要將用戶名和密碼信息放到憑據中,而後傳遞給服務端;若是使用的是ServerTrust方式,那麼客戶端就要將信任的憑據發給服務端。
通常在HTTPS訪問的第3步過程當中,服務端要求的認證方法幾乎老是ServerTrust方式。有遇到過一些網絡代理工具使用HTTP Digest的驗證方式,在瀏覽器端進行訪問的時候就彈出一個要求輸入帳號和密碼的彈窗。
對於completionHandler參數是一個最終處理憑據的回調,要求在建立好包含驗證信息的憑據以後必須調用,這樣纔會將驗證的信息發送給服務端,也就意味着第3步的完成,開始進行第4步。
它的第一個參數是處理的選項,是一個枚舉類型:
typedef NS_ENUM(NSInteger, NSURLSessionAuthChallengeDisposition) { NSURLSessionAuthChallengeUseCredential = 0, // 使用服務器發回的憑據,不過可能爲空 NSURLSessionAuthChallengePerformDefaultHandling = 1, // 默認的處理方法,憑據參數會被忽略 NSURLSessionAuthChallengeCancelAuthenticationChallenge = 2, //取消整個請求,忽略憑據參數 NSURLSessionAuthChallengeRejectProtectionSpace = 3, // 此次質詢被拒絕,下次再試 ,憑據參數被忽略 } NS_ENUM_AVAILABLE(NSURLSESSION_AVAILABLE, 7_0);
理清上面的思路以後,咱們能夠試一試使用系統的session訪問HTTPS網站了:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSURL *url = [NSURL URLWithString:@"https://www.baidu.com"]; [[self.session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error) { NSLog(@"%@", error); return ; } NSLog(@"%@", response); }] resume]; } #pragma mark - NSURLSessionDelegate - (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * __nullable credential))completionHandler { // 判斷服務器的身份驗證的方法是不是:ServerTrust方式 if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { // 建立一個新憑據,這個憑據指定了'握手'是被信任的 NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; if (credential != nil) { // 完成'處置',將信任憑據發給服務端 completionHandler(NSURLSessionAuthChallengeUseCredential, credential); } // 若是credential == nil 如下回調會自動完成 // completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, credential); } }
由於咱們使用的是使用第2步中服務端傳回來的證書,因此即便是對付https://kyfw.12306.cn/otn/leftTicket/init這樣的流氓頁面也一樣是能夠的。可是對於iOS9來講並非這樣,必須設置了Allow Arbitrary Loads爲YES纔會達到預期效果。
對於AFN,不管實在iOS9以前仍是iOS9以後,當訪問https://kyfw.12306.cn/otn/leftTicket/這個頁面的時候都會走不通,這是由於AFN對於自簽名的HTTPS網站有着特殊的驗證(有關驗證細節,請看本文下一部分),必須證書提早導入到項目中,將Chrome中的證書導入到項目中,請參見下圖:
將生成的證書文件kyfw.12306.cn.cer
加入到xcode項目中,使用AFN按照以下方式調用便可:
NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"kyfw.12306.cn.cer" ofType:nil]; NSData *cerData = [NSData dataWithContentsOfFile:cerPath]; NSSet *set = [[NSSet alloc] initWithObjects:cerData, nil]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set]; manager.securityPolicy.allowInvalidCertificates = YES; [manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) { NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]); } failure:^(NSURLSessionDataTask *task, NSError *error) { NSLog(@"%@",error); }];
這樣便能正確的訪問自簽名的網站了。
說了那麼多如何使用代碼訪問HTTPS網站,那麼AFN是如何實現的呢,AFURLSessionManager中實現了- URLSession:didReceiveChallenge:completionHandler:
代理方法:
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; __block NSURLCredential *credential = nil; if (self.taskDidReceiveAuthenticationChallenge) { disposition = self.taskDidReceiveAuthenticationChallenge(session, task, challenge, &credential); } else { if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) { disposition = NSURLSessionAuthChallengeUseCredential; credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; } else { disposition = NSURLSessionAuthChallengeRejectProtectionSpace; } } else { disposition = NSURLSessionAuthChallengePerformDefaultHandling; } } if (completionHandler) { completionHandler(disposition, credential); } }
它的思路上這樣的
若是主動經過manger的setTaskDidReceiveAuthenticationChallengeBlock:
方法傳遞了taskDidReceiveAuthenticationChallenge的值那麼,會按照傳入的block處理此次質詢,
若是沒有傳入就走AFN處理方式(else分支):
若是驗證方法爲ServerTrust就會使用securityPolicy屬性的方法針對host評判serverTrust的合法性,若是成功了就會使用服務端傳來的證書進行處理,失敗了則會拒絕本次質詢。
若是驗證方法不是ServerTrust,則使用默認的處理方式(NSURLSessionAuthChallengePerformDefaultHandling)處理。
那麼,能夠看出,這裏最關鍵的就是評判合法性的過程了,咱們重點來看一下。評判合法性的方法被定義在AFSecurity類中,是這個類惟一的對象方法:
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain { if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) { NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning."); return NO; } NSMutableArray *policies = [NSMutableArray array]; if (self.validatesDomainName) { [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)]; } else { [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()]; } SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies); if (self.SSLPinningMode == AFSSLPinningModeNone) { return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust); } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) { return NO; } switch (self.SSLPinningMode) { case AFSSLPinningModeNone: default: return NO; case AFSSLPinningModeCertificate: { NSMutableArray *pinnedCertificates = [NSMutableArray array]; for (NSData *certificateData in self.pinnedCertificates) { [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)]; } SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates); if (!AFServerTrustIsValid(serverTrust)) { return NO; } for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) { if ([self.pinnedCertificates containsObject:trustChainCertificate]) { return YES; } } return NO; } case AFSSLPinningModePublicKey: { NSUInteger trustedPublicKeyCount = 0; NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust); for (id trustChainPublicKey in publicKeys) { for (id pinnedPublicKey in self.pinnedPublicKeys) { if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) { trustedPublicKeyCount += 1; } } } return trustedPublicKeyCount > 0; } } return NO; }
這段長度爲60行的代碼實現了這樣的過程:
第一個if分支是對自簽名訪問設立條件:
domain不存在,或者
不容許無效證書,或者
不須要驗證域名,或者
SSLPinningMode不是AFSSLPinningModeNone,並且必須上傳了證書文件。若是是走了這個分支,就要求若是想要實現自簽名的HTTPS訪問成功,必須設置pinnedCertificates,且不能使用defaultPolicy,由於不能SSLPinningMode屬性是readonly的,而defaultPolicy在建立的時候已經設置SSLPinningMode屬性爲AFSSLPinningModeNone。(咱們剛纔的實現方案就是在這條分支下完成的)
接下來是這樣一塊代碼:
NSMutableArray *policies = [NSMutableArray array]; if (self.validatesDomainName) { [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)]; } else { [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()]; } SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies); if (self.SSLPinningMode == AFSSLPinningModeNone) { return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust); } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) { return NO; }
它完成的工做是:
先用policies數組組裝驗證策略,在經過SecTrustSetPolicies函數給serverTrust設置驗證策略,不過AFN並無接收函數的返回值,查看是否設置成功,不知道是爲何。
當SSLPinningMode爲AFSSLPinningModeNone時,若是容許無效的證書(allowInvalidCertificates = YES)直接返回評測成功,若是不容許,按照剛纔的驗證策略驗證,返回的是驗證的結果。
當SSLPinningMode不是AFSSLPinningModeNone時,若是既沒有驗證成功又不容許無效證書,則直接返回評測失敗。
(這裏讓我想到了另外一種訪問12306實現的方案:
manager.securityPolicy.validatesDomainName = NO; manager.securityPolicy.allowInvalidCertificates = YES;
既不用使用證書,也不用本身建立securityPolicy。
)
接下來看一下那個長長的switch:
若是self.SSLPinningMode是AFSSLPinningModeCertificate:取出self.pinnedCertificates中的全部證書,經過SecTrustSetAnchorCertificates函數設置證書驗證策略,失敗則直接返回評測失敗,不然檢查本地的證書是否包含服務端的證書
,若是是返回評測成功,不然返回評測失敗。
若是self.SSLPinningMode是AFSSLPinningModePublicKey:取出服務端證書的全部公鑰,和self.pinnedPublicKeys中全部公鑰,遍歷檢查有沒有相等的兩項,有則返回評測成功。我嘗試給securityPolicy的pinnedPublicKeys賦值一個公鑰集合,可是它並無對外提供接口,self.pinnedPublicKeys是一個私有屬性,而且是計算型的,是從本地的證書self.pinnedCertificates中提取出來的。
有關AFSecurityPolicy最核心的部分基本上將完了,最後咱們仍是要總結一下,訪問可惡的12306的兩種方法:
// 方式一 兩句就能夠 AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; manager.securityPolicy.validatesDomainName = NO; // 關鍵語句1 manager.securityPolicy.allowInvalidCertificates = YES; // 關鍵語句2 [manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) { NSLog(@"%@", responseObject); } failure:^(NSURLSessionDataTask *task, NSError *error) { }]; // 方式二 須要將證書導入到項目中 // 準備:將證書的二進制讀取,放入set中 NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"kyfw.12306.cn.cer" ofType:nil]; NSData *cerData = [NSData dataWithContentsOfFile:cerPath]; NSSet *set = [[NSSet alloc] initWithObjects:cerData, nil]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set]; // 關鍵語句1 manager.securityPolicy.allowInvalidCertificates = YES; // 關鍵語句2 [manager GET:@"https://kyfw.12306.cn/otn/leftTicket/init" parameters:nil success:^(NSURLSessionDataTask *task, id responseObject) { NSLog(@"%@", responseObject); } failure:^(NSURLSessionDataTask *task, NSError *error) { }];
有關AFNetworkReachabilityManager使用比較簡單,不作太多的解釋,只是羅列一些注意點。
AFN開啓必須開啓監控以後才能獲取到新的網絡狀態,若是不開啓各類網絡狀態都爲不可到達,例如
AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager]; NSLog(@"%zd", reachabilityManager.isReachableViaWiFi); // 始終是0 NSLog(@"%zd", reachabilityManager.isReachable); NSLog(@"%zd", reachabilityManager.isReachableViaWWAN);
即便開啓了網絡監控,也沒法再第一時間獲取到網絡狀態,例以下面的代碼執行以後,第一時間查看各類狀態依然不可達,這是由於它會在網絡情況改變時,異步改變單例中存儲的狀態。
AFNetworkReachabilityManager *reachabilityManager = [AFNetworkReachabilityManager sharedManager]; [reachabilityManager startMonitoring]; // 從開啓監控 到獲得下列值須要必定的時間 NSLog(@"%zd", reachabilityManager.isReachableViaWiFi); // 馬上調用爲0 ,過一段時間後準確 NSLog(@"%zd", reachabilityManager.isReachable); // 馬上調用爲0 ,過一段時間後準確 NSLog(@"%zd", reachabilityManager.isReachableViaWWAN); // 馬上調用爲0 ,過一段時間後準確
其實我使用較多的仍是Reachability框架,
Reachability具備獲取實時網絡狀態的-currentReachabilityStatus
方法,不須要開啓監控,只要用實例調用便可。
Reachability一樣能夠進行網絡狀態改變的監控,能夠用-startNotifier
方法開啓,可是無法傳入回調。可是每當網絡狀態改變的時候會發送一個kReachabilityChangedNotification
通知,能夠接收這個通知完成回調。