【问题标题】:How to synchronously refresh an access token using Alamofire + RxSwift如何使用 Alamofire + RxSwift 同步刷新访问令牌
【发布时间】:2021-01-10 22:52:58
【问题描述】:

我的 NetworkManager 类中有这个通用的fetchData() 函数,它能够请求向网络发出授权请求,如果它失败(在多次重试后)发出一个错误,将重新启动我的应用程序(请求一个新的登录)。我需要同步调用这个重试令牌,我的意思是,如果多个请求失败,那么一次应该只有一个请求刷新令牌。如果那个失败,另一个请求必须被丢弃。我已经尝试过使用 DispatchGroup / NSRecursiveLock / 以及调用下面描述的函数 cancelRequests 的方法(在这种情况下,任务计数始终为 0)。我怎样才能使这种行为在这种情况下起作用?

  • 我的 NetworkManager 类:

    public func fetchData<Type: Decodable>(fromApi api: TargetType,
                                           decodeFromKeyPath keyPath: String? = nil) -> Single<Response> {
        
        let request = MultiTarget(api)

        return provider.rx.request(request)
                .asRetriableAuthenticated(target: request)
    }

    func cancelAllRequests(){
        if #available(iOS 9.0, *) {
            DefaultAlamofireManager
                .sharedManager
                .session
                .getAllTasks { (tasks) in
                tasks.forEach{ $0.cancel() }
            }
        } else {
            DefaultAlamofireManager
                .sharedManager
                .session
                .getTasksWithCompletionHandler { (sessionDataTask, uploadData, downloadData) in
                    
                sessionDataTask.forEach { $0.cancel() }
                uploadData.forEach { $0.cancel() }
                downloadData.forEach { $0.cancel() }
            }
        }
    }

  • 使重试有效的单一扩展:

public extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
    
    private var refreshTokenParameters: TokenParameters {
        TokenParameters(clientId: "pdappclient",
                grantType: "refresh_token",
                refreshToken: KeychainManager.shared.refreshToken)
    }

    func retryWithToken(target: MultiTarget) -> Single<E> {
        self.catchError { error -> Single<Response> in
                    if case Moya.MoyaError.statusCode(let response) = error {
                        if self.isTokenExpiredError(error) {
                            return Single.error(error)
                        } else {
                            return self.parseError(response: response)
                        }
                    }
                    return Single.error(error)
                }
                .retryToken(target: target)
                .catchError { error -> Single<Response> in
                    if case Moya.MoyaError.statusCode(let response) = error {
                        return self.parseError(response: response)
                    }
                    return Single.error(InvalidGrantException())
                }
    }

    private func retryToken(target: MultiTarget) -> Single<E> {
        let maxRetries = 1
        return self.retryWhen({ error in
            error
                    .enumerated()
                    .flatMap { (attempt, error) -> Observable<Int> in
                        if attempt >= maxRetries {
                            return Observable.error(error)
                        }
                        if self.isTokenExpiredError(error) {
                            return Observable<Int>.just(attempt + 1)
                        }
                        return Observable.error(error)
                    }
                    .flatMap { _ -> Single<TokenResponse> in
                        self.refreshTokenRequest()
                    }
                    .share()
                    .asObservable()
        })
    }
    
    private func refreshTokenRequest() -> Single<TokenResponse> {
        return NetworkManager.shared.fetchData(fromApi: IdentityServerAPI
            .token(parameters: self.refreshTokenParameters)).do(onSuccess: { tokenResponse in
                    
            KeychainManager.shared.accessToken = tokenResponse.accessToken
            KeychainManager.shared.refreshToken = tokenResponse.refreshToken
        }, onError: { error in
            NetworkManager.shared.cancelAllRequests()
        })
    }

    func parseError<E>(response: Response) -> Single<E> {
        if response.statusCode == 401 {
            // TODO
        }

        let decoder = JSONDecoder()
        if let errors = try? response.map([BaseResponseError].self, atKeyPath: "errors", using: decoder,
                failsOnEmptyData: true) {
            return Single.error(BaseAPIErrorResponse(errors: errors))
        }

        return Single.error(APIError2.unknown)
    }

    func isTokenExpiredError(_ error: Error) -> Bool {
        if let moyaError = error as? MoyaError {
            switch moyaError {
            case .statusCode(let response):
                if response.statusCode != 401 {
                    return false
                } else if response.data.count == 0 {
                    return true
                }
            default:
                break
            }
        }
        return false
    }

    func filterUnauthorized() -> Single<E> {
        flatMap { (response) -> Single<E> in
            if 200...299 ~= response.statusCode {
                return Single.just(response)
            } else if response.statusCode == 404 {
                return Single.just(response)
            } else {
                return Single.error(MoyaError.statusCode(response))
            }
        }
    }

    func asRetriableAuthenticated(target: MultiTarget) -> Single<Element> {
        filterUnauthorized()
                .retryWithToken(target: target)
                .filterStatusCode()
    }

    func filterStatusCode() -> Single<E> {
        flatMap { (response) -> Single<E> in
            if 200...299 ~= response.statusCode {
                return Single.just(response)
            } else {
                return self.parseError(response: response)
            }
        }
    }
}

