AFNetworking(v3.1.0)源碼解析

注:本文始發(fā)于個人 GitHub 項目 ShannonChenCHN/iOSDevLevelingUp

源碼倉庫地址:https://github.com/AFNetworking/AFNetworking

AFNetworking 作為我們最基礎(chǔ)的網(wǎng)絡(luò)框架只怎,目前在 GitHub 上 Objective-C 語言類排名第一,幾乎每個涉及到網(wǎng)絡(luò)請求的 APP 都會用到探孝,其重要性可見一斑兆沙。再者轴踱,作為 iOS 開發(fā)領(lǐng)域最受歡迎的開源項目汤踏,其中凝聚了眾多大神的智慧泥从,無論是在技術(shù)點上,還是架構(gòu)設(shè)計上沪摄、問題處理方式上躯嫉,都具有很高的學(xué)習(xí)價值。

這兩天正好趁著假期有空杨拐,可以跟著前人總結(jié)的一些精華祈餐,仔細(xì)研讀一下這個優(yōu)秀的網(wǎng)絡(luò)框架的實現(xiàn)。站在巨人的肩膀上哄陶,才能看得遠(yuǎn)帆阳。

這篇文章先從整體架構(gòu)開始,再從實際使用案例入手屋吨,梳理一下核心邏輯蜒谤,然后再依次了解下各個具體模塊的實現(xiàn),最后再回顧一下 2.x 版本的實現(xiàn)至扰,總結(jié)一下 AFNetworking 的價值鳍徽。

注:這篇文章不會逐行分析源碼,具體的代碼注釋見 這里敢课。

目錄

  • 一阶祭、架構(gòu)
  • 二、核心邏輯
  • 三直秆、AFURLSessionManager
    • 3.1 線程
    • 3.2 AFURLSessionManagerTaskDelegate
    • 3.3 NSProgress
    • 3.4 NSSecureCoding
    • 3.5 _AFURLSessionTaskSwizzling
  • 四濒募、AFURLRequestSerialization
    • 4.1 構(gòu)建普通請求
    • 4.2 構(gòu)建 multipart 請求
  • 五、AFURLResponseSerialization
    • 5.1 -validateResponse:data:error: 方法
    • 5.2 -responseObjectForResponse:data:error: 方法
  • 六圾结、AFSecurityPolicy
    • 6.1 預(yù)備知識點
      • 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 傳輸時是如何驗證證書的?怎樣應(yīng)對中間人偽造證書疫稿?
      • 6.1.7 Certificate Pinning 是什么培他?
    • 6.2 AFSecurityPolicy 的實現(xiàn)
  • 七、AFNetworkReachabilityManager
  • 八遗座、UIKit 擴(kuò)展
  • 九舀凛、AFNetworking 2.x
  • 十、AFNetworking 的價值
    • 10.1 請求調(diào)度:NSURLConnection + NSOperation
    • 10.2 更高層次的抽象
    • 10.3 block
    • 10.4 模塊化
  • 十一途蒋、問題
  • 十二猛遍、收獲

一、架構(gòu)

AFNetworking 一共分為 5 個模塊,2 個核心模塊和 3 個輔助模塊:

  • Core
    • NSURLSession(網(wǎng)絡(luò)通信模塊)
      • AFURLSessionManager(封裝 NSURLSession)
      • AFHTTPSessionManager(繼承自 AFURLSessionManager懊烤,實現(xiàn)了 HTTP 請求相關(guān)的配置)
    • Serialization
      • AFURLRequestSerialization(請求參數(shù)序列化)
        • AFHTTPRequestSerializer
        • AFJSONRequestSerializer
        • AFPropertyListRequestSerializer
      • AFURLResponseSerialization(驗證返回數(shù)據(jù)和反序列化)
        • AFHTTPResponseSerializer
        • AFJSONResponseSerializer
        • AFXMLParserResponseSerializer
        • AFXMLDocumentResponseSerializer (Mac OS X)
        • AFPropertyListResponseSerializer
        • AFImageResponseSerializer
        • AFCompoundResponseSerializer
  • Additional Functionality
    • Security(網(wǎng)絡(luò)通信安全策略模塊)
    • Reachability(網(wǎng)絡(luò)狀態(tài)監(jiān)聽模塊)
    • UIKit(對 iOS 系統(tǒng) UI 控件的擴(kuò)展)
image

<div align="center">圖 1 AFNetworking 整體架構(gòu)</div>

二梯醒、核心邏輯

先來看一下如何使用 AFNetworking 發(fā)送一個 GET 請求:

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);
    }];

首先使用一個 URL,通過調(diào)用 -initWithBaseURL: 方法創(chuàng)建了一個 AFHTTPSessionManager 的實例腌紧,然后再調(diào)用 -GET:parameters:progress:success:failure: 方法發(fā)起請求茸习。

-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é)序列化請求
            - [AFJSONResponseSerializer serializer] // 負(fù)責(zé)序列化響應(yīng)

AFURLSessionManager 是 AFHTTPSessionManager 的父類,
AFURLSessionManager 負(fù)責(zé)創(chuàng)建和管理 NSURLSession 的實例壁肋,管理 AFSecurityPolicy 和初始化 AFNetworkReachabilityManager号胚,來保證請求的安全和查看網(wǎng)絡(luò)連接情況,它有一個 AFJSONResponseSerializer 的實例來序列化 HTTP 響應(yīng)浸遗。

AFHTTPSessionManager 有著自己的 AFHTTPRequestSerializer 和 AFJSONResponseSerializer 來管理請求和響應(yīng)的序列化猫胁,同時依賴父類實現(xiàn)發(fā)出 HTTP 請求、管理 Session 這一核心功能跛锌。

-GET:parameters:progress:success:failure: 方法的調(diào)用棧:

 - [AFHTTPSessionManager GET:parameters:process:success:failure:]
    - [AFHTTPSessionManager dataTaskWithHTTPMethod:parameters:uploadProgress:downloadProgress:success:failure:] // 返回一個 NSURLSessionDataTask 對象
        - [AFHTTPRequestSerializer requestWithMethod:URLString:parameters:error:] // 返回 NSMutableURLRequest
        - [AFURLSessionManager dataTaskWithRequest:uploadProgress:downloadProgress:completionHandler:] 返回一個 NSURLSessionDataTask 對象
            - [NSURLSession dataTaskWithRequest:] 返回一個 NSURLSessionDataTask 對象
            - [AFURLSessionManager addDelegateForDataTask:uploadProgress:downloadProgress:completionHandler:]
                - [AFURLSessionManagerTaskDelegate init]
                - [AFURLSessionManager setDelegate:forTask:] // 為每個 task 創(chuàng)建一個對應(yīng)的 delegate
    - [NSURLSessionDataTask resume]

