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)
}
}
}
关键设计点
- 线程安全 :使用
NSRecursiveLock
确保多线程环境下的安全操作 - 请求队列 :在Token刷新期间,将后续请求加入
pendingRequests
队列 - 防循环机制 :通过
refreshCount
防止无限刷新循环 - 特殊请求处理:排除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)
}
}
}
}