使用 RxSwift 和 URLSession 处理 401 状态

Posted

技术标签:

【中文标题】使用 RxSwift 和 URLSession 处理 401 状态【英文标题】:Handling 401 status w/ RxSwift & URLSession 【发布时间】:2019-09-26 15:24:41 【问题描述】:

我目前有一个如下所示的网络客户端:

class Client<R: ResourceType> 

    let engine: ClientEngineType
    var session: URLSession

    init(engine: ClientEngineType = ClientEngine()) 
        self.engine = engine
        self.session = URLSession.shared
    

    func request<T: Codable>(_ resource: R) -> Single<T> 
        let request = URLRequest(resource: resource)

        return Single<T>.create  [weak self] single in
            guard let self = self else  return Disposables.create() 
            let response = self.session.rx.response(request: request)

            return response.subscribe(
                onNext:  response, data in

                    if let error = self.error(from: response) 
                        single(.error(error))
                        return
                    

                    do 
                        let decoder = JSONDecoder()
                        let value = try decoder.decode(T.self, from: data)
                        single(.success(value))
                     catch let error 
                        single(.error(error))
                    
            ,
                onError:  error in
                    single(.error(error))
            )
        
    

    struct StatusCodeError: LocalizedError 
        let code: Int

        var errorDescription: String? 
            return "An error occurred communicating with the server. Please try again."
        
    

    private func error(from response: URLResponse?) -> Error? 
        guard let response = response as? HTTPURLResponse else  return nil 

        let statusCode = response.statusCode

        if 200..<300 ~= statusCode 
            return nil
         else 
            return StatusCodeError(code: statusCode)
        
    

然后我可以调用类似的东西

let client = Client<MyRoutes>()
client.request(.companyProps(params: ["collections": "settings"]))
    .map  props -> CompanyModel in return props 
    .subscribe(onSuccess:  props in
      // do something with props
    )  error in
        print(error.localizedDescription)
.disposed(by: disposeBag)

我想开始处理 401 响应并刷新我的令牌并重试请求。

我正在努力寻找一个好的方法来做到这一点。

我发现 this excellent gist 概述了实现这一目标的方法,但是我正在努力在我当前的客户中实现这一目标。

任何提示或指示将不胜感激。

【问题讨论】:

【参考方案1】:

这是我的要点! (感谢您称其为优秀。)您看到随附的文章了吗? https://medium.com/@danielt1263/retrying-a-network-request-despite-having-an-invalid-token-b8b89340d29

处理 401 重试有两个关键要素。首先,您需要一种将令牌插入请求并使用Observable.deferred tokenAcquisitionService.token.take(1) 启动请求管道的方法。在您的情况下,这意味着您需要一个 URLRequest.init 来接受资源和令牌,而不仅仅是资源。

第二个是在收到 401 时抛出 TokenAcquisitionError.unauthorized 错误并以 .retryWhen $0.renewToken(with: tokenAcquisitionService) 结束请求管道

因此,鉴于上述情况,为了处理令牌重试,您只需将我的 TokenAcquisitionService 带入您的项目并使用它:

func getToken(_ oldToken: Token) -> Observable<(response: HTTPURLResponse, data: Data)> 
    fatalError("this function needs to be able to request a new token from the server. It has access to the old token if it needs that to request the new one.")


func extractToken(_ data: Data) -> Token 
    fatalError("This function needs to be able to extract the new token using the data returned from the previous function.")


let tokenAcquisitionService = TokenAcquisitionService<Token>(initialToken: Token(), getToken: getToken, extractToken: extractToken)

final class Client<R> where R: ResourceType 
    let session: URLSession

    init(session: URLSession = URLSession.shared) 
        self.session = session
    

    func request<T>(_ resource: R) -> Single<T> where T: Decodable 
        return Observable.deferred  tokenAcquisitionService.token.take(1) 
            .map  token in URLRequest(resource: resource, token: token) 
            .flatMapLatest  [session] request in session.rx.response(request: request) 
            .do(onNext:  response, _ in
                if response.statusCode == 401 
                    throw TokenAcquisitionError.unauthorized
                
            )
            .map  (_, data) -> T in
                return try JSONDecoder().decode(T.self, from: data)
        
        .retryWhen  $0.renewToken(with: tokenAcquisitionService) 
        .asSingle()
    

注意,getToken 函数可能必须提供一个视图控制器来请求用户的凭据。这意味着您需要展示您的登录视图控制器(或 UIAlertController)来收集数据。或者,当您登录时,您可能会从服务器获得授权令牌和刷新令牌。在这种情况下,TokenAcquisitionService 应该同时保留它们(即,它的 T 应该是 (token: String, refresh: String)。两者都可以。

该服务的唯一问题是如果获取新令牌失败,则整个服务将关闭。我还没有解决这个问题。

【讨论】:

太棒了!,直到现在我才看到随附的博客文章,非常感谢您抽出时间来分解它 嘿@DanielT 我想知道您是否对如何防止服务关闭有任何指示?如果您提到的请求失败?谢谢。

以上是关于使用 RxSwift 和 URLSession 处理 401 状态的主要内容,如果未能解决你的问题,请参考以下文章

tableView.rx.itemSelected 中的 RxSwift 双映射

使用 URLSession 和 Codable 解析 JSON 数据

使用 URLSession 和 ViewModel 检索数据

使用 URLSession 和后台获取以及使用 firebase 的远程通知

如何在 Swift 3 中使用带有代理的 URLSession

如何同步 URLSession 任务的串行队列?