Alamofire源碼學習(十五): 服務器驗證處理與身份驗證處理

往期導航:

Alamofire源碼學習目錄合集swift

服務器驗證處理

相關文件:
ServerTrustEvaluation.swiftapi

簡介

當請求須要進行身份驗證的時候,URLSessionDelegate會回調方法數組

open func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)
複製代碼

來讓調用者處理驗證操做,Alamofire中響應URLSessionDelegate的對象爲SessionDelegate,在對應的回調方法中會先對驗證類型進行判斷,若是是服務器驗證類型(https,自簽名證書等),就使用ServerTrustEvaluation中的相關對象方法來處理驗證,其餘類型的驗證則使用Request中的credential來處理。安全

工具擴展 -- 輔助校驗使用

對服務器進行校驗的時候,使用的是系統的Security框架中的SecTrust,SecPolicy等對象來調用C類型的函數來操做,爲了方便調用,Alamofire在ServerTrustEvaluation中對相關類型進行了不少擴展。先弄明白這些擴展的做用於原理後對接下來去學習校驗流程有很大的幫助。服務器

擴展方式有兩種:markdown

  1. 直接擴展,好比Array,直接擴展Array,使用泛型約束來對擴展的做用範圍進行約束
  2. 使用AlamofireExtended協議包裹擴展,將所要擴展的對象實現擴展實現AlamofireExtended協議,而後對AlamofireExtended協議進行擴展+泛型約束,爲須要擴展的對象添加方法,使用時須要對對象先.af調用返回AlamofireExtension包裹對象後再調用相關方法,好處是不會避免擴展入侵。具體的會在後面的工具擴展方法學習筆記中詳細講解。

Foundation框架擴展

  1. 校驗器對象數組擴展,方便逐個遍歷擴展
extension Array where Element == ServerTrustEvaluating {
    #if os(Linux)
    // Add this same convenience method for Linux.
    #else
    // 對須要認證的host遍歷數組來認證, 任何一個處理器失敗都會拋出錯誤
    public func evaluate(_ trust: SecTrust, forHost host: String) throws {
        for evaluator in self {
            try evaluator.evaluate(trust, forHost: host)
        }
    }
    #endif
}
複製代碼
  1. Bundle擴展,用來把app內置的所有證書、公鑰給取出來
extension Bundle: AlamofireExtended {}
extension AlamofireExtension where ExtendedType: Bundle {
    // 把bundle中全部有效的證書都讀取出來返回
    public var certificates: [SecCertificate] {
        paths(forResourcesOfTypes: [".cer", ".CER", ".crt", ".CRT", ".der", ".DER"]).compactMap { path in
            //這裏用compactMap來把獲取失敗的證書過濾掉
            guard
                let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData,
                let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil }

            return certificate
        }
    }

    // 返回bundle中全部可用證書的公鑰
    public var publicKeys: [SecKey] {
        certificates.af.publicKeys
    }

    // 根據擴展類型數組, 把bundle中全部這些擴展的文件路徑以數組形式返回
    public func paths(forResourcesOfTypes types: [String]) -> [String] {
        Array(Set(types.flatMap { type.paths(forResourcesOfType: $0, inDirectory: nil) }))
    }
}
複製代碼

認證相關類擴展

  1. 證書對象擴展,提取證書的公鑰
extension SecCertificate: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecCertificate {
    // 從證書中提取公鑰, 若是提取失敗, 返回nil
    public var publicKey: SecKey? {
        let policy = SecPolicyCreateBasicX509()
        var trust: SecTrust?
        let trustCreationStatus = SecTrustCreateWithCertificates(type, policy, &trust)

        guard let createdTrust = trust, trustCreationStatus == errSecSuccess else { return nil }

        return SecTrustCopyPublicKey(createdTrust)
    }
}
複製代碼
  1. 證書數組擴展,提取所有的公鑰
// MARK: 證書數組擴展
extension Array: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == [SecCertificate] {
    // 把數組中的證書對象所有以Data格式返回
    public var data: [Data] {
        type.map { SecCertificateCopyData($0) as Data }
    }

    // 把全部證書對象的公鑰提取出來,使用compactMap過濾提取失敗的對象
    public var publicKeys: [SecKey] {
        type.compactMap { $0.af.publicKey }
    }
}
複製代碼
  1. iOS12 如下評估結果擴展

iOS如下對SecTrust進行評估校驗的方法爲SecTrustEvaluate(SecTrust, SecTrustResultType *),該方法不會拋出錯誤, 校驗結果使用第二個參數的SecTrustResultType指針返回, 方法返回OSStatus狀態碼來標記檢測狀態,因此Alamofire對OSStatus與SecTrustResultType進行了擴展,添加了快速斷定是否成功的計算屬性cookie

// MARK: OSStatus擴展
extension OSStatus: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == OSStatus {
    // 返回是否成功
    public var isSuccess: Bool { type == errSecSuccess }
}

// MARK: SecTrustResultType擴展
extension SecTrustResultType: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecTrustResultType {
    // 返回是否成功
    public var isSuccess: Bool {
        (type == .unspecified || type == .proceed)
    }
}
複製代碼
  1. SecPolicy安全策略擴展,快速建立三種安全策略,用來校驗服務端證書
