最近看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
|
static dispatch_queue_t
url_request_operation_completion_queue() { static dispatch_queue_t
af_url_request_operation_completion_queue; static dispatch_once_t
onceToken; dispatch_once(&onceToken,
^{ af_url_request_operation_completion_queue
= dispatch_queue_create( "com.alamofire.networking.operation.queue" ,
DISPATCH_QUEUE_CONCURRENT ); }); return af_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
= [[ NSURLConnection alloc]
initWithRequest:request delegate: self startImmediately: NO ]; [connection
scheduleInRunLoop:[ NSRunLoop currentRunLoop]
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的一個線程。
相關代碼:-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+,所以沒問題。
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, ^{ [ self performBlock:^(){ block(); }]; //未執行到block()
group任務就已經完成了 }); |
這時需要這樣寫:
1
2
3
4
5
|
dispatch_group_enter(group); [ self performBlock:^(){ 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
= [ NSKeyedArchiver archivedDataWithRootObject:operation]; AFHTTPRequestOperation
*operationFromDB = [ NSKeyedUnarchiver unarchiveObjectWithData: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" :
[ NSSet setWithObjects: @"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" }; [manager
POST: @"http://example.com/resources.json" parameters:parameters
constructingBodyWithBlock:^( id formData)
{ [formData
appendPartWithFileURL:filePath name: @"image" error: nil ]; }
success:^(AFHTTPRequestOperation *operation, id responseObject)
{ 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,關係大致如下圖:
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: nil success:^(AFHTTPRequestOperation
*operation, id responseObject)
{ }
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{ }]; |
AFSecurityPolicy分三種驗證模式:
AFSSLPinningModeNone
這個模式表示不做SSL pinning,只跟瀏覽器一樣在系統的信任機構列表裏驗證服務端返回的證書。若證書是信任機構簽發的就會通過,若是自己服務器生成的證書,這裏是不會通過的。
AFSSLPinningModeCertificate
這個模式表示用證書綁定方式驗證證書,需要客戶端保存有服務端的證書拷貝,這裏驗證分兩步,第一步驗證證書的域名/有效期等信息,第二步是對比服務端返回的證書跟客戶端返回的是否一致。
這裏還沒弄明白第一步的驗證是怎麼進行的,代碼上跟去系統信任機構列表裏驗證一樣調用了SecTrustEvaluate,只是這裏的列表換成了客戶端保存的那些證書列表。若要驗證這個,是否應該把服務端證書的頒發機構根證書也放到客戶端裏?
AFSSLPinningModePublicKey
這個模式同樣是用證書綁定方式驗證,客戶端要有服務端的證書拷貝,只是驗證時只驗證證書裏的公鑰,不驗證證書的有效期等信息。只要公鑰是正確的,就能保證通信不會被竊聽,因爲中間人沒有私鑰,無法解開通過公鑰加密的數據。
整個AFSecurityPolicy就是實現這這幾種驗證方式,剩下的就是實現細節了,詳見源碼。
源碼註釋
sdfs