注:本文始發(fā)于個(gè)人 GitHub 項(xiàng)目 ShannonChenCHN/iOSDevLevelingUp。
源碼倉庫地址:https://github.com/AFNetworking/AFNetworking
AFNetworking 作為我們最基礎(chǔ)的網(wǎng)絡(luò)框架,目前在 GitHub 上 Objective-C 語言類排名第一,幾乎每個(gè)涉及到網(wǎng)絡(luò)請(qǐng)求的 APP 都會(huì)用到,其重要性可見一斑。再者,作為 iOS 開發(fā)領(lǐng)域最受歡迎的開源項(xiàng)目,其中凝聚了眾多大神的智慧,無論是在技術(shù)點(diǎn)上,還是架構(gòu)設(shè)計(jì)上、問題處理方式上,都具有很高的學(xué)習(xí)價(jià)值。
這兩天正好趁著假期有空,可以跟著前人總結(jié)的一些精華,仔細(xì)研讀一下這個(gè)優(yōu)秀的網(wǎng)絡(luò)框架的實(shí)現(xiàn)。站在巨人的肩膀上,才能看得遠(yuǎn)。
這篇文章先從整體架構(gòu)開始,再從實(shí)際使用案例入手,梳理一下核心邏輯,然后再依次了解下各個(gè)具體模塊的實(shí)現(xiàn),最后再回顧一下 2.x 版本的實(shí)現(xiàn),總結(jié)一下 AFNetworking 的價(jià)值。
注:這篇文章不會(huì)逐行分析源碼,具體的代碼注釋見 這里。
目錄
- 一、架構(gòu)
- 二、核心邏輯
- 三、AFURLSessionManager
- 3.1 線程
- 3.2 AFURLSessionManagerTaskDelegate
- 3.3 NSProgress
- 3.4 NSSecureCoding
- 3.5 _AFURLSessionTaskSwizzling
- 四、AFURLRequestSerialization
- 4.1 構(gòu)建普通請(qǐng)求
- 4.2 構(gòu)建 multipart 請(qǐng)求
- 五、AFURLResponseSerialization
- 5.1
-validateResponse:data:error:方法 - 5.2
-responseObjectForResponse:data:error:方法
- 5.1
- 六、AFSecurityPolicy
- 6.1 預(yù)備知識(shí)點(diǎn)
- 6.1.1 為什么要使用 HTTPS
- 6.1.2 HTTPS 的出現(xiàn)
- 6.1.3 SSL/TLS 協(xié)議
- 6.1.4 HTTPS 與 HTTP 的區(qū)別是什么?
- 6.1.5 HTTPS 連接的建立過程
- 6.1.6 HTTPS 傳輸時(shí)是如何驗(yàn)證證書的?怎樣應(yīng)對(duì)中間人偽造證書?
- 6.1.7 Certificate Pinning 是什么?
- 6.2 AFSecurityPolicy 的實(shí)現(xiàn)
- 6.1 預(yù)備知識(shí)點(diǎn)
- 七、AFNetworkReachabilityManager
- 八、UIKit 擴(kuò)展
- 九、AFNetworking 2.x
- 十、AFNetworking 的價(jià)值
- 10.1 請(qǐng)求調(diào)度:NSURLConnection + NSOperation
- 10.2 更高層次的抽象
- 10.3 block
- 10.4 模塊化
- 十一、問題
- 十二、收獲
一、架構(gòu)
AFNetworking 一共分為 5 個(gè)模塊,2 個(gè)核心模塊和 3 個(gè)輔助模塊:
- Core
- NSURLSession(網(wǎng)絡(luò)通信模塊)
- AFURLSessionManager(封裝 NSURLSession)
- AFHTTPSessionManager(繼承自 AFURLSessionManager,實(shí)現(xiàn)了 HTTP 請(qǐng)求相關(guān)的配置)
- Serialization
- AFURLRequestSerialization(請(qǐng)求參數(shù)序列化)
- AFHTTPRequestSerializer
- AFJSONRequestSerializer
- AFPropertyListRequestSerializer
- AFURLResponseSerialization(驗(yàn)證返回?cái)?shù)據(jù)和反序列化)
- AFHTTPResponseSerializer
- AFJSONResponseSerializer
- AFXMLParserResponseSerializer
- AFXMLDocumentResponseSerializer (Mac OS X)
- AFPropertyListResponseSerializer
- AFImageResponseSerializer
- AFCompoundResponseSerializer
- AFURLRequestSerialization(請(qǐng)求參數(shù)序列化)
- NSURLSession(網(wǎng)絡(luò)通信模塊)
- Additional Functionality
- Security(網(wǎng)絡(luò)通信安全策略模塊)
- Reachability(網(wǎng)絡(luò)狀態(tài)監(jiān)聽模塊)
- UIKit(對(duì) iOS 系統(tǒng) UI 控件的擴(kuò)展)

