AFNetworking-源碼解析

 最近看AFNetworking2的源碼,學習這個知名網絡框架的實現,順便梳理寫下文章。AFNetworking2的大體架構和思路在這篇文章已經說得挺清楚了,就不再贅述了,只說說實現的細節。AFNetworking的代碼還在不斷更新中,我看的是AFNetworking2.3.1

本篇先看看AFURLConnectionOperation,AFURLConnectionOperation繼承自NSOperation,是一個封裝好的任務單元,在這裏構建了NSURLConnection,作爲NSURLConnection的delegate處理請求回調,做好狀態切換,線程管理,可以說是AFNetworking最核心的類,下面分幾部分說下看源碼時注意的點,最後放上代碼的註釋。

0.Tricks

AFNetworking代碼中有一些常用技巧,先說明一下。

A.clang warning

1
2
3
4
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//code
#pragma clang diagnostic pop

表示在這個區間裏忽略一些特定的clang的編譯警告,因爲AFNetworking作爲一個庫被其他項目引用,所以不能全局忽略clang的一些警告,只能在有需要的時候局部這樣做,作者喜歡用?:符號,所以經常見忽略-Wgnu警告的寫法,詳見這裏

B.dispatch_once

爲保證線程安全,所有單例都用dispatch_once生成,保證只執行一次,這也是iOS開發常用的技巧。例如:

1
2
3
4
5
6
7
8
staticdispatch_queue_t url_request_operation_completion_queue() {
    staticdispatch_queue_t af_url_request_operation_completion_queue;
    staticdispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        af_url_request_operation_completion_queue = dispatch_queue_create("com.alamofire.networking.operation.queue",   DISPATCH_QUEUE_CONCURRENT );
    });
    returnaf_url_request_operation_completion_queue;
}

C.weak & strong self

常看到一個block要使用self,會處理成在外部聲明一個weak變量指向self,在block裏又聲明一個strong變量指向weakSelf:

1
2
3
4
__weak __typeof(self)weakSelf = self;
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
    __strong __typeof(weakSelf)strongSelf = weakSelf;
}];

weakSelf是爲了block不持有self,避免循環引用,而再聲明一個strongSelf是因爲一旦進入block執行,就不允許self在這個執行過程中釋放。block執行完後這個strongSelf會自動釋放,沒有循環引用問題。

1.線程

先來看看NSURLConnection發送請求時的線程情況,NSURLConnection是被設計成異步發送的,調用了start方法後,NSURLConnection會新建一些線程用底層的CFSocket去發送和接收請求,在發送和接收的一些事件發生後通知原來線程的Runloop去回調事件。

NSURLConnection的同步方法sendSynchronousRequest方法也是基於異步的,同樣要在其他線程去處理請求的發送和接收,只是同步方法會手動block住線程,發送狀態的通知也不是通過RunLoop進行。

使用NSURLConnection有幾種選擇:

A.在主線程調異步接口

若直接在主線程調用異步接口,會有個Runloop相關的問題:

當在主線程調用[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]時,請求發出,偵聽任務會加入到主線程的Runloop下,RunloopMode會默認爲NSDefaultRunLoopMode。這表明只有當前線程的Runloop處於NSDefaultRunLoopMode時,這個任務纔會被執行。但當用戶滾動tableview或scrollview時,主線程的Runloop是處於NSEventTrackingRunLoopMode模式下的,不會執行NSDefaultRunLoopMode的任務,所以會出現一個問題,請求發出後,如果用戶一直在操作UI上下滑動屏幕,那在滑動結束前是不會執行回調函數的,只有在滑動結束,RunloopMode切回NSDefaultRunLoopMode,纔會執行回調函數。蘋果一直把動畫效果性能放在第一位,估計這也是蘋果提升UI動畫性能的手段之一。

所以若要在主線程使用NSURLConnection異步接口,需要手動把RunloopMode設爲NSRunLoopCommonModes。這個mode意思是無論當前Runloop處於什麼狀態,都執行這個任務。