發(fā)送請求的核心在于創(chuàng)建和啟動一個 data task弃秆,AFHTTPSessionManager 只是提供了 HTTP 請求的接口,內(nèi)部最終還是調(diào)用了父類 AFURLSessionManager 來創(chuàng)建 data task(其實也就是通過 NSURLSession 創(chuàng)建的 task)髓帽,AFURLSessionManager 中會為每個 task 創(chuàng)建一個對應(yīng)的 AFURLSessionManagerTaskDelegate 對象菠赚,用來處理回調(diào)。

在請求發(fā)起時有一個序列化的工具類 AFHTTPRequestSerializer 來處理請求參數(shù)氢卡。

請求回調(diào)時的方法調(diào)用棧:

- [AFURLSessionManager  URLSession:task:didCompleteWithError:]
  - [AFURLSessionManagerTaskDelegate URLSession:task:didCompleteWithError:]
    - [AFJSONResponseSerializer responseObjectForResponse:data:error:]  // 解析 JSON 數(shù)據(jù)
      - [AFHTTPResponseSerializer validateResponse:data:]  // 驗證數(shù)據(jù)
    - [AFURLSessionManagerTaskDelegate URLSession:task:didCompleteWithError:]_block_invoke_2.150
      - [AFHTTPSessionManager dataTaskWithHTTPMethod:URLString:parameters:uploadProgress:downloadProgress:success:failure:]_block_invoke

AFURLSessionManager 在代理方法中收到服務(wù)器返回數(shù)據(jù)的后锈至,會交給 AFURLSessionManagerTaskDelegate 去處理,接著就是用 AFJSONResponseSerializer 去驗證和解析 JSON 數(shù)據(jù)译秦,最后再通過 block 回調(diào)的方式返回最終結(jié)果峡捡。

三、AFURLSessionManager

AFURLSessionManager 是 AFHTTPSessionManager 的父類筑悴,主要有以下幾個功能:

  • 負(fù)責(zé)創(chuàng)建和管理 NSURLSession
  • 管理 NSURLSessionTask
  • 實現(xiàn) NSURLSessionDelegate 等協(xié)議中的代理方法
  • 使用 AFURLSessionManagerTaskDelegate 管理上傳们拙、下載進(jìn)度,以及請求完成的回調(diào)
  • 將整個請求流程相關(guān)的組件串聯(lián)起來
  • 負(fù)責(zé)整個請求過程的線程調(diào)度
  • 使用 AFSecurityPolicy 驗證 HTTPS 請求的證書

1. 線程

一般調(diào)用 AFNetworking 的請求 API 時阁吝,都是在主線程砚婆,也是主隊列。然后直到調(diào)用 NSURLSession 的 -resume 方法突勇,一直都是在主線程装盯。

在 AFURLSessionManager 的初始化方法中,設(shè)置了 NSURLSession 代理回調(diào)線程的最大并發(fā)數(shù)為 1甲馋,因為就像 NSURLSession 的 -sessionWithConfiguration:delegate:delegateQueue: 方法的官方文檔中所說的那樣埂奈,所有的代理方法回調(diào)都應(yīng)該在一個串行隊列中,因為只有這樣才能保證代理方法的回調(diào)順序定躏。

NSURLSession 代理方法回調(diào)是異步的账磺,所以收到回調(diào)時的線程模式是“異步+串行隊列”芹敌,這個時候可以理解為處于回調(diào)線程。

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
    ...
    self.operationQueue = [[NSOperationQueue alloc] init];
    self.operationQueue.maxConcurrentOperationCount = 1;  // 代理回調(diào)線程最大并發(fā)數(shù)為 1

    // 初始化 NSURLSession 對象
    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ù)解析完成后再異步回到主隊列或者自定義隊列冒版。

- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    
    ...
    
    // 如果請求成功液茎,則在一個 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);
            }
            ...
        });
    });
    
    ...
}

問題:
有個讓我感到困惑的地方是辞嗡,這里最后回調(diào)時為什么要用 dispatch_group_async 將任務(wù)放到隊列組中去執(zhí)行豁护,搜了一下也沒看到這個組中的任務(wù)執(zhí)行完了要做什么,難道是要留給外面的調(diào)用方用的欲间?

image

<div align="center">圖 2 AFNetworking 中的線程調(diào)度</div>

2. AFURLSessionManagerTaskDelegate

AFURLSessionManager 中幾乎實現(xiàn)了所有的 NSURLSession 相關(guān)的協(xié)議方法:

  • NSURLSessionDelegate
  • NSURLSessionTaskDelegate
  • NSURLSessionDataDelegate
  • NSURLSessionDownloadDelegate

但是AFURLSessionManager 中實現(xiàn)的這些代理方法都只是做一些非核心邏輯的處理,每個代理方法中都回調(diào)了一個自定義邏輯的 block断部,如果 block 被賦值了猎贴,那么就調(diào)用它。

AFURLSessionManager 把最核心的代理回調(diào)處理交給 AFURLSessionManagerTaskDelegate 類去實現(xiàn)了蝴光,AFURLSessionManagerTaskDelegate 可以根據(jù)對應(yīng)的 task 去進(jìn)行上傳她渴、下載進(jìn)度回調(diào)和請求完成的回調(diào)處理:

- URLSession:task:didCompleteWithError:
- URLSession:dataTask:didReceiveData:
- URLSession:downloadTask:didFinishDownloadingToURL:

AFURLSessionManager 通過屬性 mutableTaskDelegatesKeyedByTaskIdentifier (一個 NSDictionary 對象)來存儲并管理每一個 NSURLSessionTask 所對應(yīng)的 AFURLSessionManagerTaskDelegate,它以 taskIdentifier 為鍵存儲 task蔑祟。在請求最終完成后趁耗,又將 AFURLSessionManagerTaskDelegate 移除。

image

<div align="center">圖 3 AFNetworking 中的代理回調(diào)邏輯</div>

3. NSProgress

AFURLSessionManagerTaskDelegate 借助了 NSProgress 這個類來實現(xiàn)進(jìn)度的管理疆虚,NSProgress 是 iOS 7 引進(jìn)的一個用來管理任務(wù)進(jìn)度的類苛败,可以表示一個任務(wù)的進(jìn)度信息,我們還可以對其進(jìn)行開始
暫停径簿、取消等操作罢屈,完整的對應(yīng)了 task 的各種狀態(tài)。

AFURLSessionManagerTaskDelegate 通過 KVO 監(jiān)聽 task 的進(jìn)度更新篇亭,來同步更新 NSProgress 的進(jìn)度數(shù)據(jù)缠捌。同時,還用 KVO 監(jiān)聽了 NSProgress 的 fractionCompleted 屬性的變化译蒂,用來更新最外面的進(jìn)度回調(diào) block曼月,回調(diào)時將這個 NSProgress 對象作為參數(shù)帶過去。