<div align="center">圖 1 AFNetworking 整體架構(gòu)</div>
二、核心邏輯
先來看一下如何使用 AFNetworking 發(fā)送一個(gè) GET 請(qǐng)求:
NSURL *url = [[NSURL alloc] initWithString:@"https://news-at.zhihu.com"];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:url];
[manager GET:@"api/4/news/latest" parameters:nil progress:nil
success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
NSLog(@"%@" ,responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
NSLog(@"%@", error);
}];
首先使用一個(gè) URL,通過調(diào)用 -initWithBaseURL: 方法創(chuàng)建了一個(gè) AFHTTPSessionManager 的實(shí)例,然后再調(diào)用 -GET:parameters:progress:success:failure: 方法發(fā)起請(qǐng)求。
-initWithBaseURL: 方法的調(diào)用棧如下:
- [AFHTTPSessionManager initWithBaseURL:]
- [AFHTTPSessionManager initWithBaseURL:sessionConfiguration:]
- [AFURLSessionManager initWithSessionConfiguration:]
- [NSURLSession sessionWithConfiguration:delegate:delegateQueue:]
- [AFJSONResponseSerializer serializer] // 負(fù)責(zé)序列化響應(yīng)
- [AFSecurityPolicy defaultPolicy] // 負(fù)責(zé)身份認(rèn)證
- [AFNetworkReachabilityManager sharedManager] // 查看網(wǎng)絡(luò)連接情況
- [AFHTTPRequestSerializer serializer] // 負(fù)責(zé)序列化請(qǐng)求
- [AFJSONResponseSerializer serializer] // 負(fù)責(zé)序列化響應(yīng)
AFURLSessionManager 是 AFHTTPSessionManager 的父類,
AFURLSessionManager 負(fù)責(zé)創(chuàng)建和管理 NSURLSession 的實(shí)例,管理 AFSecurityPolicy 和初始化 AFNetworkReachabilityManager,來保證請(qǐng)求的安全和查看網(wǎng)絡(luò)連接情況,它有一個(gè) AFJSONResponseSerializer 的實(shí)例來序列化 HTTP 響應(yīng)。
AFHTTPSessionManager 有著自己的 AFHTTPRequestSerializer 和 AFJSONResponseSerializer 來管理請(qǐng)求和響應(yīng)的序列化,同時(shí)依賴父類實(shí)現(xiàn)發(fā)出 HTTP 請(qǐng)求、管理 Session 這一核心功能。
-GET:parameters:progress:success:failure: 方法的調(diào)用棧:
- [AFHTTPSessionManager GET:parameters:process:success:failure:]
- [AFHTTPSessionManager dataTaskWithHTTPMethod:parameters:uploadProgress:downloadProgress:success:failure:] // 返回一個(gè) NSURLSessionDataTask 對(duì)象
- [AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:] // 返回 NSMutableURLRequest
- [AFURLSessionManager dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:] 返回一個(gè) NSURLSessionDataTask 對(duì)象
- [NSURLSession dataTaskWithRequest:] 返回一個(gè) NSURLSessionDataTask 對(duì)象
- [AFURLSessionManager addDelegateForDataTask:uploadProgress:downloadProgress:completionHandler:]
- [AFURLSessionManagerTaskDelegate init]
- [AFURLSessionManager setDelegate:forTask:] // 為每個(gè) task 創(chuàng)建一個(gè)對(duì)應(yīng)的 delegate
- [NSURLSessionDataTask resume]
發(fā)送請(qǐng)求的核心在于創(chuàng)建和啟動(dòng)一個(gè) data task,AFHTTPSessionManager 只是提供了 HTTP 請(qǐng)求的接口,內(nèi)部最終還是調(diào)用了父類 AFURLSessionManager 來創(chuàng)建 data task(其實(shí)也就是通過 NSURLSession 創(chuàng)建的 task),AFURLSessionManager 中會(huì)為每個(gè) task 創(chuàng)建一個(gè)對(duì)應(yīng)的 AFURLSessionManagerTaskDelegate 對(duì)象,用來處理回調(diào)。
在請(qǐng)求發(fā)起時(shí)有一個(gè)序列化的工具類 AFHTTPRequestSerializer 來處理請(qǐng)求參數(shù)。
請(qǐng)求回調(diào)時(shí)的方法調(diào)用棧:
- [AFURLSessionManager URLSession:task:didCompleteWithError:]
- [AFURLSessionManagerTaskDelegate URLSession:task:didCompleteWithError:]
- [AFJSONResponseSerializer responseObjectForResponse:data:error:] // 解析 JSON 數(shù)據(jù)
- [AFHTTPResponseSerializer validateResponse:data:] // 驗(yàn)證數(shù)據(jù)
- [AFURLSessionManagerTaskDelegate URLSession:task:didCompleteWithError:]_block_invoke_2.150
- [AFHTTPSessionManager dataTaskWithHTTPMethod:URLString:parameters:uploadProgress:downloadProgress:success:failure:]_block_invoke
AFURLSessionManager 在代理方法中收到服務(wù)器返回?cái)?shù)據(jù)的后,會(huì)交給 AFURLSessionManagerTaskDelegate 去處理,接著就是用 AFJSONResponseSerializer 去驗(yàn)證和解析 JSON 數(shù)據(jù),最后再通過 block 回調(diào)的方式返回最終結(jié)果。
三、AFURLSessionManager
AFURLSessionManager 是 AFHTTPSessionManager 的父類,主要有以下幾個(gè)功能:
- 負(fù)責(zé)創(chuàng)建和管理 NSURLSession
- 管理 NSURLSessionTask
- 實(shí)現(xiàn) NSURLSessionDelegate 等協(xié)議中的代理方法
- 使用 AFURLSessionManagerTaskDelegate 管理上傳、下載進(jìn)度,以及請(qǐng)求完成的回調(diào)
- 將整個(gè)請(qǐng)求流程相關(guān)的組件串聯(lián)起來
- 負(fù)責(zé)整個(gè)請(qǐng)求過程的線程調(diào)度
- 使用 AFSecurityPolicy 驗(yàn)證 HTTPS 請(qǐng)求的證書
1. 線程
一般調(diào)用 AFNetworking 的請(qǐng)求 API 時(shí),都是在主線程,也是主隊(duì)列。然后直到調(diào)用 NSURLSession 的 -resume 方法,一直都是在主線程。
在 AFURLSessionManager 的初始化方法中,設(shè)置了 NSURLSession 代理回調(diào)線程的最大并發(fā)數(shù)為 1,因?yàn)榫拖?NSURLSession 的 -sessionWithConfiguration:delegate:delegateQueue: 方法的官方文檔中所說的那樣,所有的代理方法回調(diào)都應(yīng)該在一個(gè)串行隊(duì)列中,因?yàn)橹挥羞@樣才能保證代理方法的回調(diào)順序。
NSURLSession 代理方法回調(diào)是異步的,所以收到回調(diào)時(shí)的線程模式是“異步+串行隊(duì)列”,這個(gè)時(shí)候可以理解為處于回調(diào)線程。
- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
...
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1; // 代理回調(diào)線程最大并發(fā)數(shù)為 1
// 初始化 NSURLSession 對(duì)象
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
...
return self;
}
收到代理回調(diào)后,接著在 AFURLSessionManagerTaskDelegate 的 -URLSession:task:didCompleteWithError: 方法中,異步切換到 processing queue 進(jìn)行數(shù)據(jù)解析,數(shù)據(jù)解析完成后再異步回到主隊(duì)列或者自定義隊(duì)列。
- (void)URLSession:(__unused NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
...
// 如果請(qǐng)求成功,則在一個(gè) AF 的并行 queue 中,去做數(shù)據(jù)解析等后續(xù)操作
dispatch_async(url_session_manager_processing_queue(), ^{
NSError *serializationError = nil;
responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:data error:&serializationError];
...
dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
if (self.completionHandler) {
self.completionHandler(task.response, responseObject, serializationError);
}
...
});
});
...
}
問題:
有個(gè)讓我感到困惑的地方是,這里最后回調(diào)時(shí)為什么要用dispatch_group_async將任務(wù)放到隊(duì)列組中去執(zhí)行,搜了一下也沒看到這個(gè)組中的任務(wù)執(zhí)行完了要做什么,難道是要留給外面的調(diào)用方用的?

<div align="center">圖 2 AFNetworking 中的線程調(diào)度</div>
2. AFURLSessionManagerTaskDelegate
AFURLSessionManager 中幾乎實(shí)現(xiàn)了所有的 NSURLSession 相關(guān)的協(xié)議方法:
- NSURLSessionDelegate
- NSURLSessionTaskDelegate
- NSURLSessionDataDelegate
- NSURLSessionDownloadDelegate
但是AFURLSessionManager 中實(shí)現(xiàn)的這些代理方法都只是做一些非核心邏輯的處理,每個(gè)代理方法中都回調(diào)了一個(gè)自定義邏輯的 block,如果 block 被賦值了,那么就調(diào)用它。
AFURLSessionManager 把最核心的代理回調(diào)處理交給 AFURLSessionManagerTaskDelegate 類去實(shí)現(xiàn)了,AFURLSessionManagerTaskDelegate 可以根據(jù)對(duì)應(yīng)的 task 去進(jìn)行上傳、下載進(jìn)度回調(diào)和請(qǐng)求完成的回調(diào)處理:
- URLSession:task:didCompleteWithError:
- URLSession:dataTask:didReceiveData:
- URLSession:downloadTask:didFinishDownloadingToURL:
AFURLSessionManager 通過屬性 mutableTaskDelegatesKeyedByTaskIdentifier (一個(gè) NSDictionary 對(duì)象)來存儲(chǔ)并管理每一個(gè) NSURLSessionTask 所對(duì)應(yīng)的 AFURLSessionManagerTaskDelegate,它以 taskIdentifier 為鍵存儲(chǔ) task。在請(qǐng)求最終完成后,又將 AFURLSessionManagerTaskDelegate 移除。

