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)) }
        }
    }
复制代码
相关文章
相关标签/搜索