代碼示例:https://github.com/johnlui/Swift-On-iOS/blob/master/BuildYourHTTPRequestLibrary
開源項目:Pitaya,適合大文件上傳的 HTTP 請求庫:https://github.com/johnlui/Pitaya
這個系列的文章本已終結,現在續上,就是為了一個未來大家一定會越來越需要的功能:設置 SSL 證書鋼釘。
說起來這個功能也很簡單,在我們調用 HTTPS 協議的時候,事先把 SSL 證書存到 App 本地,然後在每次請求的時候都進行一次驗證,避免中間人攻擊(Man-in-the-middle attack)。同時,這個功能也是我們使用自簽名證書時候必須的,因為系統默認會拒絕我們自己簽名的不受信任的證書,導致連接失敗。
廢話不多說,我們進入正題。
證書獲取
NSURLSession 支持 cer 格式的證書文件,而 Apache 和 Nginx 默認的證書都是 crt 格式,我們需要雙擊將其安裝到系統中,再使用鑰匙串 App 將這個證書導出為 cer 格式即可。
開搞
經過查詢資料,發現 NSURLSession 提供了 SSL 證書處理的代理方法,我們需要對我們的 NetworkManager 類進行一點點改造。
自定義 session
如果想要調用到我們想要的代理方法,需要我們自定義一下 NSURLSession 對象:
var session: NSURLSession! ... ... init(... ...) { ... ... super.init() self.session = NSURLSession(configuration: NSURLSession.sharedSession().configuration, delegate: self, delegateQueue: NSURLSession.sharedSession().delegateQueue) }
實現代理
由於上面我們把 NSURLSession 的代理設置成了 self,所以現在我們要讓 NetworkManager 類實現 NSURLSessionDelegate 這個 protocol。又由於 NSURLSessionDelegate 繼承自 NSObjectProtocol,所以我們需要讓 NetworkManager 繼承自 NSObject 類:
class NetworkManager: NSObject, NSURLSessionDelegate { ... ...
實現代理方法
接下來我們就通過實現 SSL 證書檢查的代理方法來干預網絡請求了。
增加兩個成員變量:
var localCertData: NSData! var sSLValidateErrorCallBack: (() -> Void)?
增加設置他們的函數:
func addSSLPinning(LocalCertData data: NSData, SSLValidateErrorCallBack: (()->Void)? = nil) { self.localCertData = data self.sSLValidateErrorCallBack = SSLValidateErrorCallBack }
實現代理方法,介入網絡請求:
@objc func URLSession(session: NSURLSession, didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) { if let localCertificateData = self.localCertData { if let serverTrust = challenge.protectionSpace.serverTrust, certificate = SecTrustGetCertificateAtIndex(serverTrust, 0), remoteCertificateData: NSData = SecCertificateCopyData(certificate) { if localCertificateData.isEqualToData(remoteCertificateData) { let credential = NSURLCredential(forTrust: serverTrust) challenge.sender?.useCredential(credential, forAuthenticationChallenge: challenge) completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, credential) } else { challenge.sender?.cancelAuthenticationChallenge(challenge) completionHandler(NSURLSessionAuthChallengeDisposition.CancelAuthenticationChallenge, nil) self.sSLValidateErrorCallBack?() } } else { NSLog("Get RemoteCertificateData or LocalCertificateData error!") } } else { completionHandler(NSURLSessionAuthChallengeDisposition.UseCredential, nil) } }
至此,檢測 SSL 證書的功能就做完了。接下來我們檢驗成果。
檢驗成果
『Thus, programs must be written for people to read, and only incidentally for machines to execute.』
——《Structure and Interpretation of Computer Programs 》 Harold Abelson
『代碼是寫給人看的,只是恰好能運行。』這句話出自大名鼎鼎的 SICP,出處:https://mitpress.mit.edu/sicp/front/node3.html
在搞完了這個功能之後,我突然發現我好像被 Alamofire 的 API 設計給帶偏了:寫起來方便是最不重要的,便於使用者理解才是最重要的。所以我打算殺掉所有疑似假裝是奇技淫巧的集合型 API,改由純粹的 構造對象->修改對象->發起請求 模式,降低使用者的理解成本。
我使用我的網站 lvwenhan.com 的證書來進行此次驗證:
let network = NetworkManager(url: "https://lvwenhan.com/", method: "GET") { (data, response, error) -> Void in if let _ = error { NSLog(error.description) } else { print("證書正確!") } } let certData = NSData(contentsOfFile: NSBundle.mainBundle().pathForResource("lvwenhancom", ofType: "cer")!)! network.addSSLPinning(LocalCertData: certData) { () -> Void in print("SSL 證書錯誤,遭受中間人攻擊!") } network.fire() return;
得到如下結果:
接下來把網址改成 https://www.baidu.com/,運行,查看結果:
搞定!
寫在後面的話
本文中我只檢測了經過第三方簽名的受信任的 SSL 證書的檢驗結果,並沒有測試自簽名證書,希望有人測試之後把結果告訴我 :) 在文章下面評論或者上 Github 提 issue 都行~
《自己動手寫一個 iOS 網絡請求庫》系列文章可能真的結束了,感謝你的閱讀!