<div align="center">圖 3 AFNetworking 中的代理回調(diào)邏輯</div>
3. NSProgress
AFURLSessionManagerTaskDelegate 借助了 NSProgress 這個(gè)類來實(shí)現(xiàn)進(jìn)度的管理,NSProgress 是 iOS 7 引進(jìn)的一個(gè)用來管理任務(wù)進(jìn)度的類,可以表示一個(gè)任務(wù)的進(jìn)度信息,我們還可以對(duì)其進(jìn)行開始
暫停、取消等操作,完整的對(duì)應(yīng)了 task 的各種狀態(tài)。
AFURLSessionManagerTaskDelegate 通過 KVO 監(jiān)聽 task 的進(jìn)度更新,來同步更新 NSProgress 的進(jìn)度數(shù)據(jù)。同時(shí),還用 KVO 監(jiān)聽了 NSProgress 的 fractionCompleted 屬性的變化,用來更新最外面的進(jìn)度回調(diào) block,回調(diào)時(shí)將這個(gè) NSProgress 對(duì)象作為參數(shù)帶過去。
另一方面,AFURLSessionManagerTaskDelegate 中還分別對(duì)下載和上傳的 NSProgress 對(duì)象設(shè)置了開始、暫停、取消等操作的 handler,將 task 跟 NSProgress 的狀態(tài)關(guān)聯(lián)起來。這樣一來,就可以通過控制 NSProgress 對(duì)象的這些操作就可以控制 task 的狀態(tài)。
延伸閱讀:
4. NSSecureCoding
AFNetworking 的大多數(shù)類都支持歸檔解檔,但實(shí)現(xiàn)的是 NSSecureCoding 協(xié)議,而不是 NSCoding 協(xié)議,這兩個(gè)協(xié)議的區(qū)別在于 NSSecureCoding 協(xié)議中定義的解碼的方法是 -decodeObjectOfClass:forKey: 方法,而不是 -decodeObjectForKey:,這就要求解數(shù)據(jù)時(shí)要指定 Class。在 bang 的文章中看到說是這樣做更安全,因?yàn)樾蛄谢蟮臄?shù)據(jù)有可能被篡改,若不指定 Class,decode 出來的對(duì)象可能不是原來的對(duì)象,有潛在風(fēng)險(xiǎn)。(不過暫時(shí)還是沒能理解。)
5. _AFURLSessionTaskSwizzling
_AFURLSessionTaskSwizzling 的唯一作用就是將 NSURLSessionTask 的 -resume 和 -suspend 方法實(shí)現(xiàn)替換成自己的實(shí)現(xiàn),_AFURLSessionTaskSwizzling 中這兩個(gè)方法的實(shí)現(xiàn)是先調(diào)用原方法,然后再發(fā)出一個(gè)通知。
_AFURLSessionTaskSwizzling 是通過在 +load 方法中進(jìn)行 Method Swizzling 來實(shí)現(xiàn)方法交換的,由于 NSURLSessionTask 的實(shí)現(xiàn)是類簇,不能直接通過調(diào)用 +class 來獲取真正的類,而且在 iOS 7 和 iOS 8 下的實(shí)現(xiàn)不同,所以這里的 swizzling 實(shí)現(xiàn)起來有點(diǎn)復(fù)雜。具體原因見 GitHub 上的討論。
問題:
有點(diǎn)不明白的是,NSURLSessionTask 有三個(gè)子類:NSURLSessionDataTask、NSURLSessionDownloadTask 和 NSURLSessionUploadTask,為什么不用考慮這三個(gè)子類自己也實(shí)現(xiàn)了自己的-resume和-suspend方法的情況呢?
四、AFURLRequestSerialization
AFURLRequestSerialization 是一個(gè)抽象的協(xié)議,用于構(gòu)建一個(gè)規(guī)范的 NSURLRequest?;?AFURLRequestSerialization 協(xié)議,AFNetworking 提供了 3 中不同數(shù)據(jù)形式的序列化工具(當(dāng)然你也可以自定義其他數(shù)據(jù)格式的序列化類):
- AFHTTPRequestSerializer:普通的 HTTP 請(qǐng)求,默認(rèn)數(shù)據(jù)格式是
application/x-www-form-urlencoded,也就是 key-value 形式的 url 編碼字符串 - AFJSONRequestSerializer:參數(shù)格式是 json
- AFPropertyListRequestSerializer:參數(shù)格式是蘋果的 plist 格式

<div align="center">圖 4 AFURLRequestSerialization 類圖</div>
AFHTTPRequestSerializer 主要實(shí)現(xiàn)了兩個(gè)功能:
- 構(gòu)建普通請(qǐng)求:格式化請(qǐng)求參數(shù),生成 HTTP Header。
- 構(gòu)建 multipart 請(qǐng)求,上傳數(shù)據(jù)時(shí)會(huì)用到。
1. 構(gòu)建普通請(qǐng)求
AFHTTPRequestSerializer 在構(gòu)建普通請(qǐng)求時(shí),做了以下幾件事:
- 創(chuàng)建 NSURLRequest
- 設(shè)置 NSURLRequest 相關(guān)屬性
- 設(shè)置 HTTP Method
- 設(shè)置 HTTP Header
- 序列化請(qǐng)求參數(shù)
- (NSMutableURLRequest *)requestWithMethod:(NSString *)method
URLString:(NSString *)URLString
parameters:(id)parameters
error:(NSError *__autoreleasing *)error
{
NSParameterAssert(method);
NSParameterAssert(URLString);
NSURL *url = [NSURL URLWithString:URLString];
NSParameterAssert(url);
// 創(chuàng)建請(qǐng)求
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
mutableRequest.HTTPMethod = method; // 設(shè)置 Method
// 這里本來是直接把 self 的一些屬性值直接傳給 request 的,但是因?yàn)槌跏寄J(rèn)情況下,
// 當(dāng)前類中與 NSURLRequest 相關(guān)的那些屬性值為 0,而導(dǎo)致外面業(yè)務(wù)方使用 NSURLSessionConfiguration 設(shè)置屬性時(shí)失效,
// 所以通過對(duì)這些屬性添加了 KVO 監(jiān)聽判斷是否有值來解決這個(gè)傳值的有效性問題
// 詳見 https://github.com/AFNetworking/AFNetworking/commit/49f2f8c9a907977ec1b3afb182404ae0a6bce883
for (NSString *keyPath in AFHTTPRequestSerializerObservedKeyPaths()) {
if ([self.mutableObservedChangedKeyPaths containsObject:keyPath]) {
[mutableRequest setValue:[self valueForKeyPath:keyPath] forKey:keyPath];
}
}
// 設(shè)置 HTTP header;請(qǐng)求參數(shù)序列化,再添加到 query string 或者 body 中
mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];
return mutableRequest;
}
在設(shè)置 NSURLRequest 相關(guān)屬性時(shí),有點(diǎn)繞,本來可以直接將 AFHTTPRequestSerializer 自己的屬性值傳給 NSURLRequest 對(duì)象的,但是后來改成了 KVO 的形式,主要是因?yàn)? NSURLRequest 對(duì)象有些屬性是純量數(shù)據(jù)類型(比如 timeoutInterval),在 AFHTTPRequestSerializer 初始化后,這些跟 NSURLRequest 相關(guān)的屬性值初始默認(rèn)值是 0,所以是不知道外面有沒有設(shè)置過值,如果將 AFHTTPRequestSerializer 的值都傳給 NSURLRequest 對(duì)象的話,很有可能會(huì)導(dǎo)致 NSURLSessionConfiguration 中設(shè)置的相同屬性失效。
AFNetworking 幫我們組裝好了一些 HTTP 請(qǐng)求頭,包括:
-
Content-Type,請(qǐng)求參數(shù)類型 -
Accept-Language,根據(jù)[NSLocale preferredLanguages]方法讀取本地語言,告訴服務(wù)端自己能接受的語言。 User-Agent-
Authorization,提供 Basic Auth 認(rèn)證接口,幫我們把用戶名密碼做 base64 編碼后放入 HTTP 請(qǐng)求頭。
一般我們請(qǐng)求都會(huì)按 key=value 的方式帶上各種參數(shù),GET 方法參數(shù)直接拼在 URL 后面,POST 方法放在 body 上,NSURLRequest 沒有封裝好這個(gè)參數(shù)的序列化,只能我們自己拼好字符串。AFHTTPRequestSerializer 提供了接口,讓參數(shù)可以是 NSDictionary, NSArray, NSSet 這些類型,再由內(nèi)部解析成字符串后賦給 NSURLRequest。
參數(shù)序列化流程大概是這樣的:
- 用戶傳進(jìn)來的數(shù)據(jù),支持包含 NSArray,NSDictionary,NSSet 這三種數(shù)據(jù)結(jié)構(gòu)。
- 先將每組 key-value 轉(zhuǎn)成 AFQueryStringPair 對(duì)象的形式,保存到數(shù)組中(這樣做的目的是因?yàn)樽詈罂梢愿鶕?jù)不同的字符串編碼生成對(duì)應(yīng)的 key=value 字符串)
- 然后取出數(shù)組中的 AFQueryStringPair 對(duì)象,轉(zhuǎn)成一個(gè)個(gè) NSString 對(duì)象再保存到新數(shù)組中
- 最后再將這些
key=value的字符串用&符號(hào)拼接起來
@{
@"name" : @"steve",
@"phone" : @{@"mobile": @"xx", @"home": @"xx"},
@"families": @[@"father", @"mother"],
@"nums" : [NSSet setWithObjects:@"1", @"2", nil]
}
||
\/
@[
field: @"name", value: @"steve",
field: @"phone[mobile]", value: @"xx",
field: @"phone[home]", value: @"xx",
field: @"families[]", value: @"father",
field: @"families[]", value: @"mother",
field: @"nums", value: @"1",
field: @"nums", value: @"2",
]
||
\/
@[
@"name=steve", // 注:實(shí)際代碼中這里的 “=” 會(huì)被編碼
@"phone[mobile]=xx",
@"phone[home]=xx",
@"families[]=father",
@"families[]=mother",
@"nums=1",
@"nums=2"
]
||
\/
@"name=steve&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&nums=2"
請(qǐng)求參數(shù)序列化完成后,再根據(jù)不同的 HTTP 請(qǐng)求方法分別處理,對(duì)于 GET/HEAD/DELETE 方法,把參數(shù)直接加到 URL 后面,對(duì)于其他如 POST/PUT 等方法,把數(shù)據(jù)加到 body 上,并設(shè)好 HTTP 頭中的 Content-Type 為 application/x-www-form-urlencoded,告訴服務(wù)端字符串的編碼是什么。
2. 構(gòu)建 multipart 請(qǐng)求
這部分有點(diǎn)復(fù)雜,暫時(shí)還沒看。
五、AFURLResponseSerialization
AFURLResponseSerialization 模塊負(fù)責(zé)解析網(wǎng)絡(luò)返回?cái)?shù)據(jù),檢查數(shù)據(jù)是否合法,把服務(wù)器返回的 NSData 數(shù)據(jù)轉(zhuǎn)成相應(yīng)的對(duì)象。
AFURLResponseSerialization 模塊包括一個(gè)協(xié)議、一個(gè)基類和多個(gè)解析特定格式數(shù)據(jù)的子類,用戶可以很方便地繼承基類 AFHTTPResponseSerializer 去解析更多的數(shù)據(jù)格式:
- AFURLResponseSerialization 協(xié)議,定義了解析響應(yīng)數(shù)據(jù)的接口
- AFHTTPResponseSerializer, HTTP 請(qǐng)求響應(yīng)數(shù)據(jù)解析器的基類
- AFJSONResponseSerializer,專門解析 JSON 數(shù)據(jù)的解析器
- 其他數(shù)據(jù)格式(XML、image、plist等)的響應(yīng)解析器
- AFCompoundResponseSerializer,組合解析器,可以將多個(gè)解析器組合起來,以同時(shí)支持多種格式的數(shù)據(jù)解析

