咱们在之前分析拦截器的文章中提到,Alamofire中实现了一些比较常用的拦截器。AuthenticationInterceptor肯定是满分(我打的分)实现之一。今日一同来拜读一下。

AuthenticationInterceptor相似的还有RetryPolicy,也可谓精辟。详细内容放在下篇展开,敬请期待。

面临的问题

在实践的项目中咱们经常遇到的问题是:部分API是需求授权之后才能够拜访。例如:咱们获取用户信息的接口api.xx.com/users/id,需求在恳求头中增加Authorization: Bearer accessToken以完成授权,不然服务器会回来401回绝咱们拜访。这个accessToken会有过期时刻,过期后咱们需求从头获取,一般是经过登陆接口回来。后来为了削减用户登录频率,和accessToken一同回来的还有refreshToken,它的有用期会比accessToken稍长,能够运用它来对accessToken进行改写,就能够避免用户登录操作。

这儿触及OAuth2.0以及JWT相关布景知识,不了解的同学自行解决哈。

那么关于上面的需求,咱们客户端需求做的有哪些呢?详细如下:

  1. 获取accessTokenrefreshToken
  2. 在后续需求授权的接口中增加恳求头
  3. accessToken过期后,运用refreshToken进行改写
  4. 改写accessToken失利时,需求用户登录从头授权。

那么Alamofire为咱们做了哪些?继续看

怎么解决

首要,咱们能够界说一个自己的凭据(也便是后续需求用到的认证信息):

struct OAuthCredential: AuthenticationCredential {
    let accessToken: String
    let refreshToken: String
    let userID: String
    let expiration: Date
    // 这儿咱们在有用期行将过期的5分钟回来需求改写
    var requiresRefresh: Bool { Date(timeIntervalSinceNow: 60 * 5) > expiration }
}

其次,咱们再实现一个自己的授权中心:

class OAuthAuthenticator: Authenticator {
    /// 增加header
    func apply(_ credential: OAuthCredential, to urlRequest: inout URLRequest) {
        urlRequest.headers.add(.authorization(bearerToken: credential.accessToken))
    }
    /// 实现改写流程
    func refresh(_ credential: OAuthCredential,
                 for session: Session,
                 completion: @escaping (Result<OAuthCredential, Error>) -> Void) {
    }
    func didRequest(_ urlRequest: URLRequest,
                    with response: HTTPURLResponse,
                    failDueToAuthenticationError error: Error) -> Bool {
        return response.statusCode == 401
    }
    func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: OAuthCredential) -> Bool {
        let bearerToken = HTTPHeader.authorization(bearerToken: credential.accessToken).value
        return urlRequest.headers["Authorization"] == bearerToken
    }
}

之后,咱们就能够运用结构内部的AuthenticationInterceptor了:

// 生成授权凭据。用户没有登陆时,能够不生成。
let credential = OAuthCredential(accessToken: "a0",
                                 refreshToken: "r0",
                                 userID: "u0",
                                 expiration: Date(timeIntervalSinceNow: 60 * 60))
// 生成授权中心
let authenticator = OAuthAuthenticator()
// 运用授权中心和凭据(若没有能够不传)装备拦截器
let interceptor = AuthenticationInterceptor(authenticator: authenticator,
                                            credential: credential)
// 将拦截器装备在Session上或在单独的Request中运用
let session = Session()
let urlRequest = URLRequest(url: URL(string: "https://api.example.com/example/user")!)
session.request(urlRequest, interceptor: interceptor)

能够看到,运用上面的方法,咱们只需关心怎么获取accessTokenrefreshToken,以及在refreshToken也失效时触发用户从头登录授权。能够说,咱们自己的工作少到了极致。自己写的少就意味这bug少,特别是改写token这一块,什么时候应该改写accessToken、怎么控制过度改写这些繁琐的部分咱们都无需关心了。

怎么做到的

知道了怎么做,或许你还会一头雾水。为什么需求界说那两个数据结构?这一部分为你解答。

AuthenticationCredential

它代表授权凭据,这个协议的界说很简单:

/// 授权凭据,能够运用它对URLRequest进行授权。
/// 例如:在OAuth2授权系统中,凭据包括accessToken,它能够对一个用户的一切恳求进行授权。
/// 通常状况下,该accessToken有用时长为60分钟;在过期前后(一段时刻内)能够运用refreshToken对accessToken进行改写。
public protocol AuthenticationCredential {
    /// 授权凭据是否需求改写。
    /// 在凭据在行将过期或过期后,应该回来true。
    /// 例如,accessToken的有用期为60分钟,在凭据行将过期的5分钟应该回来true,保证accessToken得到改写。
    var requiresRefresh: Bool { get }
}

