Moya 11.0.1与OAuth2会话刷新:多异步HTTPS调用令牌过期问题求助
嘿,我在好几个iOS项目里都用Moya实现过OAuth2认证流程,专门踩过令牌过期的坑,给你分享下我总结的靠谱方案吧!
核心思路
Moya的插件(Plugin)机制是实现OAuth2认证的核心——它能拦截请求发送前的准备阶段,以及响应返回后的处理阶段。我们可以利用这两个时机:
- 请求发送前:检查当前令牌是否过期,未过期则自动在请求头添加
Authorization字段; - 收到响应后:如果返回
401 Unauthorized,则自动调用刷新令牌接口,拿到新令牌后重试原请求。
具体实现步骤
1. 定义令牌模型
首先创建一个令牌结构体,包含OAuth2所需的核心字段,再加上一个判断令牌是否过期的计算属性:
struct OAuthToken: Codable { let accessToken: String let refreshToken: String let expiresIn: TimeInterval var createdAt: TimeInterval // 令牌创建时间,用于计算过期时间 // 提前60秒判定为过期,避免请求过程中刚好过期 var isExpired: Bool { return Date().timeIntervalSince1970 > (createdAt + expiresIn - 60) } }
2. 实现令牌管理类
这个类负责令牌的存储、获取和刷新逻辑,还要处理并发刷新的问题(防止多个请求同时触发刷新令牌接口):
class OAuth2Manager { static let shared = OAuth2Manager() private var token: OAuthToken? private var refreshTask: Task<Void, Error>? // 控制并发刷新 // 持久化令牌(实际项目建议用Keychain,这里简化为内存存储) func saveToken(_ newToken: OAuthToken) { token = newToken // 示例:用Keychain存储的话,可以用Security框架或者第三方库(比如KeychainSwift) } func getCurrentToken() -> OAuthToken? { return token } // 刷新令牌的异步方法 func refreshToken() async throws -> OAuthToken { // 如果已有刷新任务,等待其完成 if let existingTask = refreshTask { try await existingTask.value guard let validToken = token else { throw OAuthError.tokenNotFound } return validToken } // 创建新的刷新任务 refreshTask = Task { [weak self] in guard let self = self, let refreshToken = self.token?.refreshToken else { throw OAuthError.tokenNotFound } // 调用刷新令牌的接口(假设你有对应的Moya Target) let provider = MoyaProvider<AuthTarget>() let response = try await provider.request(.refreshToken(refreshToken: refreshToken)) let newToken = try response.map(OAuthToken.self) newToken.createdAt = Date().timeIntervalSince1970 // 记录创建时间 self.saveToken(newToken) } do { try await refreshTask?.value guard let validToken = token else { throw OAuthError.tokenNotFound } return validToken } catch { throw error } finally { refreshTask = nil // 任务完成后清空 } } // 自定义错误类型 enum OAuthError: Error { case tokenNotFound case refreshFailed } }
3. 实现Moya OAuth2插件
这个插件会拦截请求和响应,处理令牌的添加和过期重试:
class OAuth2Plugin: PluginType { // 请求发送前:添加Authorization头 func prepare(_ request: URLRequest, target: TargetType) -> URLRequest { var modifiedRequest = request guard let token = OAuth2Manager.shared.getCurrentToken(), !token.isExpired else { return modifiedRequest } modifiedRequest.addValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization") return modifiedRequest } // 收到响应后:处理401错误 func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) { switch result { case .failure(let error): guard let response = error.response, response.statusCode == 401 else { return } // 触发令牌刷新并重试请求 Task { do { let newToken = try await OAuth2Manager.shared.refreshToken() // 重构请求,添加新令牌并重试 var retryRequest = target.urlRequest! retryRequest.addValue("Bearer \(newToken.accessToken)", forHTTPHeaderField: "Authorization") // 这里可以根据你的请求方式处理重试: // 如果用async/await,可以通知调用方重新发起请求; // 如果用闭包,可以重新调用provider.request方法 } catch { // 刷新失败,清除令牌并跳转登录页面 print("令牌刷新失败:\(error.localizedDescription)") // 示例:通知APP跳转登录 // NotificationCenter.default.post(name: .needLogin, object: nil) } } case .success: break } } }
4. 初始化带插件的Moya Provider
最后,在创建Moya请求器时,把OAuth2插件加进去:
let apiProvider = MoyaProvider<YourAPITarget>(plugins: [OAuth2Plugin()])
关键注意点
- 令牌持久化安全:绝对不要用
UserDefaults存储令牌,一定要用Keychain,因为UserDefaults是明文存储,容易被窃取; - 避免无限重试:如果刷新令牌也返回401(比如refreshToken也过期了),一定要终止重试流程,直接跳转到登录页面;
- 提前过期判定:设置提前60秒判定过期,能避免请求发送时令牌刚好过期的尴尬情况;
- 并发请求处理:用
Task控制刷新令牌的并发,防止多个请求同时触发刷新接口,造成资源浪费。
内容的提问来源于stack exchange,提问作者Andrea Bozza
相关产品推荐
相关产品推荐