extension SecPolicy: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecPolicy {
    
    // 校驗服務端證書, 可是不須要主機名匹配
    public static let `default` = SecPolicyCreateSSL(true, nil)

    // 校驗服務端證書, 同時必須匹配主機名
    public static func hostname(_ hostname: String) -> SecPolicy {
        SecPolicyCreateSSL(true, hostname as CFString)
    }
    
    // 校驗證書是否被撤銷, 建立策略失敗會拋出異常
    public static func revocation(options: RevocationTrustEvaluator.Options) throws -> SecPolicy {
        guard let policy = SecPolicyCreateRevocation(options.rawValue) else {
            throw AFError.serverTrustEvaluationFailed(reason: .revocationPolicyCreationFailed)
        }

        return policy
    }
}
複製代碼
  1. SecTrust擴展,用來評估服務端可靠性

SecTrust對象自己只是一個指針,用來進行證書校驗,經過調用一些列CApi風格的方法,應用SecPolicy校驗策略來懟指針所指向的待校驗信息來進行校驗,校驗結果也存在指針數據中,也是須要經過CApi方法來獲取結果或錯誤,iOS12開始提供了新的Api來進行校驗,新的Api能夠在校驗失敗時拋出錯誤,而舊的Api則須要根據狀態碼來自行拼裝錯誤,所以Alamofire同時提供了iOS12以上與如下的兩套校驗方法,而且把舊的方法標記爲iOS12 Deprecatedsession

extension SecTrust: AlamofireExtended {}
extension AlamofireExtension where ExtendedType == SecTrust {
    
    //MARK: iOS12 以上鑑定方法
    
    @available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *)
    public func evaluate(afterApplying policy: SecPolicy) throws {
        // 先應用安全策略, 而後調用evaluate方法校驗
        try apply(policy: policy).af.evaluate()
    }
    
    // 使用iOS12 的api來評估對指定證書和策略的信任
    // 錯誤類型使用CFError指針返回
    @available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *)
    public func evaluate() throws {
        var error: CFError?
        // 使用iOS12以上的Api進行校驗, 錯誤使用CFError指針返回
        let evaluationSucceeded = SecTrustEvaluateWithError(type, &error)

        if !evaluationSucceeded {
            // 校驗失敗拋出錯誤
            throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error))
        }
    }

    //MARK: iOS12 如下鑑定方法
    @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)")
    @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate(afterApplying:)")
    @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate(afterApplying:)")
    @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate(afterApplying:)")
    public func validate(policy: SecPolicy, errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws {
        // 一樣是先應用安全策略,而後調用方法校驗
        try apply(policy: policy).af.validate(errorProducer: errorProducer)
    }
    
    
    // iOS12 如下評估證書與策略是否信任的方法, 評估結果會以SecTrustResultType指針返回, 同時評估方法會返回OSStatus值來判斷結果, 當評估失敗時, 使用函數入參的errorProducer來把兩個狀態碼變成Error類型拋出
    @available(iOS, introduced: 10, deprecated: 12, renamed: "evaluate()")
    @available(macOS, introduced: 10.12, deprecated: 10.14, renamed: "evaluate()")
    @available(tvOS, introduced: 10, deprecated: 12, renamed: "evaluate()")
    @available(watchOS, introduced: 3, deprecated: 5, renamed: "evaluate()")
    public func validate(errorProducer: (_ status: OSStatus, _ result: SecTrustResultType) -> Error) throws {
        // 調用iOS12 如下校驗方法, 獲取結果與狀態
        var result = SecTrustResultType.invalid
        let status = SecTrustEvaluate(type, &result)

        // 出錯的話使用傳入的錯誤產生閉包來生成Error並拋出
        guard status.af.isSuccess && result.af.isSuccess else {
            throw errorProducer(status, result)
        }
    }
    

    // 把安全策略應用到SecTrust上, 準備接下來的評估, 失敗會拋出對應錯誤
    public func apply(policy: SecPolicy) throws -> SecTrust {
        let status = SecTrustSetPolicies(type, policy)

        guard status.af.isSuccess else {
            throw AFError.serverTrustEvaluationFailed(reason: .policyApplicationFailed(trust: type,
                                                                                       policy: policy,
                                                                                       status: status))
        }

        return type
    }


    //MARK: 工具擴展
    
    // 設置自定義證書到self, 容許對自簽名證書進行徹底驗證
    public func setAnchorCertificates(_ certificates: [SecCertificate]) throws {
        // 添加證書
        let status = SecTrustSetAnchorCertificates(type, certificates as CFArray)
        guard status.af.isSuccess else {
            throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: status,
                                                                                               certificates: certificates))
        }

        // 只信任設置的證書
        let onlyStatus = SecTrustSetAnchorCertificatesOnly(type, true)
        guard onlyStatus.af.isSuccess else {
            throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: onlyStatus,
                                                                                               certificates: certificates))
        }
    }

    // 獲取公鑰列表
    public var publicKeys: [SecKey] {
        certificates.af.publicKeys
    }

    // 獲取持有的證書
    public var certificates: [SecCertificate] {
        // 這裏使用了compactMap, 由於根據index遍歷獲取證書可能會取不到, 因此copactMap過濾後返回所有的有效證書數組
        (0..<SecTrustGetCertificateCount(type)).compactMap { index in
            SecTrustGetCertificateAtIndex(type, index)
        }
    }

    // 證書的data類型
    public var certificateData: [Data] {
        certificates.af.data
    }

    // 使用默認安全策略來評估, 不對主機名進行驗證
    public func performDefaultValidation(forHost host: String) throws {
        if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
            try evaluate(afterApplying: SecPolicy.af.default)
        } else {
            try validate(policy: SecPolicy.af.default) { status, result in
                AFError.serverTrustEvaluationFailed(reason: .defaultEvaluationFailed(output: .init(host, type, status, result)))
            }
        }
    }

    // 使用默認安全策略來評估, 同時會進行主機名驗證
    public func performValidation(forHost host: String) throws {
        if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
            try evaluate(afterApplying: SecPolicy.af.hostname(host))
        } else {
            try validate(policy: SecPolicy.af.hostname(host)) { status, result in
                AFError.serverTrustEvaluationFailed(reason: .hostValidationFailed(output: .init(host, type, status, result)))
            }
        }
    }
}
複製代碼

