iOS moya 实现双token 刷新并重试

iOS中使用Moya实现双Token刷新与请求重试机制

在现代移动应用中,身份验证通常采用Token机制,而双Token(Access Token + Refresh Token)策略已成为主流安全方案。本文将详细介绍如何在iOS中使用Moya网络库实现双Token的自动刷新和请求重试功能。

双Token机制概述

双Token认证系统通常包含:

  • Access Token‌:短期有效的令牌,用于API访问授权
  • Refresh Token‌:长期有效的令牌,用于获取新的Access Token

当Access Token过期时(通常返回401状态码),应用应自动使用Refresh Token获取新的Access Token,并重试原始请求。

Moya基础配置

首先创建一个自定义的MoyaProvider子类:

csharp 复制代码
swiftCopy Code
public class CLMoyaProvider<Target>: MoyaProvider<Target> where Target: Moya.TargetType {
    // 实现代码...
}

核心实现解析

1. 请求拦截与401处理

request方法中拦截响应,检查401状态码:

swift 复制代码
swift
public override func request(_ target: Target, callbackQueue: DispatchQueue? = .none, progress: ProgressBlock? = .none, completion: @escaping Completion) -> Cancellable {
    return super.request(target, callbackQueue: callbackQueue, progress: progress, completion: { [weak self] result in
        guard let weakSelf = self else {
            completion(result)
            return
        }
        switch result {
        case let .success(response):
            do {
                let filteredResponse = try response.filterSuccessfulStatusAndRedirectCodes()
                let jsonMap = try filteredResponse.mapJSON()
                if let entity = jsonMap as? [String: Any], let code = entity["code"] as? Int, code == 401 {
                    weakSelf.handleUnauthorized(target: target, result: result, callbackQueue: callbackQueue, progress: progress, completion: completion)
                } else {
                    if target.path != CLServerConfig.instance.shortLivedToken {
                        weakSelf.refreshCount = 0
                    }
                    completion(result)
                }
            } catch {
                completion(result)
            }
        case let .failure(error):
            completion(.failure(error))
        }
    })
}

2. Token刷新与请求队列管理

handleUnauthorized方法处理Token刷新逻辑:

