首页 > 解决方案 > 在使用 AlamoFire 的 RequestInterceptor 类时,如何保证一次只运行一次重试?

问题描述

我正在实现一种使用 OAuth2 和 AlamoFire5 刷新会话令牌的方法,并且我正在尝试弄清楚如何解决这种情况:

1 - 当某个请求失败时,必须启动一个 refreshToken 请求,这必须是一次运行的唯一一个 refreshToken 请求。即在该请求完成之前,不应重试其他失败的请求。

2 - 如果 refreshToken 以错误结束,则应用程序必须重新启动,并且必须取消所有其他正在等待的请求。

3 - 如果 refreshToken 请求成功,则必须更新令牌,并且所有其他等待的请求现在必须继续。

我正在使用 AlamoFire 的 RequestInterceptor 类来尝试解决这个问题,到目前为止我的实现是这样的:

final class RequestInterceptor: Alamofire.RequestInterceptor {
    
    private let disposeBag = DisposeBag()
    private let lock = NSRecursiveLock()

    private var refreshTokenParameters: TokenParameters {
        TokenParameters(clientId: "pdappclient",
                grantType: "refresh_token",
                refreshToken: KeychainManager.shared.refreshToken)
    }
    
    private let storage: AccessTokenStorage

    init(storage: AccessTokenStorage) {
        self.storage = storage
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var urlRequest = urlRequest

        urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")

        completion(.success(urlRequest))
    }

    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        lock.lock()
        defer { lock.unlock() }
        
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            return completion(.doNotRetryWithError(error))
        }
        
        let refreshTokenRequest: Single<TokenResponse> = NetworkManager.shared
            .fetchData(fromApi: IdentityServerAPI.token(parameters: self.refreshTokenParameters))

        refreshTokenRequest.subscribe(onSuccess: { token in
            self.lock.unlock()
            self.storage.accessToken = token.accessToken ?? ""
            completion(.retry)
        }, onError: { error in
            self.lock.unlock()
            completion(.doNotRetryWithError(error))
        }).disposed(by: disposeBag)
    }
}

如何使用 RequestInterceptor 解决这种情况?

标签: swiftalamofire

解决方案


您可以使用一个数组来存储重试闭包,用于在令牌刷新完成之前可能发生的请求,并使用一个布尔值来知道刷新操作正在进行中。

你最终会得到这样的东西:

final class RequestInterceptor: Alamofire.RequestInterceptor {
    
    private let disposeBag = DisposeBag()
    private let lock = NSRecursiveLock()

    private var refreshTokenParameters: TokenParameters {
        TokenParameters(
            clientId: "pdappclient",
            grantType: "refresh_token",
            refreshToken: KeychainManager.shared.refreshToken
        )
    }
    
    private let storage: AccessTokenStorage
    
    private var retryQueue = [(RetryResult) -> Void]()
    private var isTokenRefreshing = false

    init(storage: AccessTokenStorage) {
        self.storage = storage
    }

    func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
        var urlRequest = urlRequest

        urlRequest.setValue("Bearer " + storage.accessToken, forHTTPHeaderField: "Authorization")

        completion(.success(urlRequest))
    }

    func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
        lock.lock()
        defer { lock.unlock() }
        
        guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else {
            completion(.doNotRetryWithError(error))
            return
        }
        retryQueue.append(completion)
        
        if !isTokenRefreshing {
            isTokenRefreshing = true

            let refreshTokenRequest: Single<TokenResponse> = NetworkManager.shared
                .fetchData(fromApi: IdentityServerAPI.token(parameters: self.refreshTokenParameters))

            refreshTokenRequest.subscribe(onSuccess: { token in
                self.lock.lock()
                defer { self.lock.unlock() }
                
                self.storage.accessToken = token.accessToken ?? ""
                
                self.retryQueue.forEach { $0(.retry) }
                self.retryQueue.removeAll()
                
                self.isTokenRefreshing = false
            }, onError: { error in
                self.lock.lock()
                defer { self.lock.unlock() }
                
                self.retryQueue.forEach { $0(.doNotRetryWithError(error)) }
                self.retryQueue.removeAll()
                
                self.isTokenRefreshing = false
            }).disposed(by: disposeBag)
        }
    }
}

请注意,正如延迟文档所述:

语句用于在将defer程序控制转移到 defer 语句出现的范围之外之前执行代码。

所以,第一条语句的闭包defer会在闭包之前执行。onSuccessonError

这就是为什么我们需要再次锁定源内部onSuccessonError闭包。


推荐阅读