AFNetworking(v3.1.0)源碼解析

注:本文始發(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: 方法
  • 六、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)
  • 七、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
  • Additional Functionality
    • Security(網(wǎng)絡(luò)通信安全策略模塊)
    • Reachability(網(wǎng)絡(luò)狀態(tài)監(jiān)聽模塊)
    • UIKit(對(duì) iOS 系統(tǒng) UI 控件的擴(kuò)展)
image

<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)用方用的?

image

<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 移除。

image

<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 格式
image

<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-Typeapplication/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ù)解析

image

<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 作為解析器基類,提供了 acceptableContentTypesacceptableStatusCodes 兩個(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é)議的稱呼。

image

<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 握手過程完成。

image

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

完整的 HTTPS 連接的建立過程,包括下面三個(gè)步驟:

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

image

<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");

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

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

延伸閱讀:

七、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 中找到

延伸閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • AFNetWorkingAFNetWorking一款輕量級(jí)網(wǎng)絡(luò)請(qǐng)求開源框架,基于iOS和mac os 網(wǎng)絡(luò)進(jìn)行擴(kuò)展...
    陽明AI閱讀 33,331評(píng)論 6 100
  • AFNetworking 最新版本是3.1.0,相對(duì)于2.x版本,刪除了基于NSURLConnection的AFU...
    FlyElephant閱讀 662評(píng)論 0 3
  • AFNetworking基本每個(gè)項(xiàng)目都會(huì)用。但是看它的代碼的人不多。有一次面試,面試官問我看過AFNetworki...
    charlotte2018閱讀 976評(píng)論 1 7
  • 原文鏈接:http://www.tuicool.com/articles/eaiEba3 在我們平時(shí)的開發(fā)中,對(duì)網(wǎng)...
    BookKeeping閱讀 534評(píng)論 2 1
  • 表情是什么,我認(rèn)為表情就是表現(xiàn)出來的情緒。表情可以傳達(dá)很多信息。高興了當(dāng)然就笑了,難過就哭了。兩者是相互影響密不可...
    Persistenc_6aea閱讀 129,515評(píng)論 2 7

友情鏈接更多精彩內(nèi)容