1
2
3
NSURLConnection*connection = [[NSURLConnectionalloc] initWithRequest:request delegate:selfstartImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoopcurrentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];

B.在子線程調同步接口

若在子線程調用同步接口,一條線程只能處理一個請求,因爲請求一發出去線程就阻塞住等待回調,需要給每個請求新建一個線程,這是很浪費的,這種方式唯一的好處應該是易於控制請求併發的數量。

C.在子線程調異步接口

子線程調用異步接口,子線程需要有Runloop去接收異步回調事件,這裏也可以每個請求都新建一條帶有Runloop的線程去偵聽回調,但這一點好處都沒有,既然是異步回調,除了處理回調內容,其他時間線程都是空閒可利用的,所有請求共用一個響應的線程就夠了。

AFNetworking用的就是第三種方式,創建了一條常駐線程專門處理所有請求的回調事件,這個模型跟nodejs有點類似。網絡請求回調處理完,組裝好數據後再給上層調用者回調,這時候回調是拋回主線程的,因爲主線程是最安全的,使用者可能會在回調中更新UI,在子線程更新UI會導致各種問題,一般使用者也可以不需要關心線程問題。

以下是相關線程大致的關係,實際上多個NSURLConnection會共用一個NSURLConnectionLoader線程,這裏就不細化了,除了處理socket的CFSocket線程,還有一些Javascript:Core的線程,目前不清楚作用,歸爲NSURLConnection裏的其他線程。因爲NSURLConnection是系統控件,每個iOS版本可能都有不一樣,可以先把NSURLConnection當成一個黑盒,只管它的start和callback就行了。如果使用AFHttpRequestOperationManager的接口發送請求,這些請求會統一在一個NSOperationQueue裏去發,所以多了上面NSOperationQueue的一個線程。

afnetroking (2)

相關代碼:-networkRequestThread:, -start:, -operationDidStart:。

2.狀態機

繼承NSOperation有個很麻煩的東西要處理,就是改變狀態時需要發KVO通知,否則這個類加入NSOperationQueue不可用了。NSOperationQueue是用KVO方式偵聽NSOperation狀態的改變,以判斷這個任務當前是否已完成,完成的任務需要在隊列中除去並釋放。

AFURLConnectionOperation對此做了個狀態機,統一搞定狀態切換以及發KVO通知的問題,內部要改變狀態時,就只需要類似self.state = AFOperationReadyState的調用而不需要做其他了,狀態改變的KVO通知在setState裏發出。

總的來說狀態管理相關代碼就三部分,一是限制一個狀態可以切換到其他哪些狀態,避免狀態切換混亂,二是狀態Enum值與NSOperation四個狀態方法的對應,三是在setState時統一發KVO通知。詳見代碼註釋。

相關代碼:AFKeyPathFromOperationState, AFStateTransitionIsValid, -setState:, -isPaused:, -isReady:, -isExecuting:, -isFinished:.

3.NSURLConnectionDelegate

處理NSURLConnection Delegate的內容不多,代碼也是按請求回調的順序排列下去,十分易讀,主要流程就是接收到響應的時候打開outputStream,接着有數據過來就往outputStream寫,在上傳/接收數據過程中會回調上層傳進來的相應的callback,在請求完成回調到connectionDidFinishLoading時,關閉outputStream,用outputStream組裝responseData作爲接收到的數據,把NSOperation狀態設爲finished,表示任務完成,NSOperation會自動調用completeBlock,再回調到上層。

4.setCompleteBlock

NSOperation在iOS4.0以後提供了個接口setCompletionBlock,可以傳入一個block作爲任務執行完成時(state狀態機變爲finished時)的回調,AFNetworking直接用了這個接口,並通過重寫加了幾個功能:

A.消除循環引用

在NSOperation的實現裏,completionBlock是NSOperation對象的一個成員,NSOperation對象持有着completionBlock,若傳進來的block用到了NSOperation對象,或者block用到的對象持有了這個NSOperation對象,就會造成循環引用。這裏執行完block後調用[strongSelf setCompletionBlock:nil]把completionBlock設成nil,手動釋放self(NSOperation對象)持有的completionBlock對象,打破循環引用。