该协议只关心这个凭据是否需求改写。关于不同的授权方法,需求的元信息也不相同,结构无法也无需知道这些细节。

Authenticator

正因为AuthenticationCredential或许五花八门,这儿需求一个知道怎么运用它的角色。Authenticator就来了。该协议的实现细节比较多,我现已写在注释里了。

/// 授权中心,能够运用凭据(AuthenticationCredential)对URLRequest授权;也能够办理token的改写。
public protocol Authenticator: AnyObject {
    /// 该授权中心运用的凭据类型
    associatedtype Credential: AuthenticationCredential
    /// 运用凭据对恳求进行授权。
    /// 例如:在OAuth2系统中,应该设置恳求头 [ "Authorization": "Bearer accessToken" ]
    func apply(_ credential: Credential, to urlRequest: inout URLRequest)
    /// 改写凭据,并经过completion回调成果。
    /// 在下面两种状况下,会履行改写:
    /// 1. 适配过程中 - 对应 拦截器的 adapt(_:for:completion:) 方法
    /// 2. 重试过程中 - 对应拦截器的 retry(_:for:dueTo:completion:)方法
    ///
    /// 例如:在OAuth2系统中,应该在该方法中运用refreshToken去改写accessToken,完成后在回调中回来新的凭据。
    /// 若改写恳求被回绝(状况码401),refreshToken不该该再运用,此时应该要求用户从头授权。
    func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)
    /// 判别URLRequest失利是否因为授权问题。
    /// 若授权服务器不支持对现已收效的凭据进行吊销(也便是说凭据永久有用)应该回来false。不然应该根据详细状况判别。
    /// 例如:在OAuth2系统中, 能够运用状况码401代表授权失利,此时应该回来true。
    /// 注意:上面只是一般状况,你应该根据你所处的系统详细判别。
    func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool
    /// 判别URLRequest是否运用凭据进行了授权。
    /// 若授权服务器不支持对现已收效的凭据进行吊销(也便是说凭据永久有用)应该回来true。不然应该根据详细状况判别。
    /// 例如:在OAuth2系统中,  能够比照`URLRequest中header的授权字段Authorization的值` 和 `Credential中的token`;
    /// 若他们相等,回来true,不然回来false
    func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool
}

AuthenticationInterceptor

为了完成授权流程,该拦截器对恳求的适配和重试都进行了实现。

Adapter

先上一个适配流程图:

Alamofire - 使用拦截器优雅的对接口进行授权

下面是相关代码,我现已加上了详细注释:

public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
    let adaptResult: AdaptResult = $mutableState.write { mutableState in
        // 适配一个URLRequest时,正在改写凭据,将此次适配记录下来,推迟履行
        guard !mutableState.isRefreshing else {
            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:
        // 凭据需求改写或正在改写, 适配需求推迟到改写完成后履行
        break
    }
}

其间的改写流程,比就有意思。触及到改写窗口的概念。简单讲便是必定的时刻范围。在这个范围内,还能够设置一个最大的改写次数。在正式改写之前,会判别改写条件是否满足窗口设定。详细如下:

/// 判别是否过度改写
private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
    // refreshWindow是判别过度改写的参考,没有refreshWindow时阐明不限制改写
    guard let refreshWindow = mutableState.refreshWindow else { return false }
    // 计算可改写的时刻点
    let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval
    // 统计在可改写时刻点之前的改写次数
    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 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)
                }
            }
        }
    }
}

Retrier

还是先看流程图:

Alamofire - 使用拦截器优雅的对接口进行授权

这儿会判别是否和授权有关,无关的就不会重试。另外,若当时最新凭据没有运用,会进入重试流程。最后的改写是因为:已然需求授权,也存在凭据,也授权过了,还进入了重试那就阐明凭据过期了。下面是详细代码:

public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
    // 没有原始恳求或没有收到服务器的呼应,无需重试
    guard let urlRequest = request.request, let response = request.response else {
        completion(.doNotRetry)
        return
    }
    // 不是因为授权原因失利的,无需重试
    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(credential, for: session, insideLock: &mutableState)
    }
}

到这儿,整个流程也就清晰了。更详细的,能够参考GitHub

总结

今日咱们从详细问题出发,先了解了怎么运用Alamofire去解决该问题,然后又分析了AuthenticationInterceptor的详细实现,它是怎么解决该问题的。最后,只能说Alamofire真是太细了。