<div align="center">圖 5 AFURLResponseSerialization 類圖</div>
AFURLResponseSerialization 模塊響應(yīng)解析機(jī)制主要涉及到兩個(gè)核心方法:
- AFHTTPResponseSerializer 中定義、實(shí)現(xiàn)的
-validateResponse:data:error:方法 - AFURLResponseSerialization 協(xié)議定義的
-responseObjectForResponse:data:error:方法
1. -validateResponse:data:error: 方法
AFHTTPResponseSerializer 作為解析器基類,提供了 acceptableContentTypes 和 acceptableStatusCodes 兩個(gè)屬性,并提供了 acceptableStatusCodes 的默認(rèn)值,子類可以通過設(shè)置這兩個(gè)屬性的值來進(jìn)行自定義配置。AFHTTPResponseSerializer 中的 -validateResponse:data:error: 方法會(huì)根據(jù)這兩個(gè)屬性值來判斷響應(yīng)的文件類型 MIMEType 和狀態(tài)碼 statusCode 是否合法。
比如 AFJSONResponseSerializer 中設(shè)置了 acceptableContentTypes 的值為 [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil],如果服務(wù)器返回的 Content-Type 不是這三者之一,-validateResponse:data:error: 方法就會(huì)返回解析失敗的錯(cuò)誤信息。
案例:在網(wǎng)上看到有開發(fā)者就曾經(jīng)遇到過相關(guān)的問題
——服務(wù)器返回的數(shù)據(jù)是 JSON 數(shù)據(jù),但是Content-Type卻不符合要求,結(jié)果導(dǎo)致解析失敗。
2. -responseObjectForResponse:data:error: 方法
AFJSONResponseSerializer 等子類中實(shí)現(xiàn)的 -responseObjectForResponse:data:error: 方法會(huì)先調(diào)用 -validateResponse:data:error: 方法驗(yàn)證數(shù)據(jù)是否合法,拿到驗(yàn)證結(jié)果后,接著這里有個(gè)補(bǔ)充判斷條件——如果是 content type 的錯(cuò)誤就直接返回 nil,因?yàn)閿?shù)據(jù)類型不符合要求,就沒必要再繼續(xù)解析數(shù)據(jù)了,如果是 status code 的錯(cuò)誤就繼續(xù)解析,因?yàn)閿?shù)據(jù)本身沒問題,而錯(cuò)誤信息有可能就在返回的數(shù)據(jù)中,所以這種情況下會(huì)將 status code 產(chǎn)生的錯(cuò)誤信息和解析后的數(shù)據(jù)一起“打包”返回。
AFJSONResponseSerializer 在解析數(shù)據(jù)后還提供了移除 NSNull 的功能,主要是為了防止服務(wù)端返回 null 時(shí)導(dǎo)致解析后的數(shù)據(jù)中有了脆弱的 NSNull,這樣很容易導(dǎo)致崩潰(但是之前一直沒發(fā)現(xiàn)這個(gè)功能[捂臉])。
- (id)responseObjectForResponse:(NSURLResponse *)response
data:(NSData *)data
error:(NSError *__autoreleasing *)error
{
if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
// 如果是 content type 的錯(cuò)誤就直接返回,因?yàn)閿?shù)據(jù)類型不符合要求
// 如果是 status code 的錯(cuò)誤就繼續(xù)解析,因?yàn)殄e(cuò)誤信息有可能就在返回的數(shù)據(jù)中
if (!error || AFErrorOrUnderlyingErrorHasCodeInDomain(*error, NSURLErrorCannotDecodeContentData, AFURLResponseSerializationErrorDomain)) {
return nil;
}
}
NSError *serializationError = nil;
id responseObject = [NSJSONSerialization JSONObjectWithData:data options:self.readingOptions error:&serializationError];
...
// 移除 NSNull(如果需要的話),默認(rèn)是 NO
if (self.removesKeysWithNullValues && responseObject) {
responseObject = AFJSONObjectByRemovingKeysWithNullValues(responseObject, self.readingOptions);
}
...
return responseObject;
}
六、AFSecurityPolicy
幾個(gè)關(guān)鍵字:HTTPS,TSL,SSL,SSL Pinning,非對(duì)稱加密算法
1. 預(yù)備知識(shí)點(diǎn)
1.1 為什么要使用 HTTPS
因?yàn)橹苯邮褂?HTTP 請(qǐng)求,就會(huì)有可能遇到以下幾個(gè)安全問題:
- 傳輸數(shù)據(jù)被竊聽:HTTP 報(bào)文使用明文方式發(fā)送,而且 HTTP 本身不具備加密的功能,而互聯(lián)網(wǎng)是由聯(lián)通世界各個(gè)地方的網(wǎng)絡(luò)設(shè)施組成,所有發(fā)送和接收經(jīng)過某些設(shè)備的數(shù)據(jù)都可能被截獲或窺視。
- 認(rèn)證問題:
- 無法確認(rèn)你發(fā)送到的服務(wù)器就是真正的目標(biāo)服務(wù)器(可能服務(wù)器是偽裝的)
- 無法確定返回的客戶端是否是按照真實(shí)意圖接收的客戶端(可能是偽裝的客戶端)
- 無法確定正在通信的對(duì)方是否具備訪問權(quán)限,Web 服務(wù)器上某些重要的信息,只想發(fā)給特定用戶
- 傳輸內(nèi)容可能被篡改:請(qǐng)求或響應(yīng)在傳輸途中,可能會(huì)被攻擊者攔截并篡改內(nèi)容,也就是所謂的中間人攻擊(Man-in-the-Middle attack,MITM)。
1.2 HTTPS 的出現(xiàn)
HTTPS,也稱作 HTTP over TLS,HTTPS 就是基于 TLS 的 HTTP 請(qǐng)求。TLS 是一種基于 TCP 的加密協(xié)議,它主要做了兩件事:傳輸?shù)膬啥丝梢曰ハ囹?yàn)證對(duì)方的身份,以及驗(yàn)證后加密所傳輸?shù)臄?shù)據(jù)。
HTTPS 通過驗(yàn)證和加密兩種手段的結(jié)合解決了上面 HTTP 所面臨的 3 個(gè)安全問題。
1.3 SSL/TLS 協(xié)議
SSL(Secure Sockets Layer):SSL 協(xié)議是一種數(shù)據(jù)加密協(xié)議,為了保證網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)陌踩?,網(wǎng)景公司設(shè)計(jì)了 SSL 協(xié)議用于對(duì) HTTP 協(xié)議傳輸?shù)臄?shù)據(jù)進(jìn)行加密,從而就誕生了 HTTPS。
TLS(Transport Layer Security):TLS 協(xié)議是 SSL 協(xié)議的升級(jí)版。1999年,互聯(lián)網(wǎng)標(biāo)準(zhǔn)化組織 ISOC 接替 NetScape 公司,發(fā)布了 SSL 的升級(jí)版 TLS 1.0版。
1.4 HTTPS 與 HTTP 的區(qū)別是什么?
|
HTTP | HTTPS |
|---|---|---|
| URL |
http:// 開頭,并且默認(rèn)使用端口 80 |
https:// 開頭,并且默認(rèn)使用端口 443 |
| 數(shù)據(jù)隱私性 | 明文傳輸,不加密傳輸數(shù)據(jù) | 基于 TLS 的加密傳輸 |
| 身份認(rèn)證 | 不認(rèn)證 | 正式傳輸數(shù)據(jù)前會(huì)進(jìn)行證書認(rèn)證,第三方無法偽造服務(wù)端(客戶端)身份 |
| 數(shù)據(jù)完整性 | 沒有完整性校驗(yàn)過程 | 內(nèi)容傳輸經(jīng)過完整性校驗(yàn) |
HTTP協(xié)議和安全協(xié)議(SSL/TLS)同屬于應(yīng)用層(OSI模型的最高層),具體來講,安全協(xié)議(SSL/TLS)工作在 HTTP 之下,傳輸層之上:安全協(xié)議向運(yùn)行 HTTP 的進(jìn)程提供一個(gè)類似于 TCP 的套接字,供進(jìn)程向其中注入報(bào)文,安全協(xié)議將報(bào)文加密并注入傳輸層套接字;或是從運(yùn)輸層獲取加密報(bào)文,解密后交給對(duì)應(yīng)的進(jìn)程。嚴(yán)格地講,HTTPS 并不是一個(gè)單獨(dú)的協(xié)議,而是對(duì)工作在一加密連接(TLS或SSL)上的常規(guī) HTTP 協(xié)議的稱呼。