另一方面柔昼,AFURLSessionManagerTaskDelegate 中還分別對下載和上傳的 NSProgress 對象設(shè)置了開始哑芹、暫停、取消等操作的 handler岳锁,將 task 跟 NSProgress 的狀態(tài)關(guān)聯(lián)起來绩衷。這樣一來蹦魔,就可以通過控制 NSProgress 對象的這些操作就可以控制 task 的狀態(tài)。

延伸閱讀:

4. NSSecureCoding

AFNetworking 的大多數(shù)類都支持歸檔解檔咳燕,但實現(xiàn)的是 NSSecureCoding 協(xié)議勿决,而不是 NSCoding 協(xié)議,這兩個協(xié)議的區(qū)別在于 NSSecureCoding 協(xié)議中定義的解碼的方法是 -decodeObjectOfClass:forKey: 方法招盲,而不是 -decodeObjectForKey:低缩,這就要求解數(shù)據(jù)時要指定 Class。在 bang 的文章中看到說是這樣做更安全曹货,因為序列化后的數(shù)據(jù)有可能被篡改玩般,若不指定 Class够颠,decode 出來的對象可能不是原來的對象综苔,有潛在風(fēng)險芥颈。(不過暫時還是沒能理解岩四。)

5. _AFURLSessionTaskSwizzling

_AFURLSessionTaskSwizzling 的唯一作用就是將 NSURLSessionTask 的 -resume-suspend 方法實現(xiàn)替換成自己的實現(xiàn),_AFURLSessionTaskSwizzling 中這兩個方法的實現(xiàn)是先調(diào)用原方法剖煌,然后再發(fā)出一個通知刚夺。

_AFURLSessionTaskSwizzling 是通過在 +load 方法中進(jìn)行 Method Swizzling 來實現(xiàn)方法交換的,由于 NSURLSessionTask 的實現(xiàn)是類簇末捣,不能直接通過調(diào)用 +class 來獲取真正的類,而且在 iOS 7 和 iOS 8 下的實現(xiàn)不同创橄,所以這里的 swizzling 實現(xiàn)起來有點復(fù)雜箩做。具體原因見 GitHub 上的討論

問題:
有點不明白的是妥畏,NSURLSessionTask 有三個子類:NSURLSessionDataTask邦邦、NSURLSessionDownloadTask 和 NSURLSessionUploadTask,為什么不用考慮這三個子類自己也實現(xiàn)了自己的 -resume-suspend 方法的情況呢醉蚁?

四燃辖、AFURLRequestSerialization

AFURLRequestSerialization 是一個抽象的協(xié)議,用于構(gòu)建一個規(guī)范的 NSURLRequest网棍∏辏基于 AFURLRequestSerialization 協(xié)議,AFNetworking 提供了 3 中不同數(shù)據(jù)形式的序列化工具(當(dāng)然你也可以自定義其他數(shù)據(jù)格式的序列化類):

  • AFHTTPRequestSerializer:普通的 HTTP 請求滥玷,默認(rèn)數(shù)據(jù)格式是 application/x-www-form-urlencoded氏身,也就是 key-value 形式的 url 編碼字符串
  • AFJSONRequestSerializer:參數(shù)格式是 json
  • AFPropertyListRequestSerializer:參數(shù)格式是蘋果的 plist 格式
image

<div align="center">圖 4 AFURLRequestSerialization 類圖</div>

AFHTTPRequestSerializer 主要實現(xiàn)了兩個功能:

  • 構(gòu)建普通請求:格式化請求參數(shù),生成 HTTP Header惑畴。
  • 構(gòu)建 multipart 請求蛋欣,上傳數(shù)據(jù)時會用到。

1. 構(gòu)建普通請求

AFHTTPRequestSerializer 在構(gòu)建普通請求時如贷,做了以下幾件事:

  • 創(chuàng)建 NSURLRequest
  • 設(shè)置 NSURLRequest 相關(guān)屬性
  • 設(shè)置 HTTP Method
  • 設(shè)置 HTTP Header
  • 序列化請求參數(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)建請求
    NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url];
    mutableRequest.HTTPMethod = method; // 設(shè)置 Method

    // 這里本來是直接把 self 的一些屬性值直接傳給 request 的陷虎,但是因為初始默認(rèn)情況下到踏,
    // 當(dāng)前類中與 NSURLRequest 相關(guān)的那些屬性值為 0,而導(dǎo)致外面業(yè)務(wù)方使用 NSURLSessionConfiguration 設(shè)置屬性時失效尚猿,
    // 所以通過對這些屬性添加了 KVO 監(jiān)聽判斷是否有值來解決這個傳值的有效性問題
    // 詳見 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窝稿;請求參數(shù)序列化,再添加到 query string 或者 body 中
    mutableRequest = [[self requestBySerializingRequest:mutableRequest withParameters:parameters error:error] mutableCopy];

    return mutableRequest;
}

在設(shè)置 NSURLRequest 相關(guān)屬性時谊路,有點繞讹躯,本來可以直接將 AFHTTPRequestSerializer 自己的屬性值傳給 NSURLRequest 對象的螟炫,但是后來改成了 KVO 的形式旺坠,主要是因為 NSURLRequest 對象有些屬性是純量數(shù)據(jù)類型(比如 timeoutInterval)袱吆,在 AFHTTPRequestSerializer 初始化后万伤,這些跟 NSURLRequest 相關(guān)的屬性值初始默認(rèn)值是 0系洛,所以是不知道外面有沒有設(shè)置過值牲平,如果將 AFHTTPRequestSerializer 的值都傳給 NSURLRequest 對象的話渊季,很有可能會導(dǎo)致 NSURLSessionConfiguration 中設(shè)置的相同屬性失效唆涝。

AFNetworking 幫我們組裝好了一些 HTTP 請求頭脱羡,包括:

  • Content-Type萝究,請求參數(shù)類型
  • Accept-Language,根據(jù) [NSLocale preferredLanguages] 方法讀取本地語言锉罐,告訴服務(wù)端自己能接受的語言帆竹。
  • User-Agent
  • Authorization,提供 Basic Auth 認(rèn)證接口脓规,幫我們把用戶名密碼做 base64 編碼后放入 HTTP 請求頭栽连。