ServerTrustManager -- 證書校驗管理器

ServerTrustManager的做用是在初始化的時候能夠針對不一樣的host持有不一樣的校驗器,而後ServerTrustManager會被Session持有,在SessionDelegate須要對服務器的校驗進行處理的時候,經過SessionDelegate中的SessionStateProvider代理來從Session中獲取ServerTrustManager,而後從映射中根據host取出來對應的校驗器返回給SessionDelegate用來對服務器進行校驗處理。閉包

open class ServerTrustManager {
    /// 是否全部的域名都須要認證, 默認爲true
    /// 若爲true,每一個host都要有對應的認證器存在,不然會拋出異常
    /// 若爲false,當某個host沒有對應的認證器時,返回nil,不拋出錯誤
    public let allHostsMustBeEvaluated: Bool

    /// 保存host與認證器的映射
    public let evaluators: [String: ServerTrustEvaluating]

    /// 初始化, 因爲不一樣的服務區可能會有不一樣的認證方式, 因此管理的認證方式是基於域名的
    public init(allHostsMustBeEvaluated: Bool = true, evaluators: [String: ServerTrustEvaluating]) {
        self.allHostsMustBeEvaluated = allHostsMustBeEvaluated
        self.evaluators = evaluators
    }

    /// 根據域名返回對應的認證器, 可拋出錯誤
    open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
        guard let evaluator = evaluators[host] else {
            //若設置了所有域名都要被認證, 當沒有對應的認證器時, 就拋出錯誤
            if allHostsMustBeEvaluated {
                throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host))
            }

            return nil
        }

        return evaluator
    }
}
複製代碼

ServerTrustEvaluating協議 -- 服務器驗證協議

該協議用來對須要校驗的對象SecTrust進行校驗, 同時支持支持對host進行檢查,協議方法很簡單, 只有一個方法:app

public protocol ServerTrustEvaluating {
    #if os(Linux)
    //Linux下有對應的同名方法
    #else
    /// 對參數SecTrust與域名進行校驗, 校驗結果會保存在SecTrust中, 校驗失敗會拋出錯誤
    func evaluate(_ trust: SecTrust, forHost host: String) throws
    #endif
}
複製代碼

Alamofire內部實現了6個校驗器類,能夠直接拿來使用,這6個類都被修飾爲final,不容許繼承,若是須要實現本身的校驗邏輯,須要本身實現協議,來對傳入的SecTrust對象進行校驗處理

Alamofire默認實現的6個校驗器

DefaultTrustEvaluator -- 默認校驗器

使用默認的安全策略來對服務器進行校驗,只會簡單的控制是否須要對主機名進行驗證

public final class DefaultTrustEvaluator: ServerTrustEvaluating {
    private let validateHost: Bool

    // 初始化, 默認會對主機名進行驗證
    public init(validateHost: Bool = true) {
        self.validateHost = validateHost
    }

    public func evaluate(_ trust: SecTrust, forHost host: String) throws {
        //根據對主機名進行驗證與否, 分別調用兩個不一樣的校驗擴展方法
        if validateHost {
            try trust.af.performValidation(forHost: host)
        }
        try trust.af.performDefaultValidation(forHost: host)
    }
}
複製代碼

吊銷證書校驗器

可使用默認安全策略進行校驗的同時,檢測證書是否被吊銷。Alamofire進過測試發現蘋果從iOS10.1纔開始支持吊銷證書的檢測

public final class RevocationTrustEvaluator: ServerTrustEvaluating {
    