可以理解成對外保證傳進來的block一定會被釋放,解決外部使用使很容易出現的因對象關係複雜導致循環引用的問題,讓使用者不知道循環引用這個概念都能正確使用。

B.dispatch_group

這裏允許用戶讓所有operation的completionBlock在一個group裏執行,但我沒看出這樣做的作用,若想組裝一組請求(見下面的batchOfRequestOperations)也不需要再讓completionBlock在group裏執行,求解。

C.”The Deallocation Problem”

作者在註釋裏說這裏重寫的setCompletionBlock方法解決了”The Deallocation Problem”,實際上並沒有。”The Deallocation Problem”簡單來說就是不要讓UIKit的東西在子線程釋放。

這裏如果傳進來的block持有了外部的UIViewController或其他UIKit對象(下面暫時稱爲A對象),並且在請求完成之前其他所有對這個A對象的引用都已經釋放了,那麼這個completionBlock就是最後一個持有這個A對象的,這個block釋放時A對象也會釋放。這個block在什麼線程釋放,A對象就會在什麼線程釋放。我們看到block釋放的地方是url_request_operation_completion_queue(),這是AFNetworking特意生成的子線程,所以按理說A對象是會在子線程釋放的,會導致UIKit對象在子線程釋放,會有問題。

但AFNetworking實際用起來卻沒問題,想了很久不得其解,後來做了實驗,發現iOS5以後蘋果對UIKit對象的釋放做了特殊處理,只要發現在子線程釋放這些對象,就自動轉到主線程去釋放,斷點出來是由一個叫_objc_deallocOnMainThreadHelper的方法做的。如果不是UIKit對象就不會跳到主線程釋放。AFNetworking2.0只支持iOS6+,所以沒問題。

blockTest

5.batchOfRequestOperations

這裏額外提供了一個便捷接口,可以傳入一組請求,在所有請求完成後回調complionBlock,在每一個請求完成時回調progressBlock通知外面有多少個請求已完成。詳情參見代碼註釋,這裏需要說明下dispatch_group_enter和dispatch_group_leave的使用,這兩個方法用於把一個異步任務加入group裏。

一般我們要把一個任務加入一個group裏是這樣:

1
2
3
dispatch_group_async(group, queue, ^{
    block();
});

這個寫法等價於

1
2
3
4
5
dispatch_async(queue, ^{
    dispatch_group_enter(group);
    block()
    dispatch_group_leave(group);
});

如果要把一個異步任務加入group,這樣就行不通了:

1
2
3
4
5
6
dispatch_group_async(group, queue, ^{
    [selfperformBlock:^(){
        block();
    }];
    //未執行到block() group任務就已經完成了
});

這時需要這樣寫:

1
2
3
4
5
dispatch_group_enter(group);
[selfperformBlock:^(){
    block();
    dispatch_group_leave(group);
}];

異步任務回調後纔算這個group任務完成。對batchOfRequest的實現來說就是請求完成並回調後,纔算這個任務完成。

其實這跟retain/release差不多,都是計數,dispatch_group_enter時任務數+1,dispatch_group_leave時任務數-1,任務數爲0時執行dispatch_group_notify的內容。

相關代碼:-batchOfRequestOperations:progressBlock:completionBlock:

6.其他

A.鎖

AFURLConnectionOperation有一把遞歸鎖,在所有會訪問/修改成員變量的對外接口都加了鎖,因爲這些對外的接口用戶是可以在任意線程調用的,對於訪問和修改成員變量的接口,必須用鎖保證線程安全。

B.序列化

AFNetworking的多數類都支持序列化,但實現的是NSSecureCoding的接口,而不是NSCoding,區別在於解數據時要指定Class,用-decodeObjectOfClass:forKey:方法代替了-decodeObjectForKey:。這樣做更安全,因爲序列化後的數據有可能被篡改,若不指定Class,-decode出來的對象可能不是原來的對象,有潛在風險。另外,NSSecureCoding是iOS6以上纔有的。詳見這裏