一般我們請求都會按 key=value 的方式帶上各種參數(shù),GET 方法參數(shù)直接拼在 URL 后面侨舆,POST 方法放在 body 上秒紧,NSURLRequest 沒有封裝好這個參數(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 對象的形式,保存到數(shù)組中(這樣做的目的是因為最后可以根據(jù)不同的字符串編碼生成對應(yīng)的 key=value 字符串)
  • 然后取出數(shù)組中的 AFQueryStringPair 對象帜讲,轉(zhuǎn)成一個個 NSString 對象再保存到新數(shù)組中
  • 最后再將這些 key=value 的字符串用 & 符號拼接起來
@{
     @"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",        // 注:實際代碼中這里的 “=” 會被編碼
    @"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"

請求參數(shù)序列化完成后衅谷,再根據(jù)不同的 HTTP 請求方法分別處理,對于 GET/HEAD/DELETE 方法似将,把參數(shù)直接加到 URL 后面获黔,對于其他如 POST/PUT 等方法蚀苛,把數(shù)據(jù)加到 body 上,并設(shè)好 HTTP 頭中的 Content-Typeapplication/x-www-form-urlencoded玷氏,告訴服務(wù)端字符串的編碼是什么堵未。

2. 構(gòu)建 multipart 請求

這部分有點復(fù)雜,暫時還沒看盏触。

五渗蟹、AFURLResponseSerialization

AFURLResponseSerialization 模塊負(fù)責(zé)解析網(wǎng)絡(luò)返回數(shù)據(jù),檢查數(shù)據(jù)是否合法赞辩,把服務(wù)器返回的 NSData 數(shù)據(jù)轉(zhuǎn)成相應(yīng)的對象雌芽。
AFURLResponseSerialization 模塊包括一個協(xié)議、一個基類和多個解析特定格式數(shù)據(jù)的子類辨嗽,用戶可以很方便地繼承基類 AFHTTPResponseSerializer 去解析更多的數(shù)據(jù)格式:

  • AFURLResponseSerialization 協(xié)議世落,定義了解析響應(yīng)數(shù)據(jù)的接口
  • AFHTTPResponseSerializer, HTTP 請求響應(yīng)數(shù)據(jù)解析器的基類
  • AFJSONResponseSerializer糟需,專門解析 JSON 數(shù)據(jù)的解析器
  • 其他數(shù)據(jù)格式(XML屉佳、image、plist等)的響應(yīng)解析器
  • AFCompoundResponseSerializer洲押,組合解析器武花,可以將多個解析器組合起來,以同時支持多種格式的數(shù)據(jù)解析

image

<div align="center">圖 5 AFURLResponseSerialization 類圖</div>

AFURLResponseSerialization 模塊響應(yīng)解析機(jī)制主要涉及到兩個核心方法:

  • AFHTTPResponseSerializer 中定義杈帐、實現(xiàn)的 -validateResponse:data:error: 方法
  • AFURLResponseSerialization 協(xié)議定義的 -responseObjectForResponse:data:error: 方法

1. -validateResponse:data:error: 方法

AFHTTPResponseSerializer 作為解析器基類髓堪,提供了 acceptableContentTypesacceptableStatusCodes 兩個屬性,并提供了 acceptableStatusCodes 的默認(rèn)值娘荡,子類可以通過設(shè)置這兩個屬性的值來進(jìn)行自定義配置。AFHTTPResponseSerializer 中的 -validateResponse:data:error: 方法會根據(jù)這兩個屬性值來判斷響應(yīng)的文件類型 MIMEType 和狀態(tài)碼 statusCode 是否合法驶沼。

比如 AFJSONResponseSerializer 中設(shè)置了 acceptableContentTypes 的值為 [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil]炮沐,如果服務(wù)器返回的 Content-Type 不是這三者之一,-validateResponse:data:error: 方法就會返回解析失敗的錯誤信息回怜。

案例:在網(wǎng)上看到有開發(fā)者就曾經(jīng)遇到過相關(guān)的問題
——服務(wù)器返回的數(shù)據(jù)是 JSON 數(shù)據(jù)大年,但是 Content-Type 卻不符合要求,結(jié)果導(dǎo)致解析失敗玉雾。

2. -responseObjectForResponse:data:error: 方法

AFJSONResponseSerializer 等子類中實現(xiàn)的 -responseObjectForResponse:data:error: 方法會先調(diào)用 -validateResponse:data:error: 方法驗證數(shù)據(jù)是否合法翔试,拿到驗證結(jié)果后,接著這里有個補(bǔ)充判斷條件——如果是 content type 的錯誤就直接返回 nil复旬,因為數(shù)據(jù)類型不符合要求垦缅,就沒必要再繼續(xù)解析數(shù)據(jù)了,如果是 status code 的錯誤就繼續(xù)解析驹碍,因為數(shù)據(jù)本身沒問題壁涎,而錯誤信息有可能就在返回的數(shù)據(jù)中凡恍,所以這種情況下會將 status code 產(chǎn)生的錯誤信息和解析后的數(shù)據(jù)一起“打包”返回。

AFJSONResponseSerializer 在解析數(shù)據(jù)后還提供了移除 NSNull 的功能怔球,主要是為了防止服務(wù)端返回 null 時導(dǎo)致解析后的數(shù)據(jù)中有了脆弱的 NSNull嚼酝,這樣很容易導(dǎo)致崩潰(但是之前一直沒發(fā)現(xiàn)這個功能[捂臉])。

- (id)responseObjectForResponse:(NSURLResponse *)response
                           data:(NSData *)data
                          error:(NSError *__autoreleasing *)error
{
    if (![self validateResponse:(NSHTTPURLResponse *)response data:data error:error]) {
        
        // 如果是 content type 的錯誤就直接返回竟坛,因為數(shù)據(jù)類型不符合要求
        // 如果是 status code 的錯誤就繼續(xù)解析闽巩,因為錯誤信息有可能就在返回的數(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

幾個關(guān)鍵字:HTTPS涎跨,TSL,SSL漫试,SSL Pinning六敬,非對稱加密算法

1. 預(yù)備知識點

1.1 為什么要使用 HTTPS

因為直接使用 HTTP 請求,就會有可能遇到以下幾個安全問題:

  • 傳輸數(shù)據(jù)被竊聽:HTTP 報文使用明文方式發(fā)送驾荣,而且 HTTP 本身不具備加密的功能外构,而互聯(lián)網(wǎng)是由聯(lián)通世界各個地方的網(wǎng)絡(luò)設(shè)施組成,所有發(fā)送和接收經(jīng)過某些設(shè)備的數(shù)據(jù)都可能被截獲或窺視播掷。
  • 認(rèn)證問題:
    • 無法確認(rèn)你發(fā)送到的服務(wù)器就是真正的目標(biāo)服務(wù)器(可能服務(wù)器是偽裝的)
    • 無法確定返回的客戶端是否是按照真實意圖接收的客戶端(可能是偽裝的客戶端)
    • 無法確定正在通信的對方是否具備訪問權(quán)限,Web 服務(wù)器上某些重要的信息审编,只想發(fā)給特定用戶
  • 傳輸內(nèi)容可能被篡改:請求或響應(yīng)在傳輸途中,可能會被攻擊者攔截并篡改內(nèi)容歧匈,也就是所謂的中間人攻擊(Man-in-the-Middle attack垒酬,MITM)。

1.2 HTTPS 的出現(xiàn)

HTTPS件炉,也稱作 HTTP over TLS勘究,HTTPS 就是基于 TLS 的 HTTP 請求。TLS 是一種基于 TCP 的加密協(xié)議斟冕,它主要做了兩件事:傳輸?shù)膬啥丝梢曰ハ囹炞C對方的身份口糕,以及驗證后加密所傳輸?shù)臄?shù)據(jù)。

HTTPS 通過驗證和加密兩種手段的結(jié)合解決了上面 HTTP 所面臨的 3 個安全問題磕蛇。

1.3 SSL/TLS 協(xié)議

SSL(Secure Sockets Layer):SSL 協(xié)議是一種數(shù)據(jù)加密協(xié)議景描,為了保證網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)陌踩裕W(wǎng)景公司設(shè)計了 SSL 協(xié)議用于對 HTTP 協(xié)議傳輸?shù)臄?shù)據(jù)進(jìn)行加密秀撇,從而就誕生了 HTTPS超棺。