【问题讨论】:

  • 您在寻找反应式解决方案吗?
  • 最好。我的应用完全建立在 RxSwift 之上。但如果你可以在没有 Rx 的情况下提出建议,我也会接受这个建议。

标签: swift alamofire rx-swift moya


【解决方案1】:

这是一个 RxSwift 解决方案:RxSwift 和 Handling Invalid Tokens

只发布链接不是最好的,所以我将发布解决方案的核心:

关键是做一个类,很像ActivityMonitor类,但是处理token刷新……

public final class TokenAcquisitionService<T> {

    /// responds with the current token immediatly and emits a new token whenver a new one is aquired. You can, for example, subscribe to it in order to save the token as it's updated.
    public var token: Observable<T> {
        return _token.asObservable()
    }

    public typealias GetToken = (T) -> Observable<(response: HTTPURLResponse, data: Data)>

    /// Creates a `TokenAcquisitionService` object that will store the most recent authorization token acquired and will acquire new ones as needed.
    ///
    /// - Parameters:
    ///   - initialToken: The token the service should start with. Provide a token from storage or an empty string (object represting a missing token) if one has not been aquired yet.
    ///   - getToken: A function responsable for aquiring new tokens when needed.
    ///   - extractToken: A function that can extract a token from the data returned by `getToken`.
    public init(initialToken: T, getToken: @escaping GetToken, extractToken: @escaping (Data) throws -> T) {
        relay
            .flatMapFirst { getToken($0) }
            .map { (urlResponse) -> T in
                guard urlResponse.response.statusCode / 100 == 2 else { throw TokenAcquisitionError.refusedToken(response: urlResponse.response, data: urlResponse.data) }
                return try extractToken(urlResponse.data)
            }
            .startWith(initialToken)
            .subscribe(_token)
            .disposed(by: disposeBag)
    }

    /// Allows the token to be set imperativly if necessary.
    /// - Parameter token: The new token the service should use. It will immediatly be emitted to any subscribers to the service.
    func setToken(_ token: T) {
        lock.lock()
        _token.onNext(token)
        lock.unlock()
    }

    /// Monitors the source for `.unauthorized` error events and passes all other errors on. When an `.unauthorized` error is seen, `self` will get a new token and emit a signal that it's safe to retry the request.
    ///
    /// - Parameter source: An `Observable` (or like type) that emits errors.
    /// - Returns: A trigger that will emit when it's safe to retry the request.
    func trackErrors<O: ObservableConvertibleType>(for source: O) -> Observable<Void> where O.Element == Error {
        let lock = self.lock
        let relay = self.relay
        let error = source
            .asObservable()
            .map { error in
                guard (error as? TokenAcquisitionError) == .unauthorized else { throw error }
            }
            .flatMap { [unowned self] in  self.token }
            .do(onNext: {
                lock.lock()
                relay.onNext($0)
                lock.unlock()
            })
            .filter { _ in false }
            .map { _ in }

        return Observable.merge(token.skip(1).map { _ in }, error)
    }

    private let _token = ReplaySubject<T>.create(bufferSize: 1)
    private let relay = PublishSubject<T>()
    private let lock = NSRecursiveLock()
    private let disposeBag = DisposeBag()
}

extension ObservableConvertibleType where Element == Error {

    /// Monitors self for `.unauthorized` error events and passes all other errors on. When an `.unauthorized` error is seen, the `service` will get a new token and emit a signal that it's safe to retry the request.
    ///
    /// - Parameter service: A `TokenAcquisitionService` object that is being used to store the auth token for the request.
    /// - Returns: A trigger that will emit when it's safe to retry the request.
    public func renewToken<T>(with service: TokenAcquisitionService<T>) -> Observable<Void> {
        return service.trackErrors(for: self)
    }
}

将上述内容放入您的应用后,您只需在请求末尾添加.retryWhen { $0.renewToken(with: tokenAcquisitionService) }。如果令牌未经授权,请确保您的请求发出ResponseError.unauthorized,并且服务将处理重试。

