You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.20 07:14:09