TLS(Transport Layer Security):TLS 協(xié)議是 SSL 協(xié)議的升級版。1999年呵燕,互聯(lián)網(wǎng)標(biāo)準(zhǔn)化組織 ISOC 接替 NetScape 公司棠绘,發(fā)布了 SSL 的升級版 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ù)前會進(jìn)行證書認(rèn)證候引,第三方無法偽造服務(wù)端(客戶端)身份
數(shù)據(jù)完整性 沒有完整性校驗過程 內(nèi)容傳輸經(jīng)過完整性校驗

HTTP協(xié)議和安全協(xié)議(SSL/TLS)同屬于應(yīng)用層(OSI模型的最高層)侯养,具體來講,安全協(xié)議(SSL/TLS)工作在 HTTP 之下澄干,傳輸層之上:安全協(xié)議向運行 HTTP 的進(jìn)程提供一個類似于 TCP 的套接字逛揩,供進(jìn)程向其中注入報文,安全協(xié)議將報文加密并注入傳輸層套接字麸俘;或是從運輸層獲取加密報文辩稽,解密后交給對應(yīng)的進(jìn)程。嚴(yán)格地講从媚,HTTPS 并不是一個單獨的協(xié)議逞泄,而是對工作在一加密連接(TLS或SSL)上的常規(guī) HTTP 協(xié)議的稱呼。

image

<div align="center">圖 6 協(xié)議層</div>

HTTPS 報文中的任何東西都被加密拜效,包括所有報頭和荷載(payload)喷众。除了可能的選擇密文攻擊之外,一個攻擊者所能知道的只有在兩者之間有一連接這件事紧憾。

1.5 HTTPS 連接的建立過程

HTTPS在傳輸數(shù)據(jù)之前需要客戶端與服務(wù)端之間進(jìn)行一次握手到千,在握手過程中將確立雙方加密傳輸數(shù)據(jù)的密碼信息。(握手過程采用的非對稱加密赴穗,正式傳輸數(shù)據(jù)時采用的是對稱加密)

HTTPS 的認(rèn)證有單向認(rèn)證和雙向認(rèn)證,這里簡單梳理一下客戶端單向認(rèn)證時的握手流程:

(1)客戶端發(fā)起一個請求了赵,服務(wù)端響應(yīng)后返回一個證書甸赃,證書中包含一些基本信息和公鑰辑奈。
(2)客戶端里存有各個受信任的證書機(jī)構(gòu)根證書鸠窗,用這些根證書對服務(wù)端返回的證書進(jìn)行驗證胯究,如果不可信任稍计,則請求終止。
(3)如果證書受信任裕循,或者是用戶接受了不受信的證書臣嚣,客戶端會生成一串隨機(jī)數(shù)的密碼 random key净刮,并用證書中提供的公鑰加密,再返回給服務(wù)器硅则。
(4)服務(wù)器拿到加密后的隨機(jī)數(shù)淹父,利用私鑰解密,然后再用解密后的隨機(jī)數(shù) random key怎虫,對需要返回的數(shù)據(jù)加密暑认,加密完成后將數(shù)據(jù)返回給客戶端。
(5)最后用戶拿到被加密過的數(shù)據(jù)大审,用客戶端一開始生成的那個隨機(jī)數(shù) random key蘸际,進(jìn)行數(shù)據(jù)解密。整個 TLS/SSL 握手過程完成徒扶。

image

<div align="center">圖 7 TLS/SSL 握手過程(單向認(rèn)證)</div>

完整的 HTTPS 連接的建立過程粮彤,包括下面三個步驟:

(1)TCP 協(xié)議的三次握手;
(2)TLS/SSL 協(xié)議的握手姜骡、密鑰協(xié)商导坟;
(3)使用共同約定的密鑰開始通信。

image

<div align="center">圖 8 完整的 HTTPS 連接的建立過程</div>

1.6 HTTPS 傳輸時是如何驗證證書的?怎樣應(yīng)對中間人偽造證書漾狼?

先來看看維基百科上對對稱加密和非對稱加密的解釋:

對稱密鑰加密(英語:Symmetric-key algorithm)又稱為對稱加密、私鑰加密稽煤、共享密鑰加密,是密碼學(xué)中的一類加密算法。<u>這類算法在加密和解密時使用相同的密鑰察藐,或是使用兩個可以簡單地相互推算的密鑰</u>悴务。實務(wù)上,這組密鑰成為在兩個或多個成員間的共同秘密裂垦,以便維持專屬的通訊聯(lián)系。與公開密鑰加密相比,要求雙方取得相同的密鑰是對稱密鑰加密的主要缺點之一闸准。

公開密鑰加密(英語:public-key cryptography,又譯為公開密鑰加密)库快,也稱為非對稱加密(asymmetric cryptography),一種密碼學(xué)算法類型闽铐,在這種密碼學(xué)方法中兄墅,需要一對密鑰(其實這里密鑰說法不好,就是“鑰”)扎瓶,一個是私人密鑰,另一個則是公開密鑰误证。<u>這兩個密鑰是數(shù)學(xué)相關(guān),用某用戶密鑰加密后所得的信息,只能用該用戶的解密密鑰才能解密青团。如果知道了其中一個,并不能計算出另外一個咕缎。因此如果公開了一對密鑰中的一個锨阿,并不會危害到另外一個的秘密性質(zhì)</u>。稱公開的密鑰為公鑰末早;不公開的密鑰為私鑰。

從上面可以看出非對稱加密的特點:非對稱加密有一對公鑰私鑰姿搜,用公鑰加密的數(shù)據(jù)只能通過對應(yīng)的私鑰解密梭纹,用私鑰加密的數(shù)據(jù)只能通過對應(yīng)的公鑰解密。這種加密是單向的绍载。

(1)HTTPS 傳輸時是如何驗證證書的呢?

我們以最簡單的為例:一個證書頒發(fā)機(jī)構(gòu)(CA)曙痘,頒發(fā)了一個證書 Cer,服務(wù)器用這個證書建立 HTTPS 連接茧痒,同時客戶端在信任列表里有這個 CA 機(jī)構(gòu)的根證書。

