Moya rxswift:刷新令牌并重新启动请求

时间:2018-07-27 14:27:27

标签: ios swift rx-swift refresh-token moya

我使用的是Moya Rx swift,如果状态码为401或403,我想捕获响应,然后调用刷新令牌请求,然后再次调用/重试原始请求,为此我遵循了Link但是我做了一些调整以满足我的需求

public extension ObservableType where E == Response {

/// Tries to refresh auth token on 401 errors and retry the request.
/// If the refresh fails, the signal errors.
public func retryWithAuthIfNeeded(sessionServiceDelegate : SessionProtocol) -> Observable<E> {
    return self.retryWhen { (e: Observable<Error>) in
        return Observable
                .zip(e, Observable.range(start: 1, count: 3),resultSelector: { $1 })
                .flatMap { i in
                           return sessionServiceDelegate
                                    .getTokenObservable()?
                                    .filterSuccessfulStatusAndRedirectCodes()
                                    .mapString()
                                    .catchError {
                                        error in
                                            log.debug("ReAuth error: \(error)")
                                            if case Error.StatusCode(let response) = error {
                                                if response.statusCode == 401 || response.statusCode == 403 {
                                                    // Force logout after failed attempt
                                                    sessionServiceDelegate.doLogOut()
                                                }
                                            }
                                            return Observable.error(error)
                                    }
                                    .flatMapLatest({ responseString in
                                        sessionServiceDelegate.refreshToken(responseString: responseString)
                                        return Observable.just(responseString)
                                    })
        }}
    }
}

还有我的协议:

import RxSwift

public protocol SessionProtocol {
    func doLogOut()
    func refreshToken(responseString : String)
    func getTokenObservable() -> Observable<Response>? 
}

但是它不起作用并且代码没有编译,我得到以下信息:

  

“可观察”不能转换为“可观察<_>”

我只是在与RX-swift谈我的第一步,所以这可能很简单,但是除了要返回的类型不是我要返回的类型之外,我无法弄清楚什么是错的,但我不知道如何以及在哪里这样做。

非常感谢您的帮助,如果您有更好的主意来实现我的目标,欢迎提出建议。

预先感谢您的帮助。

3 个答案:

答案 0 :(得分:1)

您可以枚举错误并从flatMap返回String类型。如果请求成功,它将返回字符串,否则将返回可观察到的错误