    // 封裝CFOptionFlags來建立吊銷證書校驗的安全策略
    public struct Options: OptionSet {
        /// Perform revocation checking using the CRL (Certification Revocation List) method.
        public static let crl = Options(rawValue: kSecRevocationCRLMethod)
        /// Consult only locally cached replies; do not use network access.
        public static let networkAccessDisabled = Options(rawValue: kSecRevocationNetworkAccessDisabled)
        /// Perform revocation checking using OCSP (Online Certificate Status Protocol).
        public static let ocsp = Options(rawValue: kSecRevocationOCSPMethod)
        /// Prefer CRL revocation checking over OCSP; by default, OCSP is preferred.
        public static let preferCRL = Options(rawValue: kSecRevocationPreferCRL)
        /// Require a positive response to pass the policy. If the flag is not set, revocation checking is done on a
        /// "best attempt" basis, where failure to reach the server is not considered fatal.
        public static let requirePositiveResponse = Options(rawValue: kSecRevocationRequirePositiveResponse)
        /// Perform either OCSP or CRL checking. The checking is performed according to the method(s) specified in the
        /// certificate and the value of `preferCRL`.
        public static let any = Options(rawValue: kSecRevocationUseAnyAvailableMethod)

        /// The raw value of the option.
        public let rawValue: CFOptionFlags

        /// Creates an `Options` value with the given `CFOptionFlags`.
        ///
        /// - Parameter rawValue: The `CFOptionFlags` value to initialize with.
        public init(rawValue: CFOptionFlags) {
            self.rawValue = rawValue
        }
    }

    // 是否須要進行默認安全校驗, 默認true
    private let performDefaultValidation: Bool
    // 是否須要進行主機名驗證, 默認true
    private let validateHost: Bool
    // 用來建立吊銷證書校驗安全策略的Options, 默認.any
    private let options: Options

    public init(performDefaultValidation: Bool = true, validateHost: Bool = true, options: Options = .any) {
        self.performDefaultValidation = performDefaultValidation
        self.validateHost = validateHost
        self.options = options
    }

    // 實現協議的校驗方法
    public func evaluate(_ trust: SecTrust, forHost host: String) throws {
        if performDefaultValidation {
            // 須要進行默認校驗, 調用方法先進行默認校驗
            try trust.af.performDefaultValidation(forHost: host)
        }

        if validateHost {
            // 須要驗證主機名
            try trust.af.performValidation(forHost: host)
        }
        
        // 須要使用吊銷證書校驗安全策略來進行評估, iOS12上下分別用不一樣方法來進行校驗
        if #available(iOS 12, macOS 10.14, tvOS 12, watchOS 5, *) {
            try trust.af.evaluate(afterApplying: SecPolicy.af.revocation(options: options))
        } else {
            try trust.af.validate(policy: SecPolicy.af.revocation(options: options)) { status, result in
                AFError.serverTrustEvaluationFailed(reason: .revocationCheckFailed(output: .init(host, trust, status, result), options: options))
            }
        }
    }
}
複製代碼

自定義證書校驗器

可使用app內置的自定義證書來對服務端證書進行校驗,可用於自簽名證書驗證。

public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating {
    // 保存自定義證書, 默認爲app中全部的有效證書
    private let certificates: [SecCertificate]
    // 是否把自定義證書添加到校驗的錨定證書中, 用來對自簽名證書進行校驗, 默認false
    private let acceptSelfSignedCertificates: Bool
    // 是否須要進行默認校驗, 默認true
    private let performDefaultValidation: Bool
    // 是否驗證主機名, 默認true
    private let validateHost: Bool

    public init(certificates: [SecCertificate] = Bundle.main.af.certificates, acceptSelfSignedCertificates: Bool = false, performDefaultValidation: Bool = true, validateHost: Bool = true) {
        self.certificates = certificates
        self.acceptSelfSignedCertificates = acceptSelfSignedCertificates
        self.performDefaultValidation = performDefaultValidation
        self.validateHost = validateHost
    }

    public func evaluate(_ trust: SecTrust, forHost host: String) throws {
        guard !certificates.isEmpty else {
            // 若是自定義證書爲空, 直接拋出錯誤
            throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
        }

        if acceptSelfSignedCertificates {
            // 須要對自簽名證書校驗的話, 把自定義證書數組所有添加到SecTrust中
            try trust.af.setAnchorCertificates(certificates)
        }

        if performDefaultValidation {
            // 執行默認校驗
            try trust.af.performDefaultValidation(forHost: host)
        }

        if validateHost {
            // 執行主機名驗證
            try trust.af.performValidation(forHost: host)
        }

        // 從校驗結果中獲取服務端證書
        let serverCertificatesData = Set(trust.af.certificateData)
        // 獲取自定義證書
        let pinnedCertificatesData = Set(certificates.af.data)
        // 服務端證書與自定義證書是否匹配(經過判斷兩個集合存在交集肯定)
        let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
        if !pinnedCertificatesInServerData {
            // 不匹配則認爲校驗失敗, 拋出錯誤
            throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host,
                                                                                        trust: trust,
                                                                                        pinnedCertificates: certificates,
                                                                                        serverCertificates: trust.af.certificates))
        }
    }
}
複製代碼

公鑰校驗器

使用自定義公鑰來校驗服務端證書,須要注意的是由於沒有把自定義證書加入到SecTrust的錨定證書中,因此若是用這個校驗器來對自簽名證書進行校驗,會失敗。所以若是要校驗自簽名證書,請用上面的自定義證書校驗器

public final class PublicKeysTrustEvaluator: ServerTrustEvaluating {
    // 自定義公鑰數組, 默認爲app全部內置證書中可用的公鑰
    private let keys: [SecKey]
    // 是否執行默認校驗, 默認true
    private let performDefaultValidation: Bool
    // 是否驗證主機名, 默認true
    private let validateHost: Bool

