往期导航:
服务器验证处理
相关文件:
ServerTrustEvaluation.swift
简介
当请求需要进行身份验证的时候,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中对相关类型进行了很多扩展。先弄明白这些扩展的作用于原理后对接下来去学习校验流程有很大的帮助。
扩展方式有两种:
- 直接扩展,比如Array,直接扩展Array,使用泛型约束来对扩展的作用范围进行约束
- 使用AlamofireExtended协议包裹扩展,将所要扩展的对象实现扩展实现AlamofireExtended协议,然后对AlamofireExtended协议进行扩展+泛型约束,为需要扩展的对象添加方法,使用时需要对对象先
.af
调用返回AlamofireExtension包裹对象后再调用相关方法,好处是不会避免扩展入侵。具体的会在后面的工具扩展方法学习笔记中详细讲解。
Foundation框架扩展
- 校验器对象数组扩展,方便逐个遍历扩展
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
}
- 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) }))
}
}
认证相关类扩展
- 证书对象扩展,提取证书的公钥
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 }
}
}
- iOS12 以下评估结果扩展
iOS以下对SecTrust进行评估校验的方法为
SecTrustEvaluate(SecTrust, SecTrustResultType *)
,该方法不会抛出错误, 校验结果使用第二个参数的SecTrustResultType指针返回, 方法返回OSStatus状态码来标记检测状态,所以Alamofire对OSStatus与SecTrustResultType进行了扩展,添加了快速判定是否成功的计算属性
// 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)
}
}
- 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
}
}
- SecTrust扩展,用来评估服务端可靠性 SecTrust对象本身只是一个指针,用来进行证书校验,通过调用一些列CApi风格的方法,应用SecPolicy校验策略来怼指针所指向的待校验信息来进行校验,校验结果也存在指针数据中,也是需要通过CApi方法来获取结果或错误,iOS12开始提供了新的Api来进行校验,新的Api可以在校验失败时抛出错误,而旧的Api则需要根据状态码来自行拼装错误,因此Alamofire同时提供了iOS12以上与以下的两套校验方法,并且把旧的方法标记为iOS12 Deprecated
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进行检查,协议方法很简单, 只有一个方法:
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
有以下几个功能:
- 把凭证注入到请求中
- 刷新凭证并返回新的凭证或错误
- 当服务器返回401时,如果是OAuth2的401,需要验证者来鉴别这个401错误是由内容方返回的?还是由单点登录方返回的(如果是单点登录方返回的,表示是校验失败了,拦截器就不会重试请求。如果是内容方返回的,表示凭证需要刷新,拦截器会让验证者刷新凭证后重新请求)
- 在请求失败时,告诉拦截器服务端是否对身份验证通过了(如果验证通过,拦截器会直接重试请求,如果验证失败,拦截器会让验证者刷新凭证后重新请求)
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
}
身份验证拦截器AuthenticationInterceptor
- 实现了RequestInterceptor协议,可以对请求进行拦截处理与重试
- 持有一个验证者对象,用来对请求拦截下来,让验证者把凭证注入请求,同时若发现注入时凭证过期,会先让验证者刷新凭证后再注入请求中发送。在请求失败时会使用验证者来判断是否需要刷新凭证来重试请求。
- 另外持有一个时间窗口对象,可以定义多少秒内最大的刷新凭证次数,超出次数就抛出请求失败的错误。
定义
使用泛型约束声明了所持有的验证者的类型
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()
实现拦截器协议的方法
拦截器协议有两个方法:
- adapt方法,用来在发送请求前拦截请求,对请求进行适配处理,处理成功后发送新的请求,处理失败则取消请求,抛出错误。拦截器会在该方法中使用认证者对凭证进行刷新,注入操作。
- 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)
}
}
私有刷新凭证方法
拦截器的核心方法,用来使用认证者协议对象来对凭证进行刷新,并可以:
- 在刷新前先进行刷新最大次数校验
- 在请求后保存刷新时间戳,供下次刷新做次数校验
- 在刷新后对暂存的请求适配结果回调进行处理
- 在刷新后对暂存的请求重试结果回调进行处理
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)) }
}
}