public func retryWithAuthIfNeeded(sessionServiceDelegate: SessionProtocol) -> Observable<E> {
    return self.retryWhen { (error: Observable<Error>) -> Observable<String> in
        return error.enumerated().flatMap { (index, error) -> Observable<String> in
            guard let moyaError = error as? MoyaError, let response = moyaError.response, index <= 3  else {
                throw error
            }
            if response.statusCode == 401 || response.statusCode == 403 {
                // Force logout after failed attempt
                sessionServiceDelegate.doLogOut()
                return Observable.error(error)
            } else {
                return sessionServiceDelegate
                    .getTokenObservable()!
                    .filterSuccessfulStatusAndRedirectCodes()
                    .mapString()
                    .flatMapLatest { (responseString: String) -> Observable<String> in
                        sessionServiceDelegate.refreshToken(responseString: responseString)
                        return Observable.just(responseString)
                    }
            }
        }
    }

答案 1 :(得分:0)

最后,我能够通过以下操作解决此问题:

首先像这样创建一个协议(这些功能是必需的,而不是可选的)。

import RxSwift

public protocol SessionProtocol {
    func getTokenRefreshService() -> Single<Response>
    func didFailedToRefreshToken()
    func tokenDidRefresh (response : String)
}

遵循这样编写网络请求的类中的协议SessionProtocol是非常重要的:

import RxSwift

class API_Connector : SessionProtocol {
        //
        private final var apiProvider : APIsProvider<APIs>!

        required override init() {
            super.init()
            apiProvider = APIsProvider<APIs>()
        }
        // Very very important
        func getTokenRefreshService() -> Single<Response> {
             return apiProvider.rx.request(.doRefreshToken())
        }

        // Parse and save your token locally or do any thing with the new token here
        func tokenDidRefresh(response: String) {}

        // Log the user out or do anything related here
        public func didFailedToRefreshToken() {}

        func getUsers (page : Int, completion: @escaping completionHandler<Page>) {
            let _ = apiProvider.rx
                .request(.getUsers(page: String(page)))
                .filterSuccessfulStatusAndRedirectCodes()
                .refreshAuthenticationTokenIfNeeded(sessionServiceDelegate: self)
                .map(Page.self)
                .subscribe { event in
                    switch event {
                    case .success(let page) :
                        completion(.success(page))
                    case .error(let error):
                        completion(.failure(error.localizedDescription))
                    }
                }
        }

}

然后,我创建了一个返回Single<Response>的函数。

import RxSwift

extension PrimitiveSequence where TraitType == SingleTrait, ElementType == Response {

        // Tries to refresh auth token on 401 error and retry the request.
        // If the refresh fails it returns an error .
        public func refreshAuthenticationTokenIfNeeded(sessionServiceDelegate : SessionProtocol) -> Single<Response> {
            return
                // Retry and process the request if any error occurred
                self.retryWhen { responseFromFirstRequest in
                    responseFromFirstRequest.flatMap { originalRequestResponseError -> PrimitiveSequence<SingleTrait, ElementType> in
                            if let lucidErrorOfOriginalRequest : LucidMoyaError = originalRequestResponseError as? LucidMoyaError {
                            let statusCode = lucidErrorOfOriginalRequest.statusCode!
                            if statusCode == 401 {
                                // Token expired >> Call refresh token request
                                return sessionServiceDelegate
                                    .getTokenRefreshService()
                                    .filterSuccessfulStatusCodesAndProcessErrors()
                                    .catchError { tokeRefreshRequestError -> Single<Response> in
                                        // Failed to refresh token
                                        if let lucidErrorOfTokenRefreshRequest : LucidMoyaError = tokeRefreshRequestError as? LucidMoyaError {
                                            //
                                            // Logout or do any thing related
                                            sessionServiceDelegate.didFailedToRefreshToken()
                                            //
                                            return Single.error(lucidErrorOfTokenRefreshRequest)
                                        }
                                        return Single.error(tokeRefreshRequestError)
                                    }
                                    .flatMap { tokenRefreshResponseString -> Single<Response> in
                                        // Refresh token response string
                                        // Save new token locally to use with any request from now on
                                        sessionServiceDelegate.tokenDidRefresh(response: try! tokenRefreshResponseString.mapString())
                                        // Retry the original request one more time
                                        return self.retry(1)
                                }
                            }
                            else {
                                // Retuen errors other than 401 & 403 of the original request
                                return Single.error(lucidErrorOfOriginalRequest)
                            }
                        }
                        // Return any other error
                        return Single.error(originalRequestResponseError)
                    }
            }
        }
}

此函数的作用是从响应中捕获错误,然后检查状态代码。如果不是401,则它将错误返回到原始请求的onError阻止,但是如果它是401(您可以更改它以满足您的需要,但这是标准),那么它将执行刷新令牌请求。

执行刷新令牌请求后,它会检查响应。

=>如果状态码的值大于或等于400,则意味着刷新令牌请求也失败,因此将请求的结果返回到原始请求OnError块。 =>如果状态代码在200..300范围内,则意味着刷新令牌请求成功,因此它将再次重试原始请求,如果原始请求再次失败,则失败将转到OnError正常阻止。

注意:

=>在刷新令牌请求成功并返回新令牌之后,解析并保存新令牌非常重要,因此在重复原始请求时,它将使用新令牌而不是旧令牌来处理

在重复原始请求之前,将在此回调中返回令牌响应。 func tokenDidRefresh (response : String)

=>如果刷新令牌请求失败,则令牌可能已过期,因此除了将失败重定向到原始请求的onError外,您还会收到此失败回调 func didFailedToRefreshToken(),您可以使用它来通知用户会话已丢失或注销用户或其他任何信息。

=>返回执行令牌请求的函数非常重要,因为这是refreshAuthenticationTokenIfNeeded函数知道要执行刷新令牌的调用的唯一方法。

func getTokenRefreshService() -> Single<Response> {
    return apiProvider.rx.request(.doRefreshToken())
}

答案 2 :(得分:-1)

还有一个解决方案,而不是在Observable上编写扩展。 它是用纯RxSwift编写的,如果失败则返回经典错误。

The easy way to refresh session token of Auth0 with RxSwift and Moya

该解决方案的主要优势在于,它可以轻松地应用于类似于Auth0的各种服务,从而可以对移动应用中的用户进行身份验证。