    public init(keys: [SecKey] = Bundle.main.af.publicKeys, performDefaultValidation: Bool = true, validateHost: Bool = true) {
        self.keys = keys
        self.performDefaultValidation = performDefaultValidation
        self.validateHost = validateHost
    }

    public func evaluate(_ trust: SecTrust, forHost host: String) throws {
        guard !keys.isEmpty else {
            // 證書爲空則拋出異常
            throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound)
        }

        if performDefaultValidation {
            // 執行默認校驗
            try trust.af.performDefaultValidation(forHost: host)
        }

        if validateHost {
            // 驗證主機名
            try trust.af.performValidation(forHost: host)
        }

        // 默認校驗成功後, 檢測下自定義公鑰有沒有在服務端證書的公鑰中存在
        let pinnedKeysInServerKeys: Bool = {
            // 挨個遍歷自定義公鑰數組與服務端證書的公鑰數組, 存在相同對則校驗成功
            for serverPublicKey in trust.af.publicKeys {
                for pinnedPublicKey in keys {
                    if serverPublicKey == pinnedPublicKey {
                        return true
                    }
                }
            }
            return false
        }()

        if !pinnedKeysInServerKeys {
            // 公鑰匹配事變, 拋出錯誤
            throw AFError.serverTrustEvaluationFailed(reason: .publicKeyPinningFailed(host: host,
                                                                                      trust: trust,
                                                                                      pinnedKeys: keys,
                                                                                      serverKeys: trust.af.publicKeys))
        }
    }
}
複製代碼

組合校驗器

初始化持有一個校驗器數組,校驗時逐個對數組內校驗器進行校驗,所有成功纔會校驗成功,有任何一個校驗器失敗都會視爲校驗失敗

public final class CompositeTrustEvaluator: ServerTrustEvaluating {
    private let evaluators: [ServerTrustEvaluating]

    public init(evaluators: [ServerTrustEvaluating]) {
        self.evaluators = evaluators
    }

    public func evaluate(_ trust: SecTrust, forHost host: String) throws {
        // 簡單的調用校驗器數組擴展的方法對持有的校驗器數組挨個執行校驗,任何一個失敗都會拋出錯誤
        try evaluators.evaluate(trust, forHost: host)
    }
}
複製代碼

測試用校驗器

校驗方法爲空,不會對服務端作任何校驗,只是開發用,正式環境千萬不要用這個校驗器。
舊版本叫DisabledEvaluator,新版本更名叫DisabledTrustEvaluator

@available(*, deprecated, renamed: "DisabledTrustEvaluator", message: "DisabledEvaluator has been renamed DisabledTrustEvaluator.")
public typealias DisabledEvaluator = DisabledTrustEvaluator

public final class DisabledTrustEvaluator: ServerTrustEvaluating {
    
    public init() {}

    public func evaluate(_ trust: SecTrust, forHost host: String) throws {}
}
複製代碼

總結

以上就是Alamofire對服務端校驗進行的封裝,當請求碰到須要對服務端進行校驗時,會經過ServerTrustManager來獲取對應的校驗器協議對象來對SecTrust進行校驗,ServerTrustManager對校驗器內部實現原理無感知,使用接口解耦後能夠很自由的選擇使用校驗器,也能夠本身實現更加複雜的校驗器來進行業務邏輯處理。

身份驗證處理

簡介

HTTP請求是無狀態的,所以須要用一個標記來標明某個請求屬於哪一個用戶,標記用戶狀態的方法有不少,默認的方式是在登陸後由後臺建立session會話來標記用戶,並下發一個cookie放在響應頭中返回給請求端,請求端後續的每一個請求都會把該cookie帶上,來讓服務器識別該請求來自何方。可是session會話會過時,請求端須要在會話時及時刷新,來獲取新的cookie或者刷新cookie的有效期。另外OAuth2還須要從單點登陸方來獲取token,而且須要把token寫入進請求中攜帶發送給服務器。對此Alamofire使用RequestInterceptor請求攔截器來封裝了AuthenticationInterceptor身份驗證攔截器,並使用接口抽象了驗證者與驗證憑證,攔截器只負責使用攔截請求,並使用驗證者協議的相關方法來把驗證憑證注入到請求中,在收到服務端返回的401身份驗證失敗時,通知驗證者對驗證憑證進行刷新等操做,使用方只須要關注驗證者與驗證憑證便可,從而不用去關心複雜的驗證邏輯與刷新邏輯。

驗證憑證AuthenticationCredential

驗證憑證是個須要注入要請求中的東西,注入的位置由驗證者決定,驗證憑證自己只須要告知驗證攔截器自身是否須要刷新,當憑證過時須要刷新時,攔截器就會使用所持有的驗證者來對憑證進行刷新。

public protocol AuthenticationCredential {
    // 是否須要刷新憑證, 若是返回false, 下面的Authenticator接口對象將會調用刷新方法來刷新憑證
    // 要注意的時, 好比憑證有效期是60min, 那麼最好在過時前5分鐘的時候, 就要返回true刷新憑證了, 避免後續請求中憑證過時
    var requiresRefresh: Bool { get }
}
複製代碼