CA 機(jī)構(gòu)頒發(fā)的證書 Cer 里包含有證書內(nèi)容 Content区拳,以及證書加密內(nèi)容 Crypted Content(數(shù)字簽名)届良,這個加密內(nèi)容 Crypted Content 就是用這個證書機(jī)構(gòu)的私鑰對內(nèi)容 Content 加密的結(jié)果乞而。

+-------------------+
|      Content      |
+-------------------+
|   Crypted Content |
+-------------------+
     證書 Cer

建立 HTTPS 連接時欠啤,服務(wù)端會把證書 Cer 返回給客戶端声滥,客戶端系統(tǒng)里的 CA 機(jī)構(gòu)根證書有這個 CA 機(jī)構(gòu)的公鑰,用這個公鑰對證書 Cer 的加密內(nèi)容 Crypted Content 解密得到 Content,跟證書 Cer 里的內(nèi)容 Content 對比龙考,若相等就通過驗證。大概的流程如下:

       +-----------------------------------------------------+
       |           crypt with private key                    |
       |  Content ------------------------> Crypted Content  |
Server |                                                     |
       |                     證書 Cer                        |
       +-----------------------------------------------------+

                          ||        
                          ||
                          \/
                        
       +-----------------------------------------------------+
       |                                                     |
       |               Content  &  Crypted Content           |
Client |                  |               |                  |
       |                  |  證書 Cer     |                 |
       +------------------|---------------|------------------+ 
                    |         | 
                    |         | derypt with public key   
                    |         | 
                    \/    相等?  \/
                 Content√彻帧------ Decrypted Content  
                 
                                   

(2)怎樣應(yīng)對中間人偽造證書居灯?

因為中間人不會有 CA 機(jī)構(gòu)的私鑰,即便偽造了一張證書喇勋,但是私鑰不對,加密出來的內(nèi)容也就不對熄云,客戶端也就無法通過 CA 公鑰解密,所以偽造的證書肯定無法通過驗證矗漾。

1.7 Certificate Pinning 是什么薄料?

如果一個客戶端通過 TLS 和服務(wù)器建立連接敞贡,操作系統(tǒng)會驗證服務(wù)器證書的有效性(一般是按照 X.509 標(biāo)準(zhǔn))。當(dāng)然摄职,有很多手段可以繞開這個校驗誊役,最直接的是在 iOS 設(shè)備上安裝證書并且將其設(shè)置為可信的。這種情況下谷市,實施中間人攻擊也不是什么難事甫男。不過通過 Certificate Pinning 可以解決這個問題感混。

A client that does key pinning adds an extra step beyond the normal X.509 certificate validation.

—— Wikipedia:Certificate Pinning

Certificate Pinning ,可以理解為證書綁定搀庶,有時候又叫 SSL Pinning,其實更準(zhǔn)確的叫法應(yīng)該是 Public Key Pinning(公鑰綁定)董瞻。證書綁定是一種檢測和防止“中間人攻擊”的方式截珍,客戶端直接保存服務(wù)端的證書埠居,當(dāng)建立 TLS 連接后,應(yīng)立即檢查服務(wù)器的證書枷餐,<u>不僅要驗證證書的有效性,還需要確定證書是不是跟客戶端本地的證書相匹配</u>蟀苛。考慮到應(yīng)用和服務(wù)器需要同時升級證書的要求擂错,這種方式比較適合應(yīng)用在訪問自家服務(wù)器的情況下遂赠。

為什么直接對比就能保證證書沒問題绒瘦?

如果中間人從客戶端取出證書操骡,再偽裝成服務(wù)端跟其他客戶端通信键痛,它發(fā)送給客戶端的這個證書不就能通過驗證嗎?確實可以通過驗證,但后續(xù)的流程走不下去,因為下一步客戶端會用證書里的公鑰加密尿背,中間人沒有這個證書的私鑰就解不出內(nèi)容端仰,也就截獲不到數(shù)據(jù),這個證書的私鑰只有真正的服務(wù)端有田藐,中間人偽造證書主要偽造的是公鑰荔烧。

什么情況下需要使用 Certificate Pinning?

  • 就像前面所說的汽久,常規(guī)的驗證方式并不能避免遭遇中間人攻擊鹤竭,因為如果所訪問網(wǎng)站的證書是自制的,而且在客戶端上通過手動安裝根證書信任了景醇,此時就很容易被惡意攻擊了(還記得你訪問 12306 時收到的證書驗證提醒嗎)臀稚。
  • 如果服務(wù)端的證書是從受信任的的 CA 機(jī)構(gòu)頒發(fā)的,驗證是沒問題的三痰,但 CA 機(jī)構(gòu)頒發(fā)證書比較昂貴吧寺,小企業(yè)或個人用戶可能會選擇自己頒發(fā)證書,這樣就無法通過系統(tǒng)受信任的 CA 機(jī)構(gòu)列表驗證這個證書的真?zhèn)瘟恕?/li>

2. AFSecurityPolicy 的實現(xiàn)

2.1 AFSecurityPolicy 的作用

NSURLConnection 和 NSURLSession 已經(jīng)封裝了 HTTPS 連接的建立酒觅、數(shù)據(jù)的加密解密功能撮执,我們直接使用 NSURLConnection 或者 NSURLSession 也是可以訪問 HTTPS 網(wǎng)站的微峰,但 NSURLConnection 和 NSURLSession 并沒有驗證證書是否合法舷丹,無法避免中間人攻擊。要做到真正安全通訊蜓肆,需要我們手動去驗證服務(wù)端返回的證書(系統(tǒng)提供了 SecTrustEvaluate函數(shù)供我們驗證證書使用)颜凯。

AFSecurityPolicy 幫我們封裝了證書驗證的邏輯,讓用戶可以輕易使用仗扬,除了在系統(tǒng)的信任機(jī)構(gòu)列表里驗證症概,還支持 SSL Pinning 方式的驗證。

2.2 使用方法

如果是權(quán)威機(jī)構(gòu)頒發(fā)的證書早芭,不需要任何設(shè)置彼城。

如果是自簽名證書,但是不做證書綁定,直接按照下面的代碼實現(xiàn)即可:

AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
// 允許無效證書(包括自簽名證書)募壕,必須的
policy.allowInvalidCertificates = YES;
// 是否驗證域名的CN字段
// 不是必須的调炬,但是如果寫YES,則必須導(dǎo)入證書舱馅。
policy.validatesDomainName = NO;

AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:<#MyAPIBaseURLString#>]];
manager.securityPolicy = securityPolicy;

如果是自簽名證書缰泡,而且還要做證書綁定,就需要把自簽的服務(wù)端證書代嗤,或者自簽的CA根證書導(dǎo)入到項目中(把 cer 格式的服務(wù)端證書放到 APP 項目資源里棘钞,AFSecurityPolicy 會自動尋找根目錄下所有 cer 文件,當(dāng)然你也可以自己讀雀梢恪)宜猜,然后再選擇驗證證書或者公鑰。


AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
// 允許無效證書(包括自簽名證書)硝逢,必須的
policy.allowInvalidCertificates = YES;
// 是否驗證域名的CN字段宝恶,不是必須的
policy.validatesDomainName = NO;

AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:<#MyAPIBaseURLString#>]];
manager.securityPolicy = securityPolicy;

2.3 AFSecurityPolicy 的實現(xiàn)

詳細(xì)說明見源碼注釋。

在 AFURLSessionManager 中實現(xiàn)的 -URLSession:didReceiveChallenge:completionHandler: 方法中趴捅,根據(jù) NSURLAuthenticationChallenge 對象中的 authenticationMethod垫毙,來決定是否需要驗證服務(wù)器證書,如果需要驗證拱绑,則借助 AFSecurityPolicy 來驗證證書综芥,驗證通過則創(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:取消整個請求
     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ù)器端需要客戶端驗證服務(wù)器返回的證書信息
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            
            // 客戶端根據(jù)安全策略驗證服務(wù)器返回的證書
            // AFSecurityPolicy 在這里的作用就是膀藐,使得在系統(tǒng)底層自己去驗證之前,AF可以先去驗證服務(wù)端的證書红省。如果通不過额各,則直接越過系統(tǒng)的驗證,取消https的網(wǎng)絡(luò)請求吧恃。否則虾啦,繼續(xù)去走系統(tǒng)根證書的驗證(?痕寓?)傲醉。
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                // 信任的話,就創(chuàng)建驗證憑證去做系統(tǒng)根證書驗證
                
                // 創(chuàng)建 NSURLCredential 前需要調(diào)用 SecTrustEvaluate 方法來驗證證書呻率,這件事情其實 AFSecurityPolicy 已經(jīng)幫我們做了
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                if (credential) {
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                // 不信任的話硬毕,就直接取消整個請求
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }

    if (completionHandler) {
        // 疑問:這個 completionHandler 是用來干什么的呢?credential 又是用來干什么的呢礼仗?
        completionHandler(disposition, credential);
    }
}

而 AFSecurityPolicy 的核心就在于 -evaluateServerTrust:forDomain: 方法吐咳,該方法中主要做了四件事:

  • 設(shè)置驗證標(biāo)準(zhǔn)(SecTrustSetPolicies)逻悠,為認(rèn)證做準(zhǔn)備
  • 處理 SSLPinningMode 為 AFSSLPinningModeNone 的情況——如果允許無效的證書(包括自簽名證書)就直接返回 YES,不允許的話就在系統(tǒng)的信任機(jī)構(gòu)列表里驗證服務(wù)端證書韭脊。
  • 處理 SSLPinningMode 為 AFSSLPinningModeCertificate 的情況蹂风,認(rèn)證證書——設(shè)置證書錨點->驗證服務(wù)端證書->匹配服務(wù)端證書鏈
  • 處理 SSLPinningMode 為 AFSSLPinningModePublicKey 的情況,認(rèn)證公鑰——匹配服務(wù)端證書公鑰
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    /*
     AFSecurityPolicy 的四個主要屬性:
     SSLPinningMode - 證書認(rèn)證模式
     pinnedCertificates - 用來匹配服務(wù)端證書信息的證書乾蓬,這些證書保存在客戶端
     allowInvalidCertificates - 是否支持無效的證書(包括自簽名證書)
     validatesDomainName - 是否去驗證證書域名是否匹配
     
     
     SSLPinningMode 提供的三種證書認(rèn)證模式:
     AFSSLPinningModeNone - 沒有 SSL pinning
     AFSSLPinningModePublicKey - 用證書綁定方式驗證惠啄,客戶端要有服務(wù)端的證書拷貝,只是驗證時只驗證證書里的公鑰任内,不驗證證書的有效期等信息
     AFSSLPinningModeCertificate - 用證書綁定方式驗證證書撵渡,需要客戶端保存有服務(wù)端的證書拷貝,這里驗證分兩步死嗦,第一步驗證證書的域名/有效期等信息趋距,第二步是對比服務(wù)端返回的證書跟客戶端返回的是否一致。
     
     */
    
    
    // 判斷互相矛盾的情況:
    // 如果有域名越除,而且還要允許自簽證書节腐,同時還要驗證域名的話,就一定要驗證服務(wù)器返回的證書是否匹配客戶端本地的證書了
    // 所以必須滿足兩個條件:A驗證模式不能為 FSSLPinningModeNone摘盆;添加到項目里的證書至少 1 個翼雀。
    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,也就是告訴客戶端如何驗證 serverTrust
    // 如果要驗證域名的話孩擂,就以域名為參數(shù)創(chuàng)建一個 SecPolicyRef狼渊,否則會創(chuàng)建一個符合 X509 標(biāo)準(zhǔn)的默認(rèn) SecPolicyRef 對象
    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) {
        // 如果不做證書綁定,就會跟瀏覽器一樣在系統(tǒng)的信任機(jī)構(gòu)列表里驗證服務(wù)端返回的證書(如果是自己買的證書类垦,就不需要綁定證書了狈邑,可以直接在系統(tǒng)的信任機(jī)構(gòu)列表里驗證就行了)
        // 如果允許無效的證書(包括自簽名證書)就會直接返回 YES,不允許的話就會對服務(wù)端證書在系統(tǒng)的信任機(jī)構(gòu)列表里驗證蚤认。如果服務(wù)器證書無效米苹,并且不允許無效證書,就會返回 NO
        
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
        
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        // 如果不是 AFSSLPinningModeNone砰琢,而且證書在系統(tǒng)的信任機(jī)構(gòu)列表里驗證失敗蘸嘶,同時不允許無效的證書(包括自簽名證書)時,直接返回評估失敗
        // (如果是自簽名的證書氯析,驗證時就需要做證書綁定亏较,或者直接在系統(tǒng)的信任機(jī)構(gòu)列表里中添加根證書)
        
        return NO;
    }

    // 根據(jù) SSLPinningMode 對服務(wù)端返回的證書進(jìn)行 SSL Pinning 驗證莺褒,也就是說拿本地的證書和服務(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è)置成需要參與驗證的 Anchor Certificate(錨點證書,嵌入到操作系統(tǒng)中的根證書遵岩,也就是權(quán)威證書頒發(fā)機(jī)構(gòu)頒發(fā)的自簽名證書)你辣,通過 SecTrustSetAnchorCertificates 設(shè)置了參與校驗錨點證書之后巡通,假如驗證的數(shù)字證書是這個錨點證書的子節(jié)點,即驗證的數(shù)字證書是由錨點證書對應(yīng)CA或子CA簽發(fā)的舍哄,或是該證書本身宴凉,則信任該證書,具體就是調(diào)用 SecTrustEvaluate 來驗證表悬。
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);

            // 自簽證書在之前是驗證通過不了的弥锄,在這一步,把我們自己設(shè)置的證書加進(jìn)去之后蟆沫,就能驗證成功了籽暇。
            // 再去調(diào)用之前的 serverTrust 去驗證該證書是否有效,有可能經(jīng)過這個方法過濾后饭庞,serverTrust 里面的 pinnedCertificates 被篩選到只有信任的那一個證書
            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }

            // 注意戒悠,這個方法和我們之前的錨點證書沒關(guān)系了,是去從我們需要被驗證的服務(wù)端證書舟山,去拿證書鏈绸狐。
            // 服務(wù)器端的證書鏈,注意此處返回的證書鏈順序是從葉節(jié)點到根節(jié)點
            // 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]) {
                // 如果我們的證書中累盗,有一個和它證書鏈中的證書匹配的寒矿,就返回 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ù)點

