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)
            }
        }
    }
}
相关推荐
Csvn9 小时前
🧩「找不到模块」排查全记录——Monorepo 下 TypeScript 路径别名的 5 种「不通」与根治方案
前端
腻害兔9 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:字典、短信、邮件、通知——后台系统的“基础设施四件套“!
java·前端·vue.js·产品经理·ai编程
CodexDave9 小时前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡
Ai_easygo10 小时前
AI Agent开发入门——从ReAct到Tool Calling,拆解Agent的底层运行逻辑
前端·人工智能·react.js
研☆香10 小时前
分析制作html页面,如何划分页面结构
前端
腻害兔10 小时前
【若依项目-产品经理视角】RuoYi-Vue-Pro 源码拆解:支付模块 yudao-module-pay,一个让产品经理都看懂的支付中台设计
java·前端·vue.js·产品经理·ai编程
kyriewen11 小时前
我扒了最近的前端面经——2026年面试不背八股文了,考这5样
前端·面试·ai编程
IT_陈寒11 小时前
Redis的持久化配置把我坑惨了:你以为数据安全了?
前端·人工智能·后端
小徐_233311 小时前
uni-app 项目别再从零搭了!3 个 Wot UI 起手模板怎么选?
前端·uni-app
赵庆明老师12 小时前
Vben精讲:16-熟悉Vite前端工具链
前端