驗證者Authenticator

有如下幾個功能:

  1. 把憑證注入到請求中
  2. 刷新憑證並返回新的憑證或錯誤
  3. 當服務器返回401時,若是是OAuth2的401,須要驗證者來鑑別這個401錯誤是由內容方返回的?仍是由單點登陸方返回的(若是是單點登陸方返回的,表示是校驗失敗了,攔截器就不會重試請求。若是是內容方返回的,表示憑證須要刷新,攔截器會讓驗證者刷新憑證後從新請求)
  4. 在請求失敗時,告訴攔截器服務端是否對身份驗證經過了(若是驗證經過,攔截器會直接重試請求,若是驗證失敗,攔截器會讓驗證者刷新憑證後從新請求)
public protocol Authenticator: AnyObject {
    // 身份驗證憑據泛型
    associatedtype Credential: AuthenticationCredential

    // 把憑證應用到請求
    // 例如OAuth2認證, 就會把憑證中的access token添加到請求頭裏去
    func apply(_ credential: Credential, to urlRequest: inout URLRequest)

    // 刷新憑證, 完成閉包是個可逃逸閉包
    // 刷新方法會有兩種調用狀況:
    // 1.當請求準備發送時, 若是憑證須要被刷新, 攔截器就會調用驗證者的刷新方法來刷新憑證後再發出請求
    // 2.當請求響應失敗時, 攔截器會經過詢問驗證者是不是身份認證失敗, 若是是身份認證失敗, 就會調用刷新方法, 而後重試請求
    // 注意, 若是是OAuth2, 就會出現分歧, 當請求收到須要驗證身份時, 這個驗證要求究竟是來自於內容服務器?仍是來自於驗證服務器?若是是來自於內容服務器, 那麼只須要驗證者刷新憑證, 攔截器重試請求便可, 若是是來自於驗證服務器, 那麼就須要拋出錯誤, 讓用戶從新進行登陸才行.
    // 使用的時候, 若是用的OAuth2, 須要跟後臺小夥伴協商區分兩種身份驗證的狀況. 攔截器會根據下一個方法來判斷是否是身份驗證失敗了.
    func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)

    // 判斷請求失敗是否是由於身份驗證服務器致使的. 若身份驗證服務器頒發的憑證不會失效, 該方法簡單返回false就行.
    // 若是身份驗證服務器頒發的憑證會失效, 當請求碰到好比401錯誤時, 就要判斷, 驗證請求來自何方. 若來自內容服務器, 那就須要驗證者刷新憑證重試請求便可, 若來自驗證服務器, 就得拋出錯誤讓用戶從新登陸. 具體如何斷定, 須要跟後臺開發小夥伴協商
    // 所以若該協議方法返回true, 攔截器就不會重試請求, 而是直接拋出錯誤
    // 若該方法返回false, 攔截器就會根據下面的方法判斷憑證是否有效, 有效的話直接重試, 無效的話會先讓驗證者刷新憑證後再重試
    func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool

    // 判斷當請求失敗時, 本次請求有沒有被當前的憑證認證過
    // 若是驗證服務器頒發的憑證不會失效, 該方法簡單返回true就行
    /* 若驗證服務器頒發的憑證會失效, 就會存在這個狀況: 憑證A還在有效期內, 可是已經被認證服務器標記爲失效了, 那麼在失效後的第一個請求響應時, 就會觸發刷新邏輯, 在刷新過程當中, 還會有一系列使用憑證A認證的請求還沒落地, 那麼當響應觸發時, 就須要根據該方法檢測下本次請求是否被當前的憑證認證過. 若是認證過, 就須要暫存重試回調, 等刷新憑證後, 在執行重試回調. 若是未認證過, 表示當前持有的憑證可能已是新的憑證B了, 那麼直接重試請求就好 */
    func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool
}
複製代碼

身份驗證失敗錯誤定義

定義了兩個錯誤:

  1. 憑證丟失
  2. 刷新次數過多
public enum AuthenticationError: Error {
    // 憑證丟失了
    case missingCredential
    // 在刷新窗口期內刷新太屢次憑證了
    case excessiveRefresh
}
複製代碼

身份驗證攔截器AuthenticationInterceptor

  1. 實現了RequestInterceptor協議,能夠對請求進行攔截處理與重試
  2. 持有一個驗證者對象,用來對請求攔截下來,讓驗證者把憑證注入請求,同時若發現注入時憑證過時,會先讓驗證者刷新憑證後再注入請求中發送。在請求失敗時會使用驗證者來判斷是否須要刷新憑證來重試請求。
  3. 另外持有一個時間窗口對象,能夠定義多少秒內最大的刷新憑證次數,超出次數就拋出請求失敗的錯誤。

定義

使用泛型約束聲明瞭所持有的驗證者的類型

public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator // 或者這樣寫: public class AuthenticationInterceptor<AuthenticatorType: Authenticator>: RequestInterceptor 複製代碼

內部類型定義

定義了一些內部類型,用來處理內部處理邏輯