(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 宏中使用了一個 do...while(0) 的循環(huán)語句若债,從邏輯上看這個 do-while 語句完全可以不需要劫窒,但是實際上是不能去掉的,原因是為了防止在某種情況下使用該宏時出現(xiàn)語法錯誤拆座。比如主巍,在下面這種情況下,如果沒有 do...while(0) 就在編譯時報錯:

if (xxx) 
    __Require_Quiet();
else 
    NSLog("This is else");

參考:宏定義的黑魔法 - 宏菜鳥起飛手冊

(2) Core Foundation 和 Security 框架的 API 的使用

延伸閱讀:

七搞旭、AFNetworkReachabilityManager

暫時還沒看

八、UIKit 擴(kuò)展

暫時還沒看

九菇绵、AFNetworking 2.x

十肄渗、AFNetworking 的價值

1. 請求調(diào)度:NSURLConnection + NSOperation

在 NSURLConnection 時代,AFNetworking 1.x 的最核心的作用在于多線程下的請求調(diào)度——將 NSURLConnection 和 NSOperation 結(jié)合咬最,AFURLConnectionOperation 作為 NSOperation 的子類翎嫡,遵循 NSURLConnectionDelegate 的方法,可以從頭到尾監(jiān)聽請求的狀態(tài)永乌,并儲存請求、響應(yīng)、響應(yīng)數(shù)據(jù)等中間狀態(tài)燥狰。

2. 更高層次的抽象

顯然,在 NSURLSession 出現(xiàn)之后人芽,AFNetworking 的意義似乎不如以前那么重要了。實際上绩脆,雖然它們有一些重疊萤厅,AFNetworking 還是可以提供更高層次的抽象。

AFNetworking 幫我們完成了很多繁瑣的工作靴迫,這使得我們在業(yè)務(wù)層的網(wǎng)絡(luò)請求變得非常輕松:

  • 請求參數(shù)和返回數(shù)據(jù)的序列化祈坠,支持多種不同格式的數(shù)據(jù)解析
  • multipart 請求拼接數(shù)據(jù)
  • 驗證 HTTPS 請求的證書
  • 請求成功和失敗的回調(diào)處理,下載矢劲、上傳進(jìn)度的回調(diào)處理

3. block

AFNetworking 將 NSURLSession 散亂的代理回調(diào)方法都轉(zhuǎn)成了 block 形式的 API赦拘,除此之外,還提供了一些用于自定義配置的 block芬沉,比如發(fā)起 multipart 請求時躺同,提供 constructingBody 的 block 接口來拼接數(shù)據(jù)。

4. 模塊化

AFNetworking 在架構(gòu)上采用了模塊化的設(shè)計丸逸,各模塊的職責(zé)是明確的蹋艺、功能是獨立的,我們可以根據(jù)自己的需要黄刚,選擇合適的模塊組合使用:

  • 創(chuà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 為什么要對 NSURLConnection/NSURLSession 進(jìn)行封裝涛救?它是如何封裝的?

2.AFNetworking 框架的設(shè)計思路和原理是什么业扒?

3.AFNetworking 和 MKNetworkKit 以及 ASIHttpRequest 有什么不同?

4.AFNetworking 2.x 和 AFNetworking 3.x 的區(qū)別是什么检吆?

十二、收獲

  • 開源項目程储、專業(yè)素養(yǎng)蹭沛、規(guī)范
  • 完善的注釋、文檔
  • 忽略一些特定的clang的編譯警告
  • nullable
  • 規(guī)范章鲤,通過斷言檢測參數(shù)的合法性
  • 邏輯嚴(yán)謹(jǐn)摊灭、完善,擴(kuò)展性好败徊,比如針對用戶可能需要的各種自定義處理提供了 block 回調(diào)帚呼,基于協(xié)議的 serialization 設(shè)計
  • 萬物皆對象,比如請求 url 參數(shù)的解析時集嵌,使用了 AFQueryStringPair 對象來表征一個 Query 參數(shù)萝挤;還有 NSProgress 的使用
  • 面向協(xié)議編程御毅,提高程序的可擴(kuò)展性
  • 多線程編程時根欧,腦海中要有清晰的線程調(diào)度圖
  • Unit Test怜珍,看到 GitHub 上有個 pr 的討論中多次提到了 Unit Test,原來 Unit Test 對于保證修改后的代碼功能有很大用處凤粗,另外就是酥泛,有些使用的示例也可以從 test case 中找到

延伸閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末异逐,一起剝皮案震驚了整個濱河市捶索,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌灰瞻,老刑警劉巖腥例,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異酝润,居然都是意外死亡燎竖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門要销,熙熙樓的掌柜王于貴愁眉苦臉地迎上來构回,“玉大人,你說我怎么就攤上這事疏咐∠说В” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵浑塞,是天一觀的道長。 經(jīng)常有香客問我缩举,道長垦梆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任仅孩,我火速辦了婚禮托猩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘辽慕。我一直安慰自己京腥,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布溅蛉。 她就那樣靜靜地躺著公浪,像睡著了一般他宛。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上欠气,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天厅各,我揣著相機(jī)與錄音,去河邊找鬼预柒。 笑死队塘,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宜鸯。 我是一名探鬼主播憔古,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淋袖!你這毒婦竟也來了鸿市?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤即碗,失蹤者是張志新(化名)和其女友劉穎焰情,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拜姿,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡烙样,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蕊肥。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谒获。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖壁却,靈堂內(nèi)的尸體忽然破棺而出批狱,到底是詐尸還是另有隱情,我是刑警寧澤展东,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布赔硫,位于F島的核電站,受9級特大地震影響盐肃,放射性物質(zhì)發(fā)生泄漏爪膊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一砸王、第九天 我趴在偏房一處隱蔽的房頂上張望推盛。 院中可真熱鬧,春花似錦谦铃、人聲如沸耘成。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽瘪菌。三九已至撒会,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間师妙,已是汗流浹背诵肛。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留疆栏,地道東北人曾掂。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓惫谤,卻偏偏與公主長得像壁顶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子溜歪,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容