【讨论】:

    【解决方案2】:

    我使用DispatchWorkItem 找到了我的问题的解决方案,并使用布尔值控制我的函数的入口:isTokenRefreshing。也许这不是最优雅的解决方案,但它确实有效。

    因此,在我的 NetworkManager 类中,我添加了这两个新属性:

    public var savedRequests: [DispatchWorkItem] = []
    public var isTokenRefreshing = false
    

    现在在我的 SingleTrait 扩展中,每当我输入令牌刷新方法时,我都会将布尔值 isTokenRefreshing 设置为 true。所以,如果它是真的,我不会启动另一个请求,而是简单地抛出一个 RefreshTokenProcessInProgressException 并将当前请求保存在我的 savedRequests 数组中。

    private func saveRequest(_ block: @escaping () -> Void) {
        // Save request to DispatchWorkItem array
        NetworkManager.shared.savedRequests.append( DispatchWorkItem {
            block()
        })
    }
    

    (当然,如果令牌刷新成功,您必须记住继续保存在数组中的所有已保存请求,下面的代码中还没有描述)。

    好吧,我的 SingleTrait 扩展现在是这样的:

    import Foundation
    import Moya
    import RxSwift
    import Domain
    
    public extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {
        
        private var refreshTokenParameters: TokenParameters {
            TokenParameters(clientId: "pdappclient",
                    grantType: "refresh_token",
                    refreshToken: KeychainManager.shared.refreshToken)
        }
    
        func retryWithToken(target: MultiTarget) -> Single<E> {
            return self.catchError { error -> Single<Response> in
                        if case Moya.MoyaError.statusCode(let response) = error {
                            if self.isTokenExpiredError(error) {
                                return Single.error(error)
                            } else {
                                return self.parseError(response: response)
                            }
                        }
                        return Single.error(error)
                    }
                    .retryToken(target: target)
                    .catchError { error -> Single<Response> in
                        if case Moya.MoyaError.statusCode(let response) = error {
                            return self.parseError(response: response)
                        }
                        return Single.error(error)
                    }
        }
    
        private func retryToken(target: MultiTarget) -> Single<E> {
            let maxRetries = 1
            
            return self.retryWhen({ error in
                error
                        .enumerated()
                        .flatMap { (attempt, error) -> Observable<Int> in
                            if attempt >= maxRetries {
                                return Observable.error(error)
                            }
                            if self.isTokenExpiredError(error) {
                                return Observable<Int>.just(attempt + 1)
                            }
                            return Observable.error(error)
                        }
                        .flatMapFirst { _ -> Single<TokenResponse> in
                            if NetworkManager.shared.isTokenRefreshing {
                                self.saveRequest {
                                    self.retryToken(target: target)
                                }
                                return Single.error(RefreshTokenProcessInProgressException())
                            } else {
                                return self.refreshTokenRequest()
                            }
                        }
                        .share()
                        .asObservable()
            })
        }
        
        private func refreshTokenRequest() -> Single<TokenResponse> {
            NetworkManager.shared.isTokenRefreshing = true
            
            return NetworkManager.shared.fetchData(fromApi: IdentityServerAPI
                .token(parameters: self.refreshTokenParameters))
                .do(onSuccess: { tokenResponse in
                    KeychainManager.shared.accessToken = tokenResponse.accessToken
                    KeychainManager.shared.refreshToken = tokenResponse.refreshToken
                }).catchError { error -> Single<TokenResponse> in
                    return Single.error(InvalidGrantException())
            }
        }
        
        private func saveRequest(_ block: @escaping () -> Void) {
            // Save request to DispatchWorkItem array
            NetworkManager.shared.savedRequests.append( DispatchWorkItem {
                block()
            })
        }
    
        func parseError<E>(response: Response) -> Single<E> {
            if response.statusCode == 401 {
                // TODO
            }
    
            let decoder = JSONDecoder()
            if let errors = try? response.map([BaseResponseError].self, atKeyPath: "errors", using: decoder,
                    failsOnEmptyData: true) {
                return Single.error(BaseAPIErrorResponse(errors: errors))
            }
    
            return Single.error(APIError2.unknown)
        }
    
        func isTokenExpiredError(_ error: Error) -> Bool {
            if let moyaError = error as? MoyaError {
                switch moyaError {
                case .statusCode(let response):
                    if response.statusCode != 401 {
                        return false
                    } else if response.data.count == 0 {
                        return true
                    }
                default:
                    break
                }
            }
            return false
        }
    
        func filterUnauthorized() -> Single<E> {
            flatMap { (response) -> Single<E> in
                if 200...299 ~= response.statusCode {
                    return Single.just(response)
                } else if response.statusCode == 404 {
                    return Single.just(response)
                } else {
                    return Single.error(MoyaError.statusCode(response))
                }
            }
        }
    
        func asRetriableAuthenticated(target: MultiTarget) -> Single<Element> {
            filterUnauthorized()
                    .retryWithToken(target: target)
                    .filterStatusCode()
        }
    
        func filterStatusCode() -> Single<E> {
            flatMap { (response) -> Single<E> in
                if 200...299 ~= response.statusCode {
                    return Single.just(response)
                } else {
                    return self.parseError(response: response)
                }
            }
        }
    }
    

    在我的情况下,如果令牌刷新失败,在重试 N 次后,我会重新启动应用程序。因此,每当重新启动应用程序时,我都会再次将 isTokenRefreshing 设置为 false。

    这是我找到解决此问题的方法。如果您有其他方法,请告诉我。

    【讨论】:

      猜你喜欢
      • 2020-07-12
      • 2014-09-13
      • 2019-06-29
      • 2016-09-23
      • 2016-08-13
      • 2020-02-26
      • 1970-01-01
      • 2018-09-25
      相关资源
      最近更新 更多