這裏在序列化時保存了當前任務狀態,接收的數據等,但回調block是保存不了的,需要在取出來發送時重新設置。可以像下面這樣持久化保存和取出任務:

1
2
3
4
5
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
NSData*data = [NSKeyedArchiverarchivedDataWithRootObject:operation];
 
AFHTTPRequestOperation *operationFromDB = [NSKeyedUnarchiverunarchiveObjectWithData:data];
[operationFromDB start];

C.backgroundTask

這裏提供了setShouldExecuteAsBackgroundTaskWithExpirationHandler接口,決定APP進入後臺後是否繼續發送接收請求,並在後臺執行時間超時後取消所有請求。在dealloc裏需要調用[application endBackgroundTask:],告訴系統這個後臺任務已經完成,不然系統會一直讓你的APP運行在後臺,直到超時。

相關代碼:-setShouldExecuteAsBackgroundTaskWithExpirationHandler:, -dealloc:

7.AFHTTPRequestOperation

AFHTTPRequestOperation繼承了AFURLConnectionOperation,把它放一起說是因爲它沒做多少事情,主要多了responseSerializer,暫停下載斷點續傳,以及提供接口請求成功失敗的回調接口-setCompletionBlockWithSuccess:failure:。詳見源碼註釋。

8.源碼註釋

——————————————————————————————————————————————————————————

 AFURLRequestSerialization用於幫助構建NSURLRequest,主要做了兩個事情:
1.構建普通請求:格式化請求參數,生成HTTP Header。
2.構建multipart請求。
分別看看它在這兩點具體做了什麼,怎麼做的。

1.構建普通請求

A.格式化請求參數

一般我們請求都會按key=value的方式帶上各種參數,GET方法參數直接加在URL上,POST方法放在body上,NSURLRequest沒有封裝好這個參數的解析,只能我們自己拼好字符串。AFNetworking提供了接口,讓參數可以是NSDictionary, NSArray, NSSet這些類型,再由內部解析成字符串後賦給NSURLRequest。

