本篇主要講解Alamofire中安全驗證代碼html
做爲開發人員,理解HTTPS的原理和應用算是一項基本技能。HTTPS目前來講是很是安全的,但仍然有大量的公司還在使用HTTP。其實HTTPS也並非很貴啊。ios
在網上能夠找到大把的介紹HTTTPS的文章,在閱讀ServerTrustPolicy.swfit
代碼前,咱們先簡單的講一下HTTPS請求的過程:web
上邊的圖片已經標出了步驟,咱們逐步的來分析:swift
https
開頭,咱們首先向服務器發送一條請求。服務器須要一個證書,這個證書能夠從某些機構得到,也能夠本身經過工具生成,經過某些合法機構生成的證書客戶端不須要進行驗證,這樣的請求不會觸發Apple的@objc(URLSession:task:didReceiveChallenge:completionHandler:)
代理方法,本身生成的證書則須要客戶端進行驗證。證書中包含公鑰和私鑰:api
非對稱加密
的知識,你們能夠在網上找到客戶端得到了服務器的加密數據,使用隨機數解密,到此,客戶端和服務器就能經過隨機數發送數據了數組
HTTPS前邊的幾回握手是須要時間開銷的,所以,不能每次鏈接都走一遍,這就是後邊使用對稱加密數據的緣由。Alamofire中主要作的是對服務器的驗證,關於自定義的安全驗證應該也是模仿了上邊的整個過程。相對於Apple來講,隱藏了發送隨機數這一過程。安全
對於服務器的驗證除了證書驗證以外必定要加上域名驗證,這樣才能更安全。服務器若要驗證客戶端則會使用簽名技術。若是假裝成客戶端來獲取服務器的數據最大的問題就是不知道某個請求的參數是什麼,這樣也就沒法獲取數據。服務器
ServerTrustPolicyManager
是對ServerTrustPolicy
的管理,咱們能夠暫時把ServerTrustPolicy
當作是一個安全策略,就是指對一個服務器採起的策略。然而在真實的開發中,一個APP可能會用到不少不一樣的主機地址(host)。所以就產生了這樣的需求,爲不一樣的host綁定一個特定的安全策略。網絡
所以ServerTrustPolicyManager
須要一個字典來存放這些有key,value對應關係的數據。咱們看下邊的代碼:session
/// Responsible for managing the mapping of `ServerTrustPolicy` objects to a given host. open class ServerTrustPolicyManager { /// The dictionary of policies mapped to a particular host. open let policies: [String: ServerTrustPolicy] /// Initializes the `ServerTrustPolicyManager` instance with the given policies. /// /// Since different servers and web services can have different leaf certificates, intermediate and even root /// certficates, it is important to have the flexibility to specify evaluation policies on a per host basis. This /// allows for scenarios such as using default evaluation for host1, certificate pinning for host2, public key /// pinning for host3 and disabling evaluation for host4. /// /// - parameter policies: A dictionary of all policies mapped to a particular host. /// /// - returns: The new `ServerTrustPolicyManager` instance. public init(policies: [String: ServerTrustPolicy]) { self.policies = policies } /// Returns the `ServerTrustPolicy` for the given host if applicable. /// /// By default, this method will return the policy that perfectly matches the given host. Subclasses could override /// this method and implement more complex mapping implementations such as wildcards. /// /// - parameter host: The host to use when searching for a matching policy. /// /// - returns: The server trust policy for the given host if found. open func serverTrustPolicy(forHost host: String) -> ServerTrustPolicy? { return policies[host] } }
出於優秀代碼的設計問題,在後續的使用中確定會有根據host讀取策略的要求,所以,在上邊的類中設計了最後一個函數。
咱們是這麼使用的:
let serverTrustPolicies: [String: ServerTrustPolicy] = [ "test.example.com": .pinCertificates( certificates: ServerTrustPolicy.certificates(), validateCertificateChain: true, validateHost: true ), "insecure.expired-apis.com": .disableEvaluation ] let sessionManager = SessionManager( serverTrustPolicyManager: ServerTrustPolicyManager(policies: serverTrustPolicies) )
在Alamofire中這個ServerTrustPolicyManager
會在SessionDelegate的收到服務器要求驗證的方法中會出現,這個會在後續的文章中給出說明。
ServerTrustPolicyManager做爲URLSession的一個屬性,經過運行時的手段來實現。
extension URLSession { private struct AssociatedKeys { static var managerKey = "URLSession.ServerTrustPolicyManager" } var serverTrustPolicyManager: ServerTrustPolicyManager? { get { return objc_getAssociatedObject(self, &AssociatedKeys.managerKey) as? ServerTrustPolicyManager } set (manager) { objc_setAssociatedObject(self, &AssociatedKeys.managerKey, manager, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } }
上邊的代碼用到了運行時,尤爲是OBJC_ASSOCIATION_RETAIN_NONATOMIC
這個選項,其中包含了強引用和若引用的問題,我想在這裏簡單的解釋一下引用問題。
咱們能夠這麼理解,不論是類仍是對象,或者是對象的屬性,咱們都稱之爲一個object。咱們把這個object比做一個鐵盒子,當有其它的對象對他強引用的時候,就像給這個鐵盒子綁了一個繩子,弱引用就像一條虛幻的激光同樣鏈接這個盒子。固然,在oc中,不少對象默認的狀況下就是strong的。
咱們能夠想象這個盒子是被繩子拉住了,才能漂浮在空中,若是沒有繩子就會掉到無底深淵,而後銷燬。這裏最重要的概念就是,只要一個對象沒有了強引用,那麼就會馬上銷燬。
咱們舉個例子:
MyViewController *myController = [[MyViewController alloc] init…];
上邊的代碼是再日常不過的一段代碼,建立了一個MyViewController實例,而後使用myController指向了這個實例,所以這個實例就有了一個繩子,他就不會馬上銷燬,若是咱們把代碼改爲這樣:
MyViewController * __weak myController = [[MyViewController alloc] init…];
把myController指向實例設置爲弱引用,那麼即便在下一行代碼打印這個myController,也會是nil。由於實例並無一個繩子讓他能不不銷燬。
所謂道理都是相通的,只要理解了這個概念就能明白引用循環的問題,須要注意的是做用域的問題,若是上邊的myController在一個函數中,那麼出了函數的做用域,也會銷燬。
接下來將是本篇文章最核心的內容,得益於swift語言的強大,ServerTrustPolicy被設計成enum
枚舉。既然本質上只是個枚舉,那麼咱們先不關心枚舉中的函數,先單獨看看有哪些枚舉子選項:
case performDefaultEvaluation(validateHost: Bool) case performRevokedEvaluation(validateHost: Bool, revocationFlags: CFOptionFlags) case pinCertificates(certificates: [SecCertificate], validateCertificateChain: Bool, validateHost: Bool) case pinPublicKeys(publicKeys: [SecKey], validateCertificateChain: Bool, validateHost: Bool) case disableEvaluation case customEvaluation((_ serverTrust: SecTrust, _ host: String) -> Bool)
千萬別認爲上邊的某些選項是個函數,其實他們只是不一樣的類型加上關聯值而已。咱們先不對這些選項作不解釋,由於在下邊的方法中會根據這些選項作出不一樣的操做,到那時在說明這些選項的做用更好。
還有一點要明白,在swift中是像下邊代碼這樣初始化枚舉的:
ServerTrustPolicy.performDefaultEvaluation(validateHost: true)
/// Returns all certificates within the given bundle with a `.cer` file extension. /// /// - parameter bundle: The bundle to search for all `.cer` files. /// /// - returns: All certificates within the given bundle. public static func certificates(in bundle: Bundle = Bundle.main) -> [SecCertificate] { var certificates: [SecCertificate] = [] let paths = Set([".cer", ".CER", ".crt", ".CRT", ".der", ".DER"].map { fileExtension in bundle.paths(forResourcesOfType: fileExtension, inDirectory: nil) }.joined()) for path in paths { if let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData, let certificate = SecCertificateCreateWithData(nil, certificateData) { certificates.append(certificate) } } return certificates }
在開發中,若是和服務器的安全鏈接須要對服務器進行驗證,最好的辦法就是在本地保存一些證書,拿到服務器傳過來的證書,而後進行對比,若是有匹配的,就表示能夠信任該服務器。從上邊的函數中能夠看出,Alamofire會在Bundle(默認爲main)中查找帶有[".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]
後綴的證書。
注意,上邊函數中的paths保存的是這些證書的路徑,map把這些後綴轉換成路徑,咱們以.cer
爲例。經過map後,原來的".cer"
就變成了一個數組,也就是說經過map後,原來的數組變成了二維數組了,而後再經過joined()
函數,把二維數組轉換成一維數組。
而後要作的就是根據這些路徑獲取證書數據了,就很少作解釋了。
這個比較好理解,就是在本地證書中取出公鑰,至於證書是由什麼組成的,你們能夠網上本身查找相關內容,
/// Returns all public keys within the given bundle with a `.cer` file extension. /// /// - parameter bundle: The bundle to search for all `*.cer` files. /// /// - returns: All public keys within the given bundle. public static func publicKeys(in bundle: Bundle = Bundle.main) -> [SecKey] { var publicKeys: [SecKey] = [] for certificate in certificates(in: bundle) { if let publicKey = publicKey(for: certificate) { publicKeys.append(publicKey) } } return publicKeys }
上邊的函數很簡單,可是他用到了另一個函數publicKey(for: certificate)
獲取SecKey能夠經過SecCertificate也能夠經過SecTrust,下邊的函數是第一種狀況:
private static func publicKey(for certificate: SecCertificate) -> SecKey? { var publicKey: SecKey? let policy = SecPolicyCreateBasicX509() var trust: SecTrust? let trustCreationStatus = SecTrustCreateWithCertificates(certificate, policy, &trust) if let trust = trust, trustCreationStatus == errSecSuccess { publicKey = SecTrustCopyPublicKey(trust) } return publicKey }
上邊的過程沒什麼好說的,基本上這是固定寫法,值得注意的是上邊默認是按照X509證書格式來解析的,所以在生成證書的時候最好使用這個格式。不然可能沒法獲取到publicKey。
從函數設計的角度考慮,evaluate應該接受兩個參數,一個是服務器的證書,一個是host。返回一個布爾類型。
evaluate函數是枚舉中的一個函數,所以它必然依賴枚舉的子選項。這就說明只有初始化枚舉才能使用這個函數。
舉一個現實生活中的一個小例子。有一個管理員,他手下管理這3個員工,分別是廚師,前臺,行政,如今有一個任務須要想辦法弄明白這3我的會不會喊麥,有兩種方法能夠得出結果,一種是管理員一個一個的去問,也就是得出結果的方法掌握在管理員手中,只有經過管理員才能知道答案。有一個老闆想知道廚師會不會喊麥。他必需要去問管理員才行。這就形成了邏輯上的問題。另外一種方法,讓每個人當場喊一個,任何人在任何場合都能得出結果。
最近從新看了代碼大全這本書,對子程序的設計有了全新的認識。重點還在於抽象類型是什麼?這個就很少說了,有興趣的朋友能夠去看看那本書。
這個函數很長,但整體的思想是根據不一樣的策略作出不一樣的操做。咱們先把該函數弄上來:
/// Evaluates whether the server trust is valid for the given host. /// /// - parameter serverTrust: The server trust to evaluate. /// - parameter host: The host of the challenge protection space. /// /// - returns: Whether the server trust is valid. public func evaluate(_ serverTrust: SecTrust, forHost host: String) -> Bool { var serverTrustIsValid = false switch self { case let .performDefaultEvaluation(validateHost): let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) SecTrustSetPolicies(serverTrust, policy) serverTrustIsValid = trustIsValid(serverTrust) case let .performRevokedEvaluation(validateHost, revocationFlags): let defaultPolicy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) let revokedPolicy = SecPolicyCreateRevocation(revocationFlags) SecTrustSetPolicies(serverTrust, [defaultPolicy, revokedPolicy] as CFTypeRef) serverTrustIsValid = trustIsValid(serverTrust) case let .pinCertificates(pinnedCertificates, validateCertificateChain, validateHost): if validateCertificateChain { let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) SecTrustSetPolicies(serverTrust, policy) SecTrustSetAnchorCertificates(serverTrust, pinnedCertificates as CFArray) SecTrustSetAnchorCertificatesOnly(serverTrust, true) serverTrustIsValid = trustIsValid(serverTrust) } else { let serverCertificatesDataArray = certificateData(for: serverTrust) let pinnedCertificatesDataArray = certificateData(for: pinnedCertificates) outerLoop: for serverCertificateData in serverCertificatesDataArray { for pinnedCertificateData in pinnedCertificatesDataArray { if serverCertificateData == pinnedCertificateData { serverTrustIsValid = true break outerLoop } } } } case let .pinPublicKeys(pinnedPublicKeys, validateCertificateChain, validateHost): var certificateChainEvaluationPassed = true if validateCertificateChain { let policy = SecPolicyCreateSSL(true, validateHost ? host as CFString : nil) SecTrustSetPolicies(serverTrust, policy) certificateChainEvaluationPassed = trustIsValid(serverTrust) } if certificateChainEvaluationPassed { outerLoop: for serverPublicKey in ServerTrustPolicy.publicKeys(for: serverTrust) as [AnyObject] { for pinnedPublicKey in pinnedPublicKeys as [AnyObject] { if serverPublicKey.isEqual(pinnedPublicKey) { serverTrustIsValid = true break outerLoop } } } } case .disableEvaluation: serverTrustIsValid = true case let .customEvaluation(closure): serverTrustIsValid = closure(serverTrust, host) } return serverTrustIsValid }
無論選用那種策略,要完成驗證都須要3步:
SecPolicyCreateSSL
建立策略,是否驗證hostSecTrustSetPolicies
爲待驗證的對象設置策略trustIsValid
進行驗證到了這裏就有必要介紹一下幾種策略的用法了:
performDefaultEvaluation
默認的策略,只有合法證書才能經過驗證performRevokedEvaluation
對註銷證書作的一種額外設置,關於註銷證書驗證超過了本篇文章的範圍,有興趣的朋友能夠查看官方文檔。pinCertificates
驗證指定的證書,這裏邊有一個參數:是否驗證證書鏈,關於證書鏈的相關內容能夠看這篇文章iOS 中對 HTTPS 證書鏈的驗證.驗證證書鏈算是比較嚴格的驗證了。這裏邊設置錨點等等,這裏就不作解釋了。若是不驗證證書鏈的話,只要對比指定的證書有沒有和服務器信任的證書匹配項,只要有一個能匹配上,就驗證經過pinPublicKeys
這個更上邊的那個差很少,就不作介紹了disableEvaluation
該選項下,驗證一直都是經過的,也就是說無條件信任customEvaluation
自定義驗證,須要返回一個布爾類型的結果上邊的這些驗證選項中,咱們可能根據本身的需求進行驗證,其中最安全的是證書鏈加host雙重驗證。並且在上邊的evaluate函數中用到了4個輔助函數,咱們來看看:
該函數用於判斷是否驗證成功
private func trustIsValid(_ trust: SecTrust) -> Bool { var isValid = false var result = SecTrustResultType.invalid let status = SecTrustEvaluate(trust, &result) if status == errSecSuccess { let unspecified = SecTrustResultType.unspecified let proceed = SecTrustResultType.proceed isValid = result == unspecified || result == proceed } return isValid }
該函數把服務器的SecTrust處理成證書二進制數組
private func certificateData(for trust: SecTrust) -> [Data] { var certificates: [SecCertificate] = [] for index in 0..<SecTrustGetCertificateCount(trust) { if let certificate = SecTrustGetCertificateAtIndex(trust, index) { certificates.append(certificate) } } return certificateData(for: certificates) }
private func certificateData(for certificates: [SecCertificate]) -> [Data] { return certificates.map { SecCertificateCopyData($0) as Data } }
private static func publicKeys(for trust: SecTrust) -> [SecKey] { var publicKeys: [SecKey] = [] for index in 0..<SecTrustGetCertificateCount(trust) { if let certificate = SecTrustGetCertificateAtIndex(trust, index), let publicKey = publicKey(for: certificate) { publicKeys.append(publicKey) } } return publicKeys }
其實在開發中,能夠沒必要關心這些實現細節,要想弄明白這些策略的詳情,還須要作不少的功課才行。
因爲知識水平有限,若有錯誤,還望指出
Alamofire源碼解讀系列(一)之概述和使用 簡書-----博客園
Alamofire源碼解讀系列(二)之錯誤處理(AFError) 簡書-----博客園
Alamofire源碼解讀系列(三)之通知處理(Notification) 簡書-----博客園
Alamofire源碼解讀系列(四)之參數編碼(ParameterEncoding) 簡書-----博客園
Alamofire源碼解讀系列(五)之結果封裝(Result) 簡書-----博客園
Alamofire源碼解讀系列(六)之Task代理(TaskDelegate) 簡書-----博客園
Alamofire源碼解讀系列(七)之網絡監控(NetworkReachabilityManager) 簡書-----博客園