<div align="center">圖 6 協(xié)議層</div>
HTTPS 報(bào)文中的任何東西都被加密,包括所有報(bào)頭和荷載(payload)。除了可能的選擇密文攻擊之外,一個(gè)攻擊者所能知道的只有在兩者之間有一連接這件事。
1.5 HTTPS 連接的建立過程
HTTPS在傳輸數(shù)據(jù)之前需要客戶端與服務(wù)端之間進(jìn)行一次握手,在握手過程中將確立雙方加密傳輸數(shù)據(jù)的密碼信息。(握手過程采用的非對(duì)稱加密,正式傳輸數(shù)據(jù)時(shí)采用的是對(duì)稱加密)
HTTPS 的認(rèn)證有單向認(rèn)證和雙向認(rèn)證,這里簡單梳理一下客戶端單向認(rèn)證時(shí)的握手流程:
(1)客戶端發(fā)起一個(gè)請(qǐng)求,服務(wù)端響應(yīng)后返回一個(gè)證書,證書中包含一些基本信息和公鑰。
(2)客戶端里存有各個(gè)受信任的證書機(jī)構(gòu)根證書,用這些根證書對(duì)服務(wù)端返回的證書進(jìn)行驗(yàn)證,如果不可信任,則請(qǐng)求終止。
(3)如果證書受信任,或者是用戶接受了不受信的證書,客戶端會(huì)生成一串隨機(jī)數(shù)的密碼 random key,并用證書中提供的公鑰加密,再返回給服務(wù)器。
(4)服務(wù)器拿到加密后的隨機(jī)數(shù),利用私鑰解密,然后再用解密后的隨機(jī)數(shù) random key,對(duì)需要返回的數(shù)據(jù)加密,加密完成后將數(shù)據(jù)返回給客戶端。
(5)最后用戶拿到被加密過的數(shù)據(jù),用客戶端一開始生成的那個(gè)隨機(jī)數(shù) random key,進(jìn)行數(shù)據(jù)解密。整個(gè) TLS/SSL 握手過程完成。

<div align="center">圖 7 TLS/SSL 握手過程(單向認(rèn)證)</div>
完整的 HTTPS 連接的建立過程,包括下面三個(gè)步驟:
(1)TCP 協(xié)議的三次握手;
(2)TLS/SSL 協(xié)議的握手、密鑰協(xié)商;
(3)使用共同約定的密鑰開始通信。