/// 憑證別名
    public typealias Credential = AuthenticatorType.Credential

    // MARK: Helper Types

    // 刷新窗口, 限制指定時間段內的最大刷新次數
    // 攔截器會持有每次刷新的時間戳, 每次刷新時, 經過遍歷時間戳檢測在最近的時間段內刷新的次數有沒有超過鎖限制的最大刷新次數, 超過的話, 就會取消刷新並拋出錯誤
    public struct RefreshWindow {
        // 限制週期, 默認30s
        public let interval: TimeInterval

        // 週期內最大刷新次數, 默認5次
        public let maximumAttempts: Int

        public init(interval: TimeInterval = 30.0, maximumAttempts: Int = 5) {
            self.interval = interval
            self.maximumAttempts = maximumAttempts
        }
    }

    // 攔截請求準備對請求進行適配時, 若是須要刷新憑證, 就會把適配方法的參數封裝成該結構體暫存在攔截器中, 刷新完成後對保存的全部結構體逐個調用completion來發送請求
    private struct AdaptOperation {
        let urlRequest: URLRequest
        let session: Session
        let completion: (Result<URLRequest, Error>) -> Void
    }
    // 攔截請求進行適配時的結果, 攔截器會根據不一樣的適配結果執行不一樣的邏輯
    private enum AdaptResult {
        // 適配完成, 獲取到了憑證, 接下來就要讓驗證者來把憑證注入到請求中
        case adapt(Credential)
        // 驗證失敗, 憑證丟失或者刷新次數過多, 會取消發送請求
        case doNotAdapt(AuthenticationError)
        // 正在刷新憑證, 會把適配方法的參數給封裝成上面的結構體暫存, 刷新憑證後會繼續執行
        case adaptDeferred
    }

    // 可變的狀態, 會使用@Protected修飾保證線程安全
    private struct MutableState {
        // 憑證, 可能爲空
        var credential: Credential?
        // 是否正在刷新憑證
        var isRefreshing = false
        // 刷新憑證的時間戳
        var refreshTimestamps: [TimeInterval] = []
        // 持有的刷新限制窗口
        var refreshWindow: RefreshWindow?
        // 暫存的適配請求的相關參數
        var adaptOperations: [AdaptOperation] = []
        // 暫存的重試請求的完成閉包, 當攔截器對請求失敗進行重試處理時, 若是發現須要刷新憑證, 會把完成閉包暫存, 而後讓驗證者刷新憑證, 以後在逐個遍歷該數組, 執行重試邏輯
        var requestsToRetry: [(RetryResult) -> Void] = []
    }
複製代碼

屬性

只有4個屬性,一個是計算屬性,由於把幾個須要線程安全的狀態放在了MutableState中了

// 憑證, 直接從mutableState中線程安全的讀寫, 
    public var credential: Credential? {
        get { mutableState.credential }
        set { mutableState.credential = newValue }
    }

    // 驗證者
    let authenticator: AuthenticatorType
    // 刷新憑證的隊列
    let queue = DispatchQueue(label: "org.alamofire.authentication.inspector")

    // 線程安全的狀態對象
    @Protected
    private var mutableState = MutableState()
複製代碼

實現攔截器協議的方法

攔截器協議有兩個方法:

  1. adapt方法,用來在發送請求前攔截請求,對請求進行適配處理,處理成功後發送新的請求,處理失敗則取消請求,拋出錯誤。攔截器會在該方法中使用認證者對憑證進行刷新,注入操做。
  2. retry方法,用來在請求失敗時,返回重試邏輯來讓Session從新發送請求。攔截器會在該方法中使用認證者對請求與相應進行憑證有效性的判斷,必要時刷新請求。
// 適配請求
    public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        // 獲取適配結果, 須要保證線程安全
        let adaptResult: AdaptResult = $mutableState.write { mutableState in
            // 檢查下是不是已經正在刷新憑證了
            guard !mutableState.isRefreshing else {
                // 正在刷新憑證, 就把全部參數暫存到adaptOperations中, 而後等待刷新完成後再處理這些參數
                let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
                mutableState.adaptOperations.append(operation)
                // 返回適配延期
                return .adaptDeferred
            }
            // 沒有再刷新憑證, 繼續適配
            // 獲取憑證
            guard let credential = mutableState.credential else {
                // 憑證丟失了, 返回錯誤
                let error = AuthenticationError.missingCredential
                return .doNotAdapt(error)
            }

            // 檢測下憑證是否有效
            guard !credential.requiresRefresh else {
                // 憑證過時, 須要刷新, 把參數暫存
                let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
                mutableState.adaptOperations.append(operation)
                // 調用刷新憑證方法
                refresh(credential, for: session, insideLock: &mutableState)
                // 返回適配延期
                return .adaptDeferred
            }

            // 憑證有效, 返回適配成功
            return .adapt(credential)
        }
        // 處理適配結果
        switch adaptResult {
        case let .adapt(credential):
            // 適配成功, 讓驗證者把憑證注入到請求中
            var authenticatedRequest = urlRequest
            authenticator.apply(credential, to: &authenticatedRequest)
            // 調用完成回調, 返回適配後的請求
            completion(.success(authenticatedRequest))

        case let .doNotAdapt(adaptError):
            // 適配失敗, 調用完成回調拋出錯誤
            completion(.failure(adaptError))

        case .adaptDeferred:
            // 適配延期, 不作任何處理, 等刷新憑證完成後, 會使用暫存的參數中的completion繼續處理
            break
        }
    }

    // MARK: Retry
    // 請求重試
    public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        // 若是沒有url請求或相應, 不重試
        guard let urlRequest = request.request, let response = request.response else {
            completion(.doNotRetry)
            return
        }

        // 問下驗證者是不是驗證服務器驗證失敗(OAuth2狀況)
        guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
            // 驗證服務器驗證失敗, 不重試, 直接返回錯誤, 須要用戶從新登陸
            completion(.doNotRetry)
            return
        }

        // 憑證是否存在
        guard let credential = credential else {
            // 憑證丟失不重試
            let error = AuthenticationError.missingCredential
            completion(.doNotRetryWithError(error))
            return
        }

        // 問下驗證者, 請求是否被當前的憑證認證過
        guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else {
            // 若是請求沒被當前憑證認證過, 表示這個憑證是新的, 直接重試就好
            completion(.retry)
            return
        }

        // 不然表示當前憑證已經無效了, 須要刷新憑證後再重試
        $mutableState.write { mutableState in
            // 暫存完成回調
            mutableState.requestsToRetry.append(completion)
            // 若是正在刷新憑證, 返回便可
            guard !mutableState.isRefreshing else { return }
            // 當前沒有刷新憑證, 調用refresh開始刷新
            refresh(credential, for: session, insideLock: &mutableState)
        }
    }