轉化過程大致是這樣的:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
@{
     @"name":@"bang",
     @"phone": @{@"mobile":@"xx",@"home":@"xx"},
     @"families": @[@"father",@"mother"],
     @"nums": [NSSetsetWithObjects:@"1",@"2",nil]
}
->
@[
     field:@"name", value: @"bang",
     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=bang&phone[mobile]=xx&phone[home]=xx&families[]=father&families[]=mother&nums=1&num=2

第一部分是用戶傳進來的數據,支持包含NSArray,NSDictionary,NSSet這三種數據結構。
第二部分是轉換成AFNetworking內自己的數據結構,每一個key-value對都用一個對象AFQueryStringPair表示,作用是最後可以根據不同的字符串編碼生成各自的key=value字符串。主要函數是AFQueryStringPairsFromKeyAndValue,詳見源碼註釋。
第三部分是最後生成NSURLRequest可用的字符串數據,並且對參數進行url編碼,在AFQueryStringFromParametersWithEncoding這個函數裏。

最後在把數據賦給NSURLRequest時根據不同的HTTP方法分別處理,對於GET/HEAD/DELETE方法,把參數加到URL後面,對於其他如POST/PUT方法,把數據加到body上,並設好HTTP頭,告訴服務端字符串的編碼。

B.HTTP Header

AFNetworking幫你組裝好了一些HTTP請求頭,包括語言Accept-Language,根據[NSLocale preferredLanguages]方法讀取本地語言,告訴服務端自己能接受的語言。還有構建User-Agent,以及提供Basic Auth認證接口,幫你把用戶名密碼做base64編碼後放入HTTP請求頭。詳見源碼註釋。

C.其他格式化方式

HTTP請求參數不一定是要key=value形式,可以是任何形式的數據,可以是json格式,蘋果的plist格式,二進制protobuf格式等,AFNetworking提供了方法可以很容易擴展支持這些格式,默認就實現了json和plist格式。詳見源碼的類AFJSONRequestSerializer和AFPropertyListRequestSerializer。

2.構建multipart請求

構建Multipart請求是佔篇幅很大的一個功能,AFURLRequestSerialization裏2/3的代碼都是在做這個事。

A.Multipart協議介紹

Multipart是HTTP協議爲web表單新增的上傳文件的協議,協議文檔是rfc1867,它基於HTTP的POST方法,數據同樣是放在body上,跟普通POST方法的區別是數據不是key=value形式,key=value形式難以表示文件實體,爲此Multipart協議添加了分隔符,有自己的格式結構,大致如下:

—AaB03x
content-disposition: form-data; name=“name"

bang
–AaB03x
content-disposition: form-data; name=”pic”; filename=“content.txt”
Content-Type: text/plain

… contents of bang.txt …
–AaB03x–

以上表示數據name=bang以及一個文件,content.txt是文件名,… contents of bang.txt …是文件實體內容。分隔符—AaB03x是可以自定義的,寫在HTTP頭部裏:

Content-type: multipart/form-data, boundary=AaB03x

每一個部分都有自己的頭部,表明這部分的數據類型以及其他一些參數,例如文件名,普通字段的key。最後一個分隔符會多加兩橫,表示數據已經結束:—AaB03x—。

B.實現

接下來說說怎樣構造Multipart裏的數據,最簡單的方式就是直接拼數據,要發送一個文件,就直接把文件所有內容讀取出來,再按上述協議加上頭部和分隔符,拼接好數據後扔給NSURLRequest的body就可以發送了,很簡單。但這樣做是不可用的,因爲文件可能很大,這樣拼數據把整個文件讀進內存,很可能把內存撐爆了。

第二種方法是不把文件讀出來,不在內存拼,而是新建一個臨時文件,在這個文件上拼接數據,再把文件地址扔給NSURLRequest的bodyStream,這樣上傳的時候是分片讀取這個文件,不會撐爆內存,但這樣每次上傳都需要新建個臨時文件,對這個臨時文件的管理也挺麻煩的。

第三種方法是構建自己的數據結構,只保存要上傳的文件地址,邊上傳邊拼數據,上傳是分片的,拼數據也是分片的,拼到文件實體部分時直接從原來的文件分片讀取。這方法沒上述兩種的問題,只是實現起來也沒上述兩種簡單,AFNetworking就是實現這第三種方法,而且還更進一步,除了文件,還可以添加多個其他不同類型的數據,包括NSData,和InputStream。

AFNetworking裏multipart請求的使用方式是這樣:

01
02
03
04
05
06
07
08
09
10
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
NSDictionary*parameters = @{@"foo":@"bar"};
NSURL*filePath = [NSURLfileURLWithPath:@"file://path/to/image.png"];
[manager POST:@"http://example.com/resources.json"parameters:parameters constructingBodyWithBlock:^(idformData) {
    [formData appendPartWithFileURL:filePath name:@"image"error:nil];
} success:^(AFHTTPRequestOperation *operation, idresponseObject) {
    NSLog(@"Success: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError*error) {
    NSLog(@"Error: %@", error);
}];

這裏通過constructingBodyWithBlock向使用者提供了一個AFStreamingMultipartFormData對象,調這個對象的幾種append方法就可以添加不同類型的數據,包括FileURL/NSData/NSInputStream,AFStreamingMultipartFormData內部把這些append的數據轉成不同類型的AFHTTPBodyPart,添加到自定義的AFMultipartBodyStream裏。最後把AFMultipartBodyStream賦給原來NSMutableURLRequest的bodyStream。NSURLConnection發送請求時會讀取這個bodyStream,在讀取數據時會調用這個bodyStream的-read:maxLength:方法,AFMultipartBodyStream重寫了這個方法,不斷讀取之前append進來的AFHTTPBodyPart數據直到讀完。

AFHTTPBodyPart封裝了各部分數據的組裝和讀取,一個AFHTTPBodyPart就是一個數據塊。實際上三種類型(FileURL/NSData/NSInputStream)的數據在AFHTTPBodyPart都轉成NSInputStream,讀取數據時只需讀這個inputStream。inputStream只保存了數據的實體,沒有包括分隔符和頭部,AFHTTPBodyPart是邊讀取變拼接數據,用一個狀態機確定現在數據讀取到哪一部份,以及保存這個狀態下已被讀取的字節數,以此定位要讀的數據位置,詳見AFHTTPBodyPart的-read:maxLength:方法。

AFMultipartBodyStream封裝了整個multipart數據的讀取,主要是根據讀取的位置確定現在要讀哪一個AFHTTPBodyPart。AFStreamingMultipartFormData對外提供友好的append接口,並把構造好的AFMultipartBodyStream賦回給NSMutableURLRequest,關係大致如下圖:

AFURLRequestSerialization

C.NSInputStream子類

NSURLRequest的setHTTPBodyStream接受的是一個NSInputStream*參數,那我們要自定義inputStream的話,創建一個NSInputStream的子類傳給它是不是就可以了?實際上不行,這樣做後用NSURLRequest發出請求會導致crash,提示[xx _scheduleInCFRunLoop:forMode:]: unrecognized selector。

這是因爲NSURLRequest實際上接受的不是NSInputStream對象,而是CoreFoundation的CFReadStreamRef對象,因爲CFReadStreamRef和NSInputStream是toll-free bridged,可以自由轉換,但CFReadStreamRef會用到CFStreamScheduleWithRunLoop這個方法,當它調用到這個方法時,object-c的toll-free bridging機制會調用object-c對象NSInputStream的相應函數,這裏就調用到了_scheduleInCFRunLoop:forMode:,若不實現這個方法就會crash。詳見這篇文章

3.源碼註釋

——————————————————————————————————————————————————————————

 本篇說說安全相關的AFSecurityPolicy模塊,AFSecurityPolicy用於驗證HTTPS請求的證書,先來看看HTTPS的原理和證書相關的幾個問題。

HTTPS

HTTPS連接建立過程大致是,客戶端和服務端建立一個連接,服務端返回一個證書,客戶端裏存有各個受信任的證書機構根證書,用這些根證書對服務端返回的證書進行驗證,經驗證如果證書是可信任的,就生成一個pre-master secret,用這個證書的公鑰加密後發送給服務端,服務端用私鑰解密後得到pre-master secret,再根據某種算法生成master secret,客戶端也同樣根據這種算法從pre-master secret生成master secret,隨後雙方的通信都用這個master secret對傳輸數據進行加密解密。

以上是簡單過程,中間還有很多細節,詳細過程和原理已經有很多文章闡述得很好,就不再複述,推薦一些相關文章:
關於非對稱加密算法的原理:RSA算法原理<一> <二>
關於整個流程:HTTPS那些事<一> <二> <三>
關於數字證書:淺析數字證書

這裏說下一開始我比較費解的兩個問題:

1.證書是怎樣驗證的?怎樣保證中間人不能僞造證書?

首先要知道非對稱加密算法的特點,非對稱加密有一對公鑰私鑰,用公鑰加密的數據只能通過對應的私鑰解密,用私鑰加密的數據只能通過對應的公鑰解密。

我們來看最簡單的情況:一個證書頒發機構(CA),頒發了一個證書A,服務器用這個證書建立https連接。客戶端在信任列表裏有這個CA機構的根證書。

首先CA機構頒發的證書A裏包含有證書內容F,以及證書加密內容F1,加密內容F1就是用這個證書機構的私鑰對內容F加密的結果。(這中間還有一次hash算法,略過。)

建立https連接時,服務端返回證書A給客戶端,客戶端的系統裏的CA機構根證書有這個CA機構的公鑰,用這個公鑰對證書A的加密內容F1解密得到F2,跟證書A裏內容F對比,若相等就通過驗證。整個流程大致是:F->CA私鑰加密->F1->客戶端CA公鑰解密->F。因爲中間人不會有CA機構的私鑰,客戶端無法通過CA公鑰解密,所以僞造的證書肯定無法通過驗證。

2.什麼是SSL Pinning?

可以理解爲證書綁定,是指客戶端直接保存服務端的證書,建立https連接時直接對比服務端返回的和客戶端保存的兩個證書是否一樣,一樣就表明證書是真的,不再去系統的信任證書機構裏尋找驗證。這適用於非瀏覽器應用,因爲瀏覽器跟很多未知服務端打交道,無法把每個服務端的證書都保存到本地,但CS架構的像手機APP事先已經知道要進行通信的服務端,可以直接在客戶端保存這個服務端的證書用於校驗。

爲什麼直接對比就能保證證書沒問題?如果中間人從客戶端取出證書,再僞裝成服務端跟其他客戶端通信,它發送給客戶端的這個證書不就能通過驗證嗎?確實可以通過驗證,但後續的流程走不下去,因爲下一步客戶端會用證書裏的公鑰加密,中間人沒有這個證書的私鑰就解不出內容,也就截獲不到數據,這個證書的私鑰只有真正的服務端有,中間人僞造證書主要僞造的是公鑰。

爲什麼要用SSL Pinning?正常的驗證方式不夠嗎?如果服務端的證書是從受信任的的CA機構頒發的,驗證是沒問題的,但CA機構頒發證書比較昂貴,小企業或個人用戶可能會選擇自己頒發證書,這樣就無法通過系統受信任的CA機構列表驗證這個證書的真僞了,所以需要SSL Pinning這樣的方式去驗證。

AFSecurityPolicy

NSURLConnection已經封裝了https連接的建立、數據的加密解密功能,我們直接使用NSURLConnection是可以訪問https網站的,但NSURLConnection並沒有驗證證書是否合法,無法避免中間人攻擊。要做到真正安全通訊,需要我們手動去驗證服務端返回的證書,AFSecurityPolicy封裝了證書驗證的過程,讓用戶可以輕易使用,除了去系統信任CA機構列表驗證,還支持SSL Pinning方式的驗證。使用方法:

1
2
3
4
5
6
7
//把服務端證書(需要轉換成cer格式)放到APP項目資源裏,AFSecurityPolicy會自動尋找根目錄下所有cer文件
AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
securityPolicy.allowInvalidCertificates = YES;
[AFHTTPRequestOperationManager manager].securityPolicy = securityPolicy;
[manager GET:@"https://example.com/"parameters:nilsuccess:^(AFHTTPRequestOperation *operation, idresponseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError*error) {
}];

AFSecurityPolicy分三種驗證模式:

AFSSLPinningModeNone

這個模式表示不做SSL pinning,只跟瀏覽器一樣在系統的信任機構列表裏驗證服務端返回的證書。若證書是信任機構簽發的就會通過,若是自己服務器生成的證書,這裏是不會通過的。

AFSSLPinningModeCertificate

這個模式表示用證書綁定方式驗證證書,需要客戶端保存有服務端的證書拷貝,這裏驗證分兩步,第一步驗證證書的域名/有效期等信息,第二步是對比服務端返回的證書跟客戶端返回的是否一致。

這裏還沒弄明白第一步的驗證是怎麼進行的,代碼上跟去系統信任機構列表裏驗證一樣調用了SecTrustEvaluate,只是這裏的列表換成了客戶端保存的那些證書列表。若要驗證這個,是否應該把服務端證書的頒發機構根證書也放到客戶端裏?

AFSSLPinningModePublicKey

這個模式同樣是用證書綁定方式驗證,客戶端要有服務端的證書拷貝,只是驗證時只驗證證書裏的公鑰,不驗證證書的有效期等信息。只要公鑰是正確的,就能保證通信不會被竊聽,因爲中間人沒有私鑰,無法解開通過公鑰加密的數據。

整個AFSecurityPolicy就是實現這這幾種驗證方式,剩下的就是實現細節了,詳見源碼。

源碼註釋


sdfs


發佈了42 篇原創文章 · 獲贊 9 · 訪問量 28萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章