<div align="center">圖 8 完整的 HTTPS 連接的建立過程</div>
1.6 HTTPS 傳輸時(shí)是如何驗(yàn)證證書的?怎樣應(yīng)對(duì)中間人偽造證書?
先來看看維基百科上對(duì)對(duì)稱加密和非對(duì)稱加密的解釋:
對(duì)稱密鑰加密(英語:Symmetric-key algorithm)又稱為對(duì)稱加密、私鑰加密、共享密鑰加密,是密碼學(xué)中的一類加密算法。<u>這類算法在加密和解密時(shí)使用相同的密鑰,或是使用兩個(gè)可以簡單地相互推算的密鑰</u>。實(shí)務(wù)上,這組密鑰成為在兩個(gè)或多個(gè)成員間的共同秘密,以便維持專屬的通訊聯(lián)系。與公開密鑰加密相比,要求雙方取得相同的密鑰是對(duì)稱密鑰加密的主要缺點(diǎn)之一。
公開密鑰加密(英語:public-key cryptography,又譯為公開密鑰加密),也稱為非對(duì)稱加密(asymmetric cryptography),一種密碼學(xué)算法類型,在這種密碼學(xué)方法中,需要一對(duì)密鑰(其實(shí)這里密鑰說法不好,就是“鑰”),一個(gè)是私人密鑰,另一個(gè)則是公開密鑰。<u>這兩個(gè)密鑰是數(shù)學(xué)相關(guān),用某用戶密鑰加密后所得的信息,只能用該用戶的解密密鑰才能解密。如果知道了其中一個(gè),并不能計(jì)算出另外一個(gè)。因此如果公開了一對(duì)密鑰中的一個(gè),并不會(huì)危害到另外一個(gè)的秘密性質(zhì)</u>。稱公開的密鑰為公鑰;不公開的密鑰為私鑰。
從上面可以看出非對(duì)稱加密的特點(diǎn):非對(duì)稱加密有一對(duì)公鑰私鑰,用公鑰加密的數(shù)據(jù)只能通過對(duì)應(yīng)的私鑰解密,用私鑰加密的數(shù)據(jù)只能通過對(duì)應(yīng)的公鑰解密。這種加密是單向的。
(1)HTTPS 傳輸時(shí)是如何驗(yàn)證證書的呢?
我們以最簡單的為例:一個(gè)證書頒發(fā)機(jī)構(gòu)(CA),頒發(fā)了一個(gè)證書 Cer,服務(wù)器用這個(gè)證書建立 HTTPS 連接,同時(shí)客戶端在信任列表里有這個(gè) CA 機(jī)構(gòu)的根證書。
CA 機(jī)構(gòu)頒發(fā)的證書 Cer 里包含有證書內(nèi)容 Content,以及證書加密內(nèi)容 Crypted Content(數(shù)字簽名),這個(gè)加密內(nèi)容 Crypted Content 就是用這個(gè)證書機(jī)構(gòu)的私鑰對(duì)內(nèi)容 Content 加密的結(jié)果。
+-------------------+
| Content |
+-------------------+
| Crypted Content |
+-------------------+
證書 Cer
建立 HTTPS 連接時(shí),服務(wù)端會(huì)把證書 Cer 返回給客戶端,客戶端系統(tǒng)里的 CA 機(jī)構(gòu)根證書有這個(gè) CA 機(jī)構(gòu)的公鑰,用這個(gè)公鑰對(duì)證書 Cer 的加密內(nèi)容 Crypted Content 解密得到 Content,跟證書 Cer 里的內(nèi)容 Content 對(duì)比,若相等就通過驗(yàn)證。大概的流程如下:
+-----------------------------------------------------+
| crypt with private key |
| Content ------------------------> Crypted Content |
Server | |
| 證書 Cer |
+-----------------------------------------------------+
||
||
\/
+-----------------------------------------------------+
| |
| Content & Crypted Content |
Client | | | |
| | 證書 Cer | |
+------------------|---------------|------------------+
| |
| | derypt with public key
| |
\/ 相等? \/
Content?。?Decrypted Content
(2)怎樣應(yīng)對(duì)中間人偽造證書?
因?yàn)橹虚g人不會(huì)有 CA 機(jī)構(gòu)的私鑰,即便偽造了一張證書,但是私鑰不對(duì),加密出來的內(nèi)容也就不對(duì),客戶端也就無法通過 CA 公鑰解密,所以偽造的證書肯定無法通過驗(yàn)證。
1.7 Certificate Pinning 是什么?
如果一個(gè)客戶端通過 TLS 和服務(wù)器建立連接,操作系統(tǒng)會(huì)驗(yàn)證服務(wù)器證書的有效性(一般是按照 X.509 標(biāo)準(zhǔn))。當(dāng)然,有很多手段可以繞開這個(gè)校驗(yàn),最直接的是在 iOS 設(shè)備上安裝證書并且將其設(shè)置為可信的。這種情況下,實(shí)施中間人攻擊也不是什么難事。不過通過 Certificate Pinning 可以解決這個(gè)問題。
A client that does key pinning adds an extra step beyond the normal X.509 certificate validation.
—— Wikipedia:Certificate Pinning
Certificate Pinning ,可以理解為證書綁定,有時(shí)候又叫 SSL Pinning,其實(shí)更準(zhǔn)確的叫法應(yīng)該是 Public Key Pinning(公鑰綁定)。證書綁定是一種檢測(cè)和防止“中間人攻擊”的方式,客戶端直接保存服務(wù)端的證書,當(dāng)建立 TLS 連接后,應(yīng)立即檢查服務(wù)器的證書,<u>不僅要驗(yàn)證證書的有效性,還需要確定證書是不是跟客戶端本地的證書相匹配</u>??紤]到應(yīng)用和服務(wù)器需要同時(shí)升級(jí)證書的要求,這種方式比較適合應(yīng)用在訪問自家服務(wù)器的情況下。
為什么直接對(duì)比就能保證證書沒問題?
如果中間人從客戶端取出證書,再偽裝成服務(wù)端跟其他客戶端通信,它發(fā)送給客戶端的這個(gè)證書不就能通過驗(yàn)證嗎?確實(shí)可以通過驗(yàn)證,但后續(xù)的流程走不下去,因?yàn)橄乱徊娇蛻舳藭?huì)用證書里的公鑰加密,中間人沒有這個(gè)證書的私鑰就解不出內(nèi)容,也就截獲不到數(shù)據(jù),這個(gè)證書的私鑰只有真正的服務(wù)端有,中間人偽造證書主要偽造的是公鑰。
什么情況下需要使用 Certificate Pinning?
- 就像前面所說的,常規(guī)的驗(yàn)證方式并不能避免遭遇中間人攻擊,因?yàn)槿绻L問網(wǎng)站的證書是自制的,而且在客戶端上通過手動(dòng)安裝根證書信任了,此時(shí)就很容易被惡意攻擊了(還記得你訪問 12306 時(shí)收到的證書驗(yàn)證提醒嗎)。
- 如果服務(wù)端的證書是從受信任的的 CA 機(jī)構(gòu)頒發(fā)的,驗(yàn)證是沒問題的,但 CA 機(jī)構(gòu)頒發(fā)證書比較昂貴,小企業(yè)或個(gè)人用戶可能會(huì)選擇自己頒發(fā)證書,這樣就無法通過系統(tǒng)受信任的 CA 機(jī)構(gòu)列表驗(yàn)證這個(gè)證書的真?zhèn)瘟恕?/li>
2. AFSecurityPolicy 的實(shí)現(xiàn)
2.1 AFSecurityPolicy 的作用
NSURLConnection 和 NSURLSession 已經(jīng)封裝了 HTTPS 連接的建立、數(shù)據(jù)的加密解密功能,我們直接使用 NSURLConnection 或者 NSURLSession 也是可以訪問 HTTPS 網(wǎng)站的,但 NSURLConnection 和 NSURLSession 并沒有驗(yàn)證證書是否合法,無法避免中間人攻擊。要做到真正安全通訊,需要我們手動(dòng)去驗(yàn)證服務(wù)端返回的證書(系統(tǒng)提供了 SecTrustEvaluate函數(shù)供我們驗(yàn)證證書使用)。
AFSecurityPolicy 幫我們封裝了證書驗(yàn)證的邏輯,讓用戶可以輕易使用,除了在系統(tǒng)的信任機(jī)構(gòu)列表里驗(yàn)證,還支持 SSL Pinning 方式的驗(yàn)證。
2.2 使用方法
如果是權(quán)威機(jī)構(gòu)頒發(fā)的證書,不需要任何設(shè)置。
如果是自簽名證書,但是不做證書綁定,直接按照下面的代碼實(shí)現(xiàn)即可:
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
// 允許無效證書(包括自簽名證書),必須的
policy.allowInvalidCertificates = YES;
// 是否驗(yàn)證域名的CN字段
// 不是必須的,但是如果寫YES,則必須導(dǎo)入證書。
policy.validatesDomainName = NO;
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:<#MyAPIBaseURLString#>]];
manager.securityPolicy = securityPolicy;
如果是自簽名證書,而且還要做證書綁定,就需要把自簽的服務(wù)端證書,或者自簽的CA根證書導(dǎo)入到項(xiàng)目中(把 cer 格式的服務(wù)端證書放到 APP 項(xiàng)目資源里,AFSecurityPolicy 會(huì)自動(dòng)尋找根目錄下所有 cer 文件,當(dāng)然你也可以自己讀取),然后再選擇驗(yàn)證證書或者公鑰。
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
// 允許無效證書(包括自簽名證書),必須的
policy.allowInvalidCertificates = YES;
// 是否驗(yàn)證域名的CN字段,不是必須的
policy.validatesDomainName = NO;
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:<#MyAPIBaseURLString#>]];
manager.securityPolicy = securityPolicy;
2.3 AFSecurityPolicy 的實(shí)現(xiàn)
詳細(xì)說明見源碼注釋。
在 AFURLSessionManager 中實(shí)現(xiàn)的 -URLSession:didReceiveChallenge:completionHandler: 方法中,根據(jù) NSURLAuthenticationChallenge 對(duì)象中的 authenticationMethod,來決定是否需要驗(yàn)證服務(wù)器證書,如果需要驗(yàn)證,則借助 AFSecurityPolicy 來驗(yàn)證證書,驗(yàn)證通過則創(chuàng)建 NSURLCredential,并回調(diào) handler:
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
/*
NSURLSessionAuthChallengeUseCredential:使用指定的證書
NSURLSessionAuthChallengePerformDefaultHandling:默認(rèn)方式處理
NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消整個(gè)請(qǐng)求
NSURLSessionAuthChallengeRejectProtectionSpace:
*/
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
__block NSURLCredential *credential = nil;
if (self.sessionDidReceiveAuthenticationChallenge) {
disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
} else {
// 此處服務(wù)器要求客戶端的接收認(rèn)證挑戰(zhàn)方法是 NSURLAuthenticationMethodServerTrust,也就是說服務(wù)器端需要客戶端驗(yàn)證服務(wù)器返回的證書信息
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
// 客戶端根據(jù)安全策略驗(yàn)證服務(wù)器返回的證書
// AFSecurityPolicy 在這里的作用就是,使得在系統(tǒng)底層自己去驗(yàn)證之前,AF可以先去驗(yàn)證服務(wù)端的證書。如果通不過,則直接越過系統(tǒng)的驗(yàn)證,取消https的網(wǎng)絡(luò)請(qǐng)求。否則,繼續(xù)去走系統(tǒng)根證書的驗(yàn)證(??)。
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// 信任的話,就創(chuàng)建驗(yàn)證憑證去做系統(tǒng)根證書驗(yàn)證
// 創(chuàng)建 NSURLCredential 前需要調(diào)用 SecTrustEvaluate 方法來驗(yàn)證證書,這件事情其實(shí) AFSecurityPolicy 已經(jīng)幫我們做了
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
if (credential) {
disposition = NSURLSessionAuthChallengeUseCredential;
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
} else {
// 不信任的話,就直接取消整個(gè)請(qǐng)求
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
} else {
disposition = NSURLSessionAuthChallengePerformDefaultHandling;
}
}
if (completionHandler) {
// 疑問:這個(gè) completionHandler 是用來干什么的呢?credential 又是用來干什么的呢?
completionHandler(disposition, credential);
}
}
而 AFSecurityPolicy 的核心就在于 -evaluateServerTrust:forDomain: 方法,該方法中主要做了四件事:
- 設(shè)置驗(yàn)證標(biāo)準(zhǔn)(
SecTrustSetPolicies),為認(rèn)證做準(zhǔn)備 - 處理 SSLPinningMode 為
AFSSLPinningModeNone的情況——如果允許無效的證書(包括自簽名證書)就直接返回 YES,不允許的話就在系統(tǒng)的信任機(jī)構(gòu)列表里驗(yàn)證服務(wù)端證書。 - 處理 SSLPinningMode 為
AFSSLPinningModeCertificate的情況,認(rèn)證證書——設(shè)置證書錨點(diǎn)->驗(yàn)證服務(wù)端證書->匹配服務(wù)端證書鏈 - 處理 SSLPinningMode 為
AFSSLPinningModePublicKey的情況,認(rèn)證公鑰——匹配服務(wù)端證書公鑰
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
forDomain:(NSString *)domain
{
/*
AFSecurityPolicy 的四個(gè)主要屬性:
SSLPinningMode - 證書認(rèn)證模式
pinnedCertificates - 用來匹配服務(wù)端證書信息的證書,這些證書保存在客戶端
allowInvalidCertificates - 是否支持無效的證書(包括自簽名證書)
validatesDomainName - 是否去驗(yàn)證證書域名是否匹配
SSLPinningMode 提供的三種證書認(rèn)證模式:
AFSSLPinningModeNone - 沒有 SSL pinning
AFSSLPinningModePublicKey - 用證書綁定方式驗(yàn)證,客戶端要有服務(wù)端的證書拷貝,只是驗(yàn)證時(shí)只驗(yàn)證證書里的公鑰,不驗(yàn)證證書的有效期等信息
AFSSLPinningModeCertificate - 用證書綁定方式驗(yàn)證證書,需要客戶端保存有服務(wù)端的證書拷貝,這里驗(yàn)證分兩步,第一步驗(yàn)證證書的域名/有效期等信息,第二步是對(duì)比服務(wù)端返回的證書跟客戶端返回的是否一致。
*/
// 判斷互相矛盾的情況:
// 如果有域名,而且還要允許自簽證書,同時(shí)還要驗(yàn)證域名的話,就一定要驗(yàn)證服務(wù)器返回的證書是否匹配客戶端本地的證書了
// 所以必須滿足兩個(gè)條件:A驗(yàn)證模式不能為 FSSLPinningModeNone;添加到項(xiàng)目里的證書至少 1 個(gè)。
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;
}
// 為 serverTrust 設(shè)置 policy,也就是告訴客戶端如何驗(yàn)證 serverTrust
// 如果要驗(yàn)證域名的話,就以域名為參數(shù)創(chuàng)建一個(gè) SecPolicyRef,否則會(huì)創(chuàng)建一個(gè)符合 X509 標(biāo)準(zhǔn)的默認(rèn) SecPolicyRef 對(duì)象
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);
// 驗(yàn)證證書是否有效
if (self.SSLPinningMode == AFSSLPinningModeNone) {
// 如果不做證書綁定,就會(huì)跟瀏覽器一樣在系統(tǒng)的信任機(jī)構(gòu)列表里驗(yàn)證服務(wù)端返回的證書(如果是自己買的證書,就不需要綁定證書了,可以直接在系統(tǒng)的信任機(jī)構(gòu)列表里驗(yàn)證就行了)
// 如果允許無效的證書(包括自簽名證書)就會(huì)直接返回 YES,不允許的話就會(huì)對(duì)服務(wù)端證書在系統(tǒng)的信任機(jī)構(gòu)列表里驗(yàn)證。如果服務(wù)器證書無效,并且不允許無效證書,就會(huì)返回 NO
return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
} else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
// 如果不是 AFSSLPinningModeNone,而且證書在系統(tǒng)的信任機(jī)構(gòu)列表里驗(yàn)證失敗,同時(shí)不允許無效的證書(包括自簽名證書)時(shí),直接返回評(píng)估失敗
// (如果是自簽名的證書,驗(yàn)證時(shí)就需要做證書綁定,或者直接在系統(tǒng)的信任機(jī)構(gòu)列表里中添加根證書)
return NO;
}
// 根據(jù) SSLPinningMode 對(duì)服務(wù)端返回的證書進(jìn)行 SSL Pinning 驗(yàn)證,也就是說拿本地的證書和服務(wù)端證書進(jìn)行匹配
switch (self.SSLPinningMode) {
case AFSSLPinningModeNone:
default:
return NO;
case AFSSLPinningModeCertificate: {
// 把證書 data 轉(zhuǎn)成 SecCertificateRef 類型的數(shù)據(jù),保證返回的證書都是 DER 編碼的 X.509 證書
NSMutableArray *pinnedCertificates = [NSMutableArray array];
for (NSData *certificateData in self.pinnedCertificates) {
[pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
}
// 1.
// 將 pinnedCertificates 設(shè)置成需要參與驗(yàn)證的 Anchor Certificate(錨點(diǎn)證書,嵌入到操作系統(tǒng)中的根證書,也就是權(quán)威證書頒發(fā)機(jī)構(gòu)頒發(fā)的自簽名證書),通過 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)證。
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;
}
// 注意,這個(gè)方法和我們之前的錨點(diǎn)證書沒關(guān)系了,是去從我們需要被驗(yàn)證的服務(wù)端證書,去拿證書鏈。
// 服務(wù)器端的證書鏈,注意此處返回的證書鏈順序是從葉節(jié)點(diǎn)到根節(jié)點(diǎn)
// obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
// 如果我們的證書中,有一個(gè)和它證書鏈中的證書匹配的,就返回 YES
if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
return YES;
}
}
return NO;
}
case AFSSLPinningModePublicKey: {
NSUInteger trustedPublicKeyCount = 0;
// 獲取服務(wù)器證書公鑰
NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
// 判斷自己本地的證書的公鑰是否存在與服務(wù)器證書公鑰一樣的情況,只要有一組符合就為真
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;
}
2.4 技術(shù)點(diǎn)
(1) __Require_Quiet 宏中 do-while 的特殊使用
#ifndef __Require_Quiet
#define __Require_Quiet(assertion, exceptionLabel) \
do \
{ \
if ( __builtin_expect(!(assertion), 0) ) \
{ \
goto exceptionLabel; \
} \
} while ( 0 )
#endif
__Require_Quiet 宏中使用了一個(gè) do...while(0) 的循環(huán)語句,從邏輯上看這個(gè) do-while 語句完全可以不需要,但是實(shí)際上是不能去掉的,原因是為了防止在某種情況下使用該宏時(shí)出現(xiàn)語法錯(cuò)誤。比如,在下面這種情況下,如果沒有 do...while(0) 就在編譯時(shí)報(bào)錯(cuò):
if (xxx)
__Require_Quiet();
else
NSLog("This is else");
(2) Core Foundation 和 Security 框架的 API 的使用
延伸閱讀:
- 關(guān)于 HTTPS 請(qǐng)求流程
- TLS/SSL
- 關(guān)于數(shù)字證書
- 加密算法
- 白話解釋 對(duì)稱加密算法 VS 非對(duì)稱加密算法
- 關(guān)于非對(duì)稱加密算法的原理:RSA算法原理(一) (二)
- 認(rèn)證流程
七、AFNetworkReachabilityManager
暫時(shí)還沒看
八、UIKit 擴(kuò)展
暫時(shí)還沒看
九、AFNetworking 2.x
十、AFNetworking 的價(jià)值
1. 請(qǐng)求調(diào)度:NSURLConnection + NSOperation
在 NSURLConnection 時(shí)代,AFNetworking 1.x 的最核心的作用在于多線程下的請(qǐng)求調(diào)度——將 NSURLConnection 和 NSOperation 結(jié)合,AFURLConnectionOperation 作為 NSOperation 的子類,遵循 NSURLConnectionDelegate 的方法,可以從頭到尾監(jiān)聽請(qǐng)求的狀態(tài),并儲(chǔ)存請(qǐng)求、響應(yīng)、響應(yīng)數(shù)據(jù)等中間狀態(tài)。
2. 更高層次的抽象
顯然,在 NSURLSession 出現(xiàn)之后,AFNetworking 的意義似乎不如以前那么重要了。實(shí)際上,雖然它們有一些重疊,AFNetworking 還是可以提供更高層次的抽象。
AFNetworking 幫我們完成了很多繁瑣的工作,這使得我們?cè)跇I(yè)務(wù)層的網(wǎng)絡(luò)請(qǐng)求變得非常輕松:
- 請(qǐng)求參數(shù)和返回?cái)?shù)據(jù)的序列化,支持多種不同格式的數(shù)據(jù)解析
- multipart 請(qǐng)求拼接數(shù)據(jù)
- 驗(yàn)證 HTTPS 請(qǐng)求的證書
- 請(qǐng)求成功和失敗的回調(diào)處理,下載、上傳進(jìn)度的回調(diào)處理
3. block
AFNetworking 將 NSURLSession 散亂的代理回調(diào)方法都轉(zhuǎn)成了 block 形式的 API,除此之外,還提供了一些用于自定義配置的 block,比如發(fā)起 multipart 請(qǐng)求時(shí),提供 constructingBody 的 block 接口來拼接數(shù)據(jù)。
4. 模塊化
AFNetworking 在架構(gòu)上采用了模塊化的設(shè)計(jì),各模塊的職責(zé)是明確的、功能是獨(dú)立的,我們可以根據(jù)自己的需要,選擇合適的模塊組合使用:
- 創(chuàng)建請(qǐng)求
- 序列化 query string 參數(shù)
- 確定響應(yīng)解析行為
- 管理 Session
- HTTPS 認(rèn)證
- 監(jiān)視網(wǎng)絡(luò)狀態(tài)
- UIKit 擴(kuò)展
十一、問題:
1.AFNetworking 的作用是什么?不用 AFNetworking 直接用系統(tǒng)的 NSURLSession 不可以嗎?AFNetworking 為什么要對(duì) NSURLConnection/NSURLSession 進(jìn)行封裝?它是如何封裝的?
2.AFNetworking 框架的設(shè)計(jì)思路和原理是什么?
3.AFNetworking 和 MKNetworkKit 以及 ASIHttpRequest 有什么不同?
4.AFNetworking 2.x 和 AFNetworking 3.x 的區(qū)別是什么?
十二、收獲
- 開源項(xiàng)目、專業(yè)素養(yǎng)、規(guī)范
- 完善的注釋、文檔
- 忽略一些特定的clang的編譯警告
- nullable
- 規(guī)范,通過斷言檢測(cè)參數(shù)的合法性
- 邏輯嚴(yán)謹(jǐn)、完善,擴(kuò)展性好,比如針對(duì)用戶可能需要的各種自定義處理提供了 block 回調(diào),基于協(xié)議的 serialization 設(shè)計(jì)
- 萬物皆對(duì)象,比如請(qǐng)求 url 參數(shù)的解析時(shí),使用了 AFQueryStringPair 對(duì)象來表征一個(gè) Query 參數(shù);還有 NSProgress 的使用
- 面向協(xié)議編程,提高程序的可擴(kuò)展性
- 多線程編程時(shí),腦海中要有清晰的線程調(diào)度圖
- Unit Test,看到 GitHub 上有個(gè) pr 的討論中多次提到了 Unit Test,原來 Unit Test 對(duì)于保證修改后的代碼功能有很大用處,另外就是,有些使用的示例也可以從 test case 中找到
延伸閱讀
- AFNetworking到底做了什么?(一)(系列文章,寫的非常詳細(xì),非常推薦)
- bang:AFNetworking2.0 源碼解析(一)(二)(三)(四)
- Draveness :AFNetworking 源碼解析(一)
- NSHipster: AFNetworking 2.0
- 四種常見的 POST 提交數(shù)據(jù)方式
- IP,TCP 和 HTTP