swift 复制代码
swift
private func handleUnauthorized(target: Target, result: Result<Response, MoyaError>, callbackQueue: DispatchQueue?, progress: ProgressBlock?, completion: @escaping Completion) {
    // 如果是刷新Token的请求本身失败,直接退出登录
    guard target.path != CLServerConfig.instance.shortLivedToken else {
        CLToastManager.showMessageCenter(message: "登录状态已过期,请重新登录")
        refreshCount = 0
        completion(result)
        return
    }
    
    // 如果正在刷新Token,将请求加入等待队列
    if self.isRefreshingToken {
        if !self.pendingRequests.contains(where: {$0.0.path == target.path}) {
            self.pendingRequests.append((target, callbackQueue, progress, completion))
        }
        completion(result)
        return
    }
    
    self.isRefreshingToken = true
    
    // 防止无限刷新循环
    if refreshCount > 0 {
        refreshCount = 0
        self.isRefreshingToken = false
        pendingRequests.removeAll()
        completion(result)
        return
    }
    
    refreshCount += 1
    
    // 加锁保证线程安全
    self.theLock.lock()
    CLLoginModule.refreshToken { [weak self] success in
        self?.theLock.unlock()
        self?.isRefreshingToken = false
        guard let weakSelf = self else { return }
        if success {
            // 刷新成功后重试所有挂起的请求
            for (target, callbackQueue, progress, completion) in weakSelf.pendingRequests {
                weakSelf.request(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
            }
            weakSelf.pendingRequests.removeAll()
            weakSelf.request(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
        } else {
            completion(result)
        }
    }
}

关键设计点

  1. 线程安全 ‌:使用NSRecursiveLock确保多线程环境下的安全操作
  2. 请求队列 ‌:在Token刷新期间,将后续请求加入pendingRequests队列
  3. 防循环机制 ‌:通过refreshCount防止无限刷新循环
  4. 特殊请求处理‌:排除Token刷新请求本身的401处理

使用示例

javascript 复制代码
swift
let provider = CLMoyaProvider<YourAPI>()

provider.request(.someEndpoint) { result in
    switch result {
    case let .success(response):
        // 处理成功响应
    case let .failure(error):
        // 处理错误
    }
}

总结

通过自定义MoyaProvider,我们实现了优雅的双Token刷新机制,具有以下优点:

  • 自动处理Token过期情况
  • 透明的请求重试机制
  • 线程安全的设计
  • 防止无限刷新循环
  • 良好的用户体验(无需用户手动重新登录)

这种实现方式可以无缝集成到现有项目中,大大简化了身份验证流程的管理。

完整的代码实现部分:

swift 复制代码
public class CLMoyaProvider<Target>: MoyaProvider<Target> where Target: Moya.TargetType {
    private var refreshCount: Int = 0
    private let theLock = NSRecursiveLock()
    private var isRefreshingToken = false
    private var pendingRequests: [(Target, DispatchQueue?, ProgressBlock?, Completion)] = []
    @discardableResult
    public override func request(_ target: Target, callbackQueue: DispatchQueue? = .none, progress: ProgressBlock? = .none, completion: @escaping Completion) -> Cancellable {
        return super.request(target, callbackQueue: callbackQueue, progress: progress, completion: { [weak self] result in
            guard let weakSelf = self else {
                completion(result)
                return
            }
            switch result {
            case let .success(response):
                do {
                    let filteredResponse = try response.filterSuccessfulStatusAndRedirectCodes()
                    let jsonMap = try filteredResponse.mapJSON()
                    if let entity = jsonMap as? [String: Any], let code = entity["code"] as? Int, code == 401 {
                        weakSelf.handleUnauthorized(target: target, result: result, callbackQueue: callbackQueue, progress: progress, completion: completion)
                    } else {
                        if target.path != CLServerConfig.instance.shortLivedToken {
                            weakSelf.refreshCount = 0
                        }
                        completion(result)
                    }
                } catch {
                    completion(result)
                }
            case let .failure(error):
              completion(.failure(error))
            }
        })
    }
    private func handleUnauthorized(target: Target, result: Result<Response, MoyaError>, callbackQueue: DispatchQueue?, progress: ProgressBlock?, completion: @escaping Completion) {
        guard target.path != CLServerConfig.instance.shortLivedToken else {
         CLToastManager.showMessageCenter(message: "登录状态已过期,请重新登录")
         refreshCount = 0
         completion(result)
         return
        }
        if self.isRefreshingToken {
          if !self.pendingRequests.contains(where: {$0.0.path == target.path}) {
                self.pendingRequests.append((target, callbackQueue, progress, completion))
            }
            completion(result)
            return
        }
        self.isRefreshingToken = true
        if refreshCount > 0 {
            refreshCount = 0
            self.isRefreshingToken = false
            pendingRequests.removeAll()
            completion(result)
            return
        }
        refreshCount += 1
        self.theLock.lock()
        CLLoginModule.refreshToken { [weak self] success in
            self?.theLock.unlock()
            self?.isRefreshingToken = false
            guard let weakSelf = self else { return }
            if success {
                for (target, callbackQueue, progress, completion) in weakSelf.pendingRequests {
                    weakSelf.request(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
                }
                weakSelf.pendingRequests.removeAll()
                weakSelf.request(target, callbackQueue: callbackQueue, progress: progress, completion: completion)
            } else {
                completion(result)
            }
        }
    }
}
相关推荐
烛阴13 分钟前
让你的Python并发飞起来:多线程开发实用技巧大全
前端·python
旺代15 分钟前
Vue3中的v-model、computed、watch
前端
excel1 小时前
微信小程序鉴权登录详解 —— 基于 wx.login 与后端 openid 换取流程
前端
Gazer_S1 小时前
【前端隐蔽 Bug 深度剖析:SVG 组件复用中的 ID 冲突陷阱】
前端·bug
蓝婷儿1 小时前
每天一个前端小知识 Day 7 - 现代前端工程化与构建工具体系
前端
mfxcyh2 小时前
npm下载离线依赖包
前端·npm·node.js
waterHBO3 小时前
01 ( chrome 浏览器插件, 立马翻译), 设计
前端·chrome
江城开朗的豌豆3 小时前
Vue组件CSS防污染指南:让你的样式乖乖“宅”在自家地盘!
前端·javascript·vue.js
江城开朗的豌豆3 小时前
Vue组件花式传值:祖孙组件如何愉快地聊天?
前端·javascript·vue.js
浩男孩4 小时前
【🍀新鲜出炉 】十个 “如何”从零搭建 Nuxt3 项目
前端·vue.js·nuxt.js