複製代碼

私有刷新憑證方法

攔截器的核心方法,用來使用認證者協議對象來對憑證進行刷新,並能夠:

  1. 在刷新前先進行刷新最大次數校驗
  2. 在請求後保存刷新時間戳,供下次刷新作次數校驗
  3. 在刷新後對暫存的請求適配結果回調進行處理
  4. 在刷新後對暫存的請求重試結果回調進行處理
private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) {
        // 檢測是否超出最大刷新次數
        guard !isRefreshExcessive(insideLock: &mutableState) else {
            // 超出最大刷新次數了, 走刷新失敗邏輯
            let error = AuthenticationError.excessiveRefresh
            handleRefreshFailure(error, insideLock: &mutableState)
            return
        }
        // 保存刷新時間戳
        mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime)
        // 標記正在刷新
        mutableState.isRefreshing = true

        // 在隊列裏異步調用驗證者的刷新方法, 由於攔截器在調用刷新方法前已經上鎖了, 因此這裏異步執行如下能夠跳出鎖, 能夠保證刷新行爲會是同步執行的.
        queue.async {
            self.authenticator.refresh(credential, for: session) { result in
                // 刷新完成回調
                self.$mutableState.write { mutableState in
                    switch result {
                    case let .success(credential):
                        // 成功處理
                        self.handleRefreshSuccess(credential, insideLock: &mutableState)
                    case let .failure(error):
                        // 失敗處理
                        self.handleRefreshFailure(error, insideLock: &mutableState)
                    }
                }
            }
        }
    }

    // 檢測是否超出最大刷新次數
    private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
        // 先獲取時間窗口對象, 沒有的話表示沒有限制最大次數
        guard let refreshWindow = mutableState.refreshWindow else { return false }
        
        // 時間窗口最小值的時間戳
        let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval

        // 遍歷保存的刷新時間戳, 使用reduce計算下窗口內刷新的次數
        let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in
            guard refreshWindowMin <= refreshTimestamp else { return }
            attempts += 1
        }
        
        // 是否超過最大刷新次數了
        let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts

        return isRefreshExcessive
    }

    // 處理刷新成功
    private func handleRefreshSuccess(_ credential: Credential, insideLock mutableState: inout MutableState) {
        // 保存憑證
        mutableState.credential = credential
        // 取出暫存的適配請求的參數數組
        let adaptOperations = mutableState.adaptOperations
        // 取出暫存的重試回調數組
        let requestsToRetry = mutableState.requestsToRetry
        // 把self持有的暫存數據清空
        mutableState.adaptOperations.removeAll()
        mutableState.requestsToRetry.removeAll()
        // 關閉刷新中的狀態
        mutableState.isRefreshing = false
        // 在queue中異步執行來跳出鎖
        queue.async {
            // 適配參數挨個繼續執行請求適配邏輯
            adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) }
            // 重試block也挨個執行, 開始重試
            requestsToRetry.forEach { $0(.retry) }
        }
    }
    // 處理刷新失敗
    private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) {
        // 取出暫存的兩個數組
        let adaptOperations = mutableState.adaptOperations
        let requestsToRetry = mutableState.requestsToRetry
        // 清空self持有的暫存數組
        mutableState.adaptOperations.removeAll()
        mutableState.requestsToRetry.removeAll()
        // 關閉刷新中狀態
        mutableState.isRefreshing = false
        // 在queue中異步執行來跳出鎖
        queue.async {
            // 適配器挨個調用失敗
            adaptOperations.forEach { $0.completion(.failure(error)) }
            // 重試器也挨個調用失敗
            requestsToRetry.forEach { $0(.doNotRetryWithError(error)) }
        }
    }
複製代碼
相關文章
相關標籤/搜索