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
.af
調用返回AlamofireExtension包裹對象後再調用相關方法,好處是不會避免擴展入侵。具體的會在後面的工具擴展方法學習筆記中詳細講解。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
}
複製代碼
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) }))
}
}
複製代碼
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)
}
}
複製代碼
// 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 }
}
}
複製代碼
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)
}
}
複製代碼
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
}
}
複製代碼
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的做用是在初始化的時候能夠針對不一樣的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
}
}
複製代碼
該協議用來對須要校驗的對象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對象進行校驗處理
使用默認的安全策略來對服務器進行校驗,只會簡單的控制是否須要對主機名進行驗證
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身份驗證失敗時,通知驗證者對驗證憑證進行刷新等操做,使用方只須要關注驗證者與驗證憑證便可,從而不用去關心複雜的驗證邏輯與刷新邏輯。
驗證憑證是個須要注入要請求中的東西,注入的位置由驗證者決定,驗證憑證自己只須要告知驗證攔截器自身是否須要刷新,當憑證過時須要刷新時,攔截器就會使用所持有的驗證者來對憑證進行刷新。
public protocol AuthenticationCredential {
// 是否須要刷新憑證, 若是返回false, 下面的Authenticator接口對象將會調用刷新方法來刷新憑證
// 要注意的時, 好比憑證有效期是60min, 那麼最好在過時前5分鐘的時候, 就要返回true刷新憑證了, 避免後續請求中憑證過時
var requiresRefresh: Bool { get }
}
複製代碼
有如下幾個功能:
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
}
複製代碼
定義了兩個錯誤:
public enum AuthenticationError: Error {
// 憑證丟失了
case missingCredential
// 在刷新窗口期內刷新太屢次憑證了
case excessiveRefresh
}
複製代碼
使用泛型約束聲明瞭所持有的驗證者的類型
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()
複製代碼
攔截器協議有兩個方法:
// 適配請求
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)
}
}
複製代碼
攔截器的核心方法,用來使用認證者協議對象來對憑證進行刷新,並能夠:
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)) }
}
}
複製代碼