Previous navigation:
Alamofire source learning directory collection
Server validation processing
Related documents: ServerTrustEvaluation. Swift
Introduction to the
The URLSessionDelegate calls back the method when the request needs to be authenticated
open func urlSession(_ session: URLSession.task: URLSessionTask.didReceive challenge: URLAuthenticationChallenge.completionHandler: @escaping (URLSession.AuthChallengeDisposition.URLCredential?). ->Void)
Copy the code
In Alamofire, the object that responds to the URLSessionDelegate is a SessionDelegate. In the corresponding callback method, the authentication type will be determined first. If the authentication type is server (HTTPS, self-signed certificate, etc.), The related object method in ServerTrustEvaluation is used for the validation, and the other types of validation are handled with credential in the Request.
Tool extension – auxiliary validation use
When verifying the server, the SecTrust, SecPolicy and other objects in the system Security framework are used to call the C type function for operation. To facilitate the call, Alamofire has extended the related types in ServerTrustEvaluation. Understanding how these extensions work will help you learn the validation process.
There are two ways to extend:
- Direct extensions, such as Array, directly extend Array, using generic constraints to constrain the scope of the extension
- Use AlamofireExtended protocol package extension, the object to be extended to implement the extension to implement AlamofireExtended protocol, and then extend the AlamofireExtended protocol + generic constraints, add methods for the object that needs to be extended, and need to use the object first
.af
The call returns the AlamofireExtension wrapped object and then calls the related method, with the benefit that extension intrusion is not avoided. The details will be explained in detail in the following tool extension method study notes.
Foundation Framework Extension
- Validator object array extension, easy to traverse the extension one by one
extension Array where Element= =ServerTrustEvaluating {
#if os(Linux)
// Add this same convenience method for Linux.
#else
// An error will be thrown if any handler fails to pass through the array of hosts that need authentication
public func evaluate(_ trust: SecTrust.forHost host: String) throws {
for evaluator in self {
try evaluator.evaluate(trust, forHost: host)
}
}
#endif
}
Copy the code
- The Bundle extension is used to extract all the certificates and public keys in the app
extension Bundle: AlamofireExtended {}
extension AlamofireExtension where ExtendedType: Bundle {
// Read all valid certificates from the bundle and return them
public var certificates: [SecCertificate] {
paths(forResourcesOfTypes: [".cer".".CER".".crt".".CRT".".der".".DER"]).compactMap { path in
// We use compactMap to filter out the failed certificates
guard
let certificateData = try? Data(contentsOf: URL(fileURLWithPath: path)) as CFData.let certificate = SecCertificateCreateWithData(nil, certificateData) else { return nil }
return certificate
}
}
// Return the public keys of all available certificates in the bundle
public var publicKeys: [SecKey] {
certificates.af.publicKeys
}
// Return the file paths of all the extensions in the bundle as an array based on the array of extension types
public func paths(forResourcesOfTypes types: [String])- > [String] {
Array(Set(types.flatMap { type.paths(forResourcesOfType: $0, inDirectory: nil)}}}))Copy the code
Authentication related class extensions
- Certificate object extension to extract the public key of the certificate
extension SecCertificate: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecCertificate {
// Retrieve the public key from the certificate. If the extract fails, return nil
public var publicKey: SecKey? {
let policy = SecPolicyCreateBasicX509(a)var trust: SecTrust?
let trustCreationStatus = SecTrustCreateWithCertificates(type, policy, &trust)
guard let createdTrust = trust, trustCreationStatus = = errSecSuccess else { return nil }
return SecTrustCopyPublicKey(createdTrust)
}
}
Copy the code
- Certificate array extension to extract all public keys
// MARK: Certificate array extension
extension Array: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= = [SecCertificate] {
// Return all the certificate objects in the array in Data format
public var data: [Data] {
type.map { SecCertificateCopyData($0) as Data}}// Extract the public keys of all certificate objects and filter the failed objects using compactMap
public var publicKeys: [SecKey] {
type.compactMap { $0.af.publicKey }
}
}
Copy the code
- IOS12 Following evaluation results extension
The following method for evaluating and verifying SecTrust is SecTrustEvaluate(SecTrust, SecTrustResultType *). This method does not throw an error and returns the result using the SecTrustResultType pointer of the second parameter. Method returns the OSStatus status code to mark the detection status, so Alamofire extends OSStatus and SecTrustResultType by adding a computing attribute that quickly determines success
// MARK: OSStatus extension
extension OSStatus: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =OSStatus {
// Return success
public var isSuccess: Bool { type = = errSecSuccess }
}
// MARK: SecTrustResultType extension
extension SecTrustResultType: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecTrustResultType {
// Return success
public var isSuccess: Bool {
(type = = .unspecified || type = = .proceed)
}
}
Copy the code
- SecPolicy Extends security policies to quickly create three security policies for verifying server certificates
extension SecPolicy: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecPolicy {
// Verify the server certificate, but do not need to match the host name
public static let `default` = SecPolicyCreateSSL(true.nil)
// Verify the server certificate, which must match the host name
public static func hostname(_ hostname: String) -> SecPolicy {
SecPolicyCreateSSL(true, hostname as CFString)}// If the certificate is revoked, an exception is thrown when the policy fails to be created
public static func revocation(options: RevocationTrustEvaluator.Options) throws -> SecPolicy {
guard let policy = SecPolicyCreateRevocation(options.rawValue) else {
throw AFError.serverTrustEvaluationFailed(reason: .revocationPolicyCreationFailed)
}
return policy
}
}
Copy the code
- The SecTrust extension is used to evaluate server reliability
The SecTrust object itself is only a pointer. It is used for certificate verification. A series of CAPI-style methods are called and the SecPolicy verification policy is applied to verify the information pointed to by the pointer. IOS12 started to provide a new Api for verification. The new Api could throw an error when the verification failed, while the old Api needed to assemble the error according to the status code. Therefore, Alamofire provides two sets of verification methods, both above iOS12 and below. And mark the old method as iOS12 Deprecated
extension SecTrust: AlamofireExtended {}
extension AlamofireExtension where ExtendedType= =SecTrust {
//MARK: iOS12 above authentication method
@available(iOS 12.macOS 10.14.tvOS 12.watchOS 5.*)
public func evaluate(afterApplying policy: SecPolicy) throws {
// Apply the security policy first, and then call evaluate to verify
try apply(policy: policy).af.evaluate()
}
// Use the iOS12 API to evaluate trust for specified certificates and policies
// The error type is returned with the CFError pointer
@available(iOS 12.macOS 10.14.tvOS 12.watchOS 5.*)
public func evaluate(a) throws {
var error: CFError?
// The error is returned with the CFError pointer
let evaluationSucceeded = SecTrustEvaluateWithError(type, &error)
if !evaluationSucceeded {
// The verification failed and an error was thrown
throw AFError.serverTrustEvaluationFailed(reason: .trustEvaluationFailed(error: error))
}
}
//MARK: iOS12 the following authentication methods
@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 {
// Again, apply the security policy first, and then call method validation
try apply(policy: policy).af.validate(errorProducer: errorProducer)
}
// iOS12 The following methods are used to evaluate whether certificates and policies are trusted. The evaluation result is returned as a SecTrustResultType pointer, and the evaluation method returns the OSStatus value to judge the result. When the evaluation fails, Use the errorProducer of the function input to throw the two status codes as Error types
@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 {
// Call the validation method below iOS12 to get the result and status
var result = SecTrustResultType.invalid
let status = SecTrustEvaluate(type, &result)
// If an Error occurs, use the passed Error to generate a closure to generate an Error and throw it
guard status.af.isSuccess && result.af.isSuccess else {
throw errorProducer(status, result)
}
}
// Apply the security policy to the SecTrust and prepare for the next evaluation. Failure will throw an error
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: tool extension
// Set the custom certificate to self to allow full verification of self-signed certificates
public func setAnchorCertificates(_ certificates: [SecCertificate]) throws {
// Add a certificate
let status = SecTrustSetAnchorCertificates(type, certificates as CFArray)
guard status.af.isSuccess else {
throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: status,
certificates: certificates))
}
// Only the set certificate is trusted
let onlyStatus = SecTrustSetAnchorCertificatesOnly(type, true)
guard onlyStatus.af.isSuccess else {
throw AFError.serverTrustEvaluationFailed(reason: .settingAnchorCertificatesFailed(status: onlyStatus,
certificates: certificates))
}
}
// Get the list of public keys
public var publicKeys: [SecKey] {
certificates.af.publicKeys
}
// Obtain the certificate
public var certificates: [SecCertificate] {
// compactMap is used, because the index traversal may not get the certificate, so copactMap filters and returns an array of valid certificates
(0..<SecTrustGetCertificateCount(type)).compactMap { index in
SecTrustGetCertificateAtIndex(type, index)
}
}
// The data type of the certificate
public var certificateData: [Data] {
certificates.af.data
}
// Use the default security policy for the evaluation and do not authenticate the host name
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)))
}
}
}
// The default security policy is used for evaluation, and host name authentication is performed
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)))
}
}
}
}
Copy the code
ServerTrustManager — Certificate verification manager
The purpose of the ServerTrustManager is to hold different validators for different hosts during initialization. The ServerTrustManager is then held by the Session. In case the SessionDelegate needs to handle server validation, Obtain the ServerTrustManager from the Session using the SessionStateProvider agent in the SessionDelegate. The validator is then retrieved from the map according to the host and returned to the SessionDelegate for verification processing on the server.
open class ServerTrustManager {
/// Whether all domain names require authentication. Default: true
/// If true, each host must have a corresponding authenticator, otherwise an exception will be thrown
/// If false, return nil and no error is thrown if a host does not have a corresponding authenticator
public let allHostsMustBeEvaluated: Bool
/// Save the mapping between host and authenticator
public let evaluators: [String: ServerTrustEvaluating]
/// initialization. Because different service areas may have different authentication modes, the managed authentication mode is based on domain name
public init(allHostsMustBeEvaluated: Bool = true.evaluators: [String: ServerTrustEvaluating]) {
self.allHostsMustBeEvaluated = allHostsMustBeEvaluated
self.evaluators = evaluators
}
/// Return the corresponding authenticator based on the domain name, which can throw an error
open func serverTrustEvaluator(forHost host: String) throws -> ServerTrustEvaluating? {
guard let evaluator = evaluators[host] else {
// If all domain names are set to be authenticated, an error will be thrown if there is no corresponding authenticator
if allHostsMustBeEvaluated {
throw AFError.serverTrustEvaluationFailed(reason: .noRequiredEvaluator(host: host))
}
return nil
}
return evaluator
}
}
Copy the code
ServerTrustEvaluating Protocol — Server authentication protocol
This protocol is used to verify the SecTrust object that needs to be verified. It also supports the check of host. The protocol method is very simple, only one method:
public protocol ServerTrustEvaluating {
#if os(Linux)
// There is a corresponding method of the same name in Linux
#else
/// Verify the SecTrust parameter and domain name. The verification result is saved in SecTrust. If the verification fails, an error is thrown
func evaluate(_ trust: SecTrust.forHost host: String) throws
#endif
}
Copy the code
Alamofire internally implements six validator classes that can be used directly. These six classes are all final and do not allow inheritance. If you need to implement your own validator logic, you need to implement your own protocol to validate incoming SecTrust objects
Alamofire implements six validators by default
DefaultTrustEvaluator — Default validator
Using the default security policy to verify the server simply controls whether the host name needs to be verified
public final class DefaultTrustEvaluator: ServerTrustEvaluating {
private let validateHost: Bool
// Initialize, which validates the host name by default
public init(validateHost: Bool = true) {
self.validateHost = validateHost
}
public func evaluate(_ trust: SecTrust.forHost host: String) throws {
// Call two different validation extensions based on whether the host name is validated or not
if validateHost {
try trust.af.performValidation(forHost: host)
}
try trust.af.performDefaultValidation(forHost: host)
}
}
Copy the code
Revocation the certificate verifier
You can use the default security policy to check whether the certificate is revoked. Alamofire’s previous tests found that apple only supports certificate revocation checks in iOS10.1
public final class RevocationTrustEvaluator: ServerTrustEvaluating {
// Encapsulate CFOptionFlags to create a security policy for revocation certificate verification
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
}
}
// Whether the default security check is required. The default is true
private let performDefaultValidation: Bool
// Whether host name authentication is required. The default value is true
private let validateHost: Bool
Options used to create a security policy for revocation certificate verification. The default is
private let options: Options
public init(performDefaultValidation: Bool = true.validateHost: Bool = true.options: Options = .any) {
self.performDefaultValidation = performDefaultValidation
self.validateHost = validateHost
self.options = options
}
// Implement the protocol verification method
public func evaluate(_ trust: SecTrust.forHost host: String) throws {
if performDefaultValidation {
// The default validation is required. The default validation is performed first when calling the method
try trust.af.performDefaultValidation(forHost: host)
}
if validateHost {
// The host name needs to be verified
try trust.af.performValidation(forHost: host)
}
// You need to use the revocation certificate validation security policy to evaluate. IOS12 uses different methods to verify
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))
}
}
}
}
Copy the code
Custom certificate verifier
You can use the custom certificate built in the APP to verify the server certificate, which can be used to verify the self-signed certificate.
public final class PinnedCertificatesTrustEvaluator: ServerTrustEvaluating {
// Save the custom certificates, which are all valid certificates in the app by default
private let certificates: [SecCertificate]
// Whether to add a custom certificate to the verification anchor certificate, which is used to verify self-signed certificates. The default value is false
private let acceptSelfSignedCertificates: Bool
// Whether the default validation is required. The default is true
private let performDefaultValidation: Bool
// Whether to verify the host name. Default: 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 {
// If the custom certificate is empty, throw an error
throw AFError.serverTrustEvaluationFailed(reason: .noCertificatesFound)
}
if acceptSelfSignedCertificates {
// Add the array of custom certificates to SecTrust if you need to verify self-signed certificates
try trust.af.setAnchorCertificates(certificates)
}
if performDefaultValidation {
// Perform default validation
try trust.af.performDefaultValidation(forHost: host)
}
if validateHost {
// Perform host name authentication
try trust.af.performValidation(forHost: host)
}
// Obtain the server certificate from the verification result
let serverCertificatesData = Set(trust.af.certificateData)
// Obtain a custom certificate
let pinnedCertificatesData = Set(certificates.af.data)
// Whether the server certificate matches the custom certificate (by determining the intersection of the two sets)
let pinnedCertificatesInServerData = !serverCertificatesData.isDisjoint(with: pinnedCertificatesData)
if !pinnedCertificatesInServerData {
// If there is no match, the verification fails and an error is thrown
throw AFError.serverTrustEvaluationFailed(reason: .certificatePinningFailed(host: host,
trust: trust,
pinnedCertificates: certificates,
serverCertificates: trust.af.certificates))
}
}
}
Copy the code
Public key validator
Using the custom public key to verify the server certificate, note that the custom certificate is not added to the SecTrust anchor certificate, so using this validator to verify the self-signed certificate will fail. Therefore, if you want to verify a self-signed certificate, use the custom certificate validator above
public final class PublicKeysTrustEvaluator: ServerTrustEvaluating {
// Customize the public key array. The default is the public key available in all app built-in certificates
private let keys: [SecKey]
// Whether to perform the default validation, which defaults to true
private let performDefaultValidation: Bool
// Whether to verify the host name. Default: 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 an exception if the certificate is empty
throw AFError.serverTrustEvaluationFailed(reason: .noPublicKeysFound)
}
if performDefaultValidation {
// Perform default validation
try trust.af.performDefaultValidation(forHost: host)
}
if validateHost {
// Verify the host name
try trust.af.performValidation(forHost: host)
}
// After the default verification is successful, check whether the user-defined public key exists in the public key of the server certificate
let pinnedKeysInServerKeys: Bool = {
// Iterate over the user-defined public key array and the public key array of the server certificate one by one. If the same pair exists, the verification succeeds
for serverPublicKey in trust.af.publicKeys {
for pinnedPublicKey in keys {
if serverPublicKey = = pinnedPublicKey {
return true}}}return false} ()if !pinnedKeysInServerKeys {
// The public key matches the event, throwing an error
throw AFError.serverTrustEvaluationFailed(reason: .publicKeyPinningFailed(host: host,
trust: trust,
pinnedKeys: keys,
serverKeys: trust.af.publicKeys))
}
}
}
Copy the code
Combination validator
Initialization holds an array of validators. During the verification, validators in the array are verified one by one. The verification succeeds only when all validators succeed
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 {
// Simply call the extended validator array method to check each of the held arrays. Any failure will throw an error
try evaluators.evaluate(trust, forHost: host)
}
}
Copy the code
Verifier for testing
The validation method is null. It does not perform any validation on the server. It is only used for development purposes. The old version is called DisabledEvaluator, and the new version is called 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 {}
}
Copy the code
conclusion
The above is the encapsulation of Alamofire for server verification. When the request needs to verify the server, it will obtain the corresponding verifier protocol object through ServerTrustManager to verify the SecTrust. ServerTrustManager is not aware of the internal implementation principle of the validator. After using the interface decoupling, the ServerTrustManager can choose to use the validator freely or implement more complex validators to process the business logic.
Authentication processing
Introduction to the
HTTP requests are stateless, so you need to use a token to indicate which user a request belongs to. There are many ways to mark the user’s state. The default way is to create a session in the background after login to mark the user, and send a cookie in the response header back to the request end. Each subsequent request from the requester carries the cookie with it, allowing the server to identify where the request came from. However, session sessions will expire, and the requestor needs to refresh the session in time to obtain new cookies or refresh the cookie validity period. OAuth2 also needs to obtain the token from the single sign-on party and write the token into the request to be sent to the server. Alamofire use RequestInterceptor request interceptor to encapsulate the AuthenticationInterceptor authentication interceptors, and use the interface to abstract the verifier and authentication credentials, interceptor only responsible for using interceptor request, And relevant methods to use validation agreement authentication credentials into the request, after receiving the server returned 401 authentication fails, notify the verifier to refresh operations such as authentication credentials, use the party you just need to focus on verification and validation documents, which need not to care about the complex logic validation logic and refresh.
AuthenticationCredential
A validation certificate is something that needs to be injected into the request. The location of the injection is determined by the verifier. The validation certificate itself only needs to tell the validation interceptor whether it needs to refresh.
public protocol AuthenticationCredential {
// Whether the credentials need to be refreshed. If false is returned, the Authenticator interface object below calls the refresh method to refresh the credentials
For example, if the credential is valid for 60 minutes, it is better to return true 5 minutes before the expiration date to refresh the credential, so that the credential will not expire on subsequent requests
var requiresRefresh: Bool { get}}Copy the code
Verify the Authenticator
It has the following functions:
- Inject the credentials into the request
- Refreshes credentials and returns a new credential or error
- When the server returns a 401, if it’s OAuth2’s 401, do you need a verifier to verify that the 401 error was returned by the content side? It is returned by the single sign-on party (if it is returned by the single sign-on party, it indicates a verification failure and the interceptor will not retry the request. If it is returned by the content party, it indicates that the credentials need to be refreshed. The interceptor will ask the verifier to refresh the credentials and then request again.)
- If the request fails, it tells the interceptor whether the server authenticated (if yes, the interceptor retries the request directly; if not, the interceptor tells the verifier to refresh the credentials and re-request).
public protocol Authenticator: AnyObject {
// Authentication credential generics
associatedtype Credential: AuthenticationCredential
// Apply the credentials to the request
// For example, OAuth2 authentication adds the Access token from the credential to the request header
func apply(_ credential: Credential.to urlRequest: inout URLRequest)
// Refresh the credentials. The completion closure is an escape closure
// The refresh method can be called in two ways:
// 1. When the request is ready to be sent, if the credentials need to be flushed, the interceptor will call the validator's refresh method to refresh the credentials before issuing the request
// 2. When the response fails, the interceptor will ask the verifier if the authentication has failed. If so, the refresh method will be called and the request will be retried
// If OAuth2 is used, there will be a disagreement as to whether the authentication request came from the content server. Is it from the authentication server? If it comes from the content server, all it needs is for the verifier to refresh the credentials and the interceptor to retry the request. If it comes from the authentication server, it needs to throw an error and let the user log in again.
// If OAuth2 is used, you need to negotiate with the background partner to distinguish the two authentication cases. The interceptor will determine whether the authentication has failed based on the next method.
func refresh(_ credential: Credential.for session: Session.completion: @escaping (Result<Credential.Error- > >)Void)
// Check whether the request failed because of the authentication server. This method simply returns false if the credentials issued by the authentication server are not invalidated.
// If the credentials issued by the authentication server are invalid, determine where the authentication request came from when the request encountered an error such as 401. If it comes from a content server, you need the verifier to refresh the credentials and retry the request. If it comes from an authentication server, you need to throw an error to get the user to log in again. Specific how to judge, need to consult with background development small partner
// So if the protocol method returns true, the interceptor will not retry the request, but simply throw an error
// If this method returns false, the interceptor will determine whether the credential is valid by using the following method. If it is valid, the interceptor will retry directly. If it is not valid, the validator will first refresh the credential and retry again
func didRequest(_ urlRequest: URLRequest.with response: HTTPURLResponse.failDueToAuthenticationError error: Error) -> Bool
// If the request fails, determine whether the request was authenticated by the current credential
// This method simply returns true if the certificate issued by the server is not invalidated
/* This is the case if the credentials issued by the authentication server are invalid: Credential A is still in the validity period, but has been marked as invalid by the authentication server, then the first request response after the invalidity will trigger the refresh logic. During the refresh process, there will be A series of requests authenticated by credential A that have not fallen to the ground, so when the response is triggered, According to this method, we need to check whether the request is authenticated by the current certificate. If so, the retry callback needs to be provisioned and executed after the credentials have been refreshed. If not, it means that the current certificate may already be the new certificate B, so directly retry the request */
func isRequest(_ urlRequest: URLRequest.authenticatedWith credential: Credential) -> Bool
}
Copy the code
Authentication failure error definition
Two errors are defined:
- Voucher lost
- Too many flushes
public enum AuthenticationError: Error {
// The certificate is missing
case missingCredential
// Refreshed credentials too many times during the refresh window
case excessiveRefresh
}
Copy the code
Authentication AuthenticationInterceptor interceptor
- The RequestInterceptor protocol is implemented to intercept and retry requests
- It holds a verifier object, which is used to intercept the request and let the verifier inject the credentials into the request. At the same time, if the credentials expire during the injection, the verifier will first refresh the credentials and then send them in the injection request. When a request fails, a verifier is used to determine whether the credentials need to be refreshed to retry the request.
- It also has a time window object that defines the maximum number of times a credentialcan be refreshed in seconds before the request fails.
define
The type of the verifier held is declared using the generic constraint
public class AuthenticationInterceptor<AuthenticatorType> :RequestInterceptor where AuthenticatorType: Authenticator// Or write:public class AuthenticationInterceptor<AuthenticatorType: Authenticator> :RequestInterceptor
Copy the code
Internal type definition
Internal types are defined to handle internal processing logic
/// Certificate alias
public typealias Credential = AuthenticatorType.Credential
// MARK: Helper Types
// Refresh the window to limit the maximum number of refreshes within a specified period
// The interceptor holds the timestamp for each flush. On each flush, it checks whether the number of flush-times in the most recent period has exceeded the maximum number of flush-times. If so, it cancles the flush and throws an error
public struct RefreshWindow {
// Limit the period, default 30s
public let interval: TimeInterval
// The maximum number of refreshes in a period. The default is 5
public let maximumAttempts: Int
public init(interval: TimeInterval = 30.0.maximumAttempts: Int = 5) {
self.interval = interval
self.maximumAttempts = maximumAttempts
}
}
// When the interception request is ready to adapt the request, if the credentials need to be refreshed, the parameters of the adaptation method will be encapsulated into the structure and stored in the interceptor temporarily. After the refreshing is complete, completion will be called one by one for all the saved structures to send the request
private struct AdaptOperation {
let urlRequest: URLRequest
let session: Session
let completion: (Result<URLRequest.Error- > >)Void
}
// Intercepts the result of the adaptation request. The interceptor executes different logic according to different adaptation results
private enum AdaptResult {
// After the adaptation is complete and the credentials are obtained, it is time for the verifier to inject the credentials into the request
case adapt(Credential)
// The request will be cancelled if the authentication fails, the credentials are lost, or the refresh times are too many
case doNotAdapt(AuthenticationError)
// Credential refresh is going on, and the parameters of the adaptation method are wrapped into the above structure for temporary storage, and the execution will continue after credential refresh
case adaptDeferred
}
// The mutable state will use the @protected modifier to ensure thread safety
private struct MutableState {
// Credentials, which may be empty
var credential: Credential?
// Whether credentials are being refreshed
var isRefreshing = false
// Refresh the timestamp of the credential
var refreshTimestamps: [TimeInterval] = []
// Hold the refresh limit window
var refreshWindow: RefreshWindow?
// The parameters related to the temporary adaptation request
var adaptOperations: [AdaptOperation] = []
// The completion closure of the staged retry request. When the interceptor retries the failed request, if it finds that the credentials need to be flushed, it will temporarily store the completed closure, then let the verisher refresh the credentials, and then iterate through the array one by one, executing the retry logic
var requestsToRetry: [(RetryResult) - >Void] =[]}Copy the code
attribute
There are only four properties, one of which is a computed property because it puts several states that need to be thread-safe in MutableState
// Credential, which reads and writes directly from the mutableState thread safe,
public var credential: Credential? {
get { mutableState.credential }
set { mutableState.credential = newValue }
}
/ / verification
let authenticator: AuthenticatorType
// Refresh the queue of credentials
let queue = DispatchQueue(label: "org.alamofire.authentication.inspector")
// Thread safe state objects
@Protected
private var mutableState = MutableState(a)Copy the code
The method to implement the interceptor protocol
The interceptor protocol has two methods:
- Adapt method, which intercepts the request before it is sent, ADAPTS the request, sends a new request when it succeeds, cancles the request if it fails, and throws an error. The interceptor uses the authenticator in this method to refresh and inject the credentials.
- The retry method is used to return retry logic to the Session to resend the request if the request fails. In this method, the interceptor uses the authenticator to validate the request and the corresponding credentials, refreshing the request if necessary.
// Adaptation request
public func adapt(_ urlRequest: URLRequest.for session: Session.completion: @escaping (Result<URLRequest.Error- > >)Void) {
// To obtain the adaptation result, ensure thread safety
let adaptResult: AdaptResult = $mutableState.write { mutableState in
// Check whether the credentials are already being refreshed
guard !mutableState.isRefreshing else {
// While the credentials are being flushed, store all the parameters in adaptOperations and wait for the flush to complete before processing the parameters
let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
mutableState.adaptOperations.append(operation)
// Return adaptation delay
return .adaptDeferred
}
// Do not refresh credentials again, continue to adapt
// Obtain the credentials
guard let credential = mutableState.credential else {
// The credentials are missing, return error
let error = AuthenticationError.missingCredential
return .doNotAdapt(error)
}
// Check whether the certificate is valid
guard !credential.requiresRefresh else {
// The credentials have expired and need to be refreshed
let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
mutableState.adaptOperations.append(operation)
// Call the refresh credential method
refresh(credential, for: session, insideLock: &mutableState)
// Return adaptation delay
return .adaptDeferred
}
// If the credential is valid, a message is displayed indicating successful adaptation
return .adapt(credential)
}
// Process the adaptation result
switch adaptResult {
case let .adapt(credential):
// If the adaptation succeeds, let the verifier inject the credentials into the request
var authenticatedRequest = urlRequest
authenticator.apply(credential, to: &authenticatedRequest)
// Call the completion callback and return the adapted request
completion(.success(authenticatedRequest))
case let .doNotAdapt(adaptError):
// Adaptation failed and the call completion callback threw an error
completion(.failure(adaptError))
case .adaptDeferred:
// The adaptation will be delayed and no processing will be done. After the credential is refreshed, the completion in the temporary parameters will be used to continue processing
break}}// MARK: Retry
// Request retry
public func retry(_ request: Request.for session: Session.dueTo error: Error.completion: @escaping (RetryResult) - >Void) {
// If there is no URL request or response, do not retry
guard let urlRequest = request.request, let response = request.response else {
completion(.doNotRetry)
return
}
// Ask the verifier if the authentication server failed (OAuth2)
guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
// If the authentication fails, the server does not retry and returns an error, requiring the user to log in again
completion(.doNotRetry)
return
}
// Whether the credential exists
guard let credential = credential else {
// The credentials are lost
let error = AuthenticationError.missingCredential
completion(.doNotRetryWithError(error))
return
}
// Ask the verifier if the request is authenticated by the current credentials
guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else {
// If the request is not authenticated by the current credential, it indicates that the credential is new and retry directly
completion(.retry)
return
}
Otherwise, the current credential is invalid. Refresh the credential and try again
$mutableState.write { mutableState in
// Hold the completion callback
mutableState.requestsToRetry.append(completion)
// If you are refreshing credentials, return
guard !mutableState.isRefreshing else { return }
// No credentials are currently refreshed, so call refresh to start refreshing
refresh(credential, for: session, insideLock: &mutableState)
}
}
Copy the code
Private refresh credential method
The interceptor’s core method, used to refresh credentials using the authenticator protocol object, can:
- Check the maximum number of refreshes before refreshing
- The refresh timestamp is saved after the request for the next refresh to check the number of times
- The staged request adaptation result callback is processed after refresh
- The staged request retry result callback is processed after refresh
private func refresh(_ credential: Credential.for session: Session.insideLock mutableState: inout MutableState) {
// Check whether the maximum number of refresh times is exceeded
guard !isRefreshExcessive(insideLock: &mutableState) else {
// The maximum number of refresh times is exceeded
let error = AuthenticationError.excessiveRefresh
handleRefreshFailure(error, insideLock: &mutableState)
return
}
// Save the refresh timestamp
mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime)
// The tag is refreshing
mutableState.isRefreshing = true
// Call the validator's refresh method asynchronously in the queue. Because the interceptor is locked before calling the refresh method, the following asynchronously executes to break the lock and ensure that the refresh behavior will be synchronized.
queue.async {
self.authenticator.refresh(credential, for: session) { result in
// Refresh to complete the callback
self.$mutableState.write { mutableState in
switch result {
case let .success(credential):
// Processing succeeded
self.handleRefreshSuccess(credential, insideLock: &mutableState)
case let .failure(error):
// Failure handling
self.handleRefreshFailure(error, insideLock: &mutableState)
}
}
}
}
}
// Check whether the maximum number of refresh times is exceeded
private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
// Get the time window object first. If not, there is no limit on the maximum number of times
guard let refreshWindow = mutableState.refreshWindow else { return false }
// The minimum timestamp of the time window
let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval
// Iterate over the saved refresh timestamp and use Reduce to calculate the number of refreshes in the next window
let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in
guard refreshWindowMin < = refreshTimestamp else { return }
attempts + = 1
}
// Whether the maximum number of refreshes has been exceeded
let isRefreshExcessive = refreshAttemptsWithinWindow > = refreshWindow.maximumAttempts
return isRefreshExcessive
}
// Processing the refresh succeeded
private func handleRefreshSuccess(_ credential: Credential.insideLock mutableState: inout MutableState) {
// Save the certificate
mutableState.credential = credential
// Fetch an array of parameters for the adaptation request
let adaptOperations = mutableState.adaptOperations
// Fetch the staged retry callback array
let requestsToRetry = mutableState.requestsToRetry
// Empty the temporary data held by self
mutableState.adaptOperations.removeAll()
mutableState.requestsToRetry.removeAll()
// Turn off the status in refresh
mutableState.isRefreshing = false
// Execute asynchronously in the queue to break the lock
queue.async {
// Continue the request adaptation logic one by one
adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) }
Retry blocks are executed one by one to start retry
requestsToRetry.forEach { $0(.retry) }
}
}
// Failed to process the refresh
private func handleRefreshFailure(_ error: Error.insideLock mutableState: inout MutableState) {
// Fetch the two temporary arrays
let adaptOperations = mutableState.adaptOperations
let requestsToRetry = mutableState.requestsToRetry
// Empty the temporary array held by self
mutableState.adaptOperations.removeAll()
mutableState.requestsToRetry.removeAll()
// Turn off the refresh status
mutableState.isRefreshing = false
// Execute asynchronously in the queue to break the lock
queue.async {
// The adapter fails to call one by one
adaptOperations.forEach { $0.completion(.failure(error)) }
// The rereiter also failed to call one after another
requestsToRetry.forEach { $0(.doNotRetryWithError(error)) }
}
}
Copy the code