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

iOS中Refresh Token实现及401场景下多API同步执行方案咨询

嘿,这个问题我之前在项目里踩过坑,核心痛点就是并发请求遇到401时,不能让每个请求都触发Token刷新——不仅浪费资源,还可能导致Token状态混乱。下面给你一套经过实战验证的iOS实现方案:

核心思路

用全局刷新锁+请求等待队列的组合逻辑:当第一个触发401的请求发起Token刷新后,后续所有需要Token的请求都进入等待队列;等刷新成功/失败后,再统一处理队列里的请求,彻底避免重复刷新。

具体实现步骤

1. 封装全局网络管理单例

先搞一个单例的APIManager,用来统一处理请求拦截、Token管理和刷新逻辑。里面需要三个关键成员:

  • isRefreshingToken: 布尔值,作为“刷新锁”,标记当前是否正在执行Token刷新
  • pendingRequests: 闭包数组,用来缓存等待刷新完成后需要重试的请求
  • 串行队列:保证多线程下操作上述两个属性的线程安全

2. 拦截401响应,处理刷新逻辑

以Alamofire为例(用URLSession自定义Delegate逻辑类似),通过RequestInterceptor拦截请求和响应:

  • 请求适配阶段:给需要Token的接口自动添加Authorization头
  • 响应重试阶段:判断是否是需要Token的接口返回401,再根据刷新锁状态决定是发起刷新还是加入等待队列

代码示例

class APIManager {
    static let shared = APIManager()
    private let session: Session
    private let tokenQueue = DispatchQueue(label: "com.yourapp.token.sync")
    private var _isRefreshingToken = false
    private var _pendingRequests: [() -> Void] = []
    
    // 线程安全的属性访问器
    private var isRefreshingToken: Bool {
        get { tokenQueue.sync { _isRefreshingToken } }
        set { tokenQueue.sync { _isRefreshingToken = newValue } }
    }
    
    private var pendingRequests: [() -> Void] {
        get { tokenQueue.sync { _pendingRequests } }
        set { tokenQueue.sync { _pendingRequests = newValue } }
    }
    
    private init() {
        let interceptor = TokenRequestInterceptor()
        session = Session(interceptor: interceptor)
    }
    
    // 自定义请求拦截器
    private struct TokenRequestInterceptor: Alamofire.RequestInterceptor {
        // 给需要Token的接口添加Authorization头
        func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
            var adaptedRequest = urlRequest
            guard needsToken(for: adaptedRequest) else {
                completion(.success(adaptedRequest))
                return
            }
            
            if let accessToken = TokenManager.shared.accessToken {
                adaptedRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
            }
            completion(.success(adaptedRequest))
        }
        
        // 处理401重试逻辑
        func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
            guard let response = request.task?.response as? HTTPURLResponse,
                  response.statusCode == 401,
                  needsToken(for: request.request!) else {
                completion(.doNotRetry)
                return
            }
            
            let apiManager = APIManager.shared
            if !apiManager.isRefreshingToken {
                apiManager.isRefreshingToken = true
                
                // 发起Refresh Token请求
                TokenManager.shared.refreshToken { success in
                    apiManager.isRefreshingToken = false
                    if success {
                        // 重试所有等待的请求
                        let pending = apiManager.pendingRequests
                        apiManager.pendingRequests.removeAll()
                        pending.forEach { $0() }
                        completion(.retry)
                    } else {
                        // 刷新失败,跳转到登录页
                        DispatchQueue.main.async {
                            // 这里替换成你的登录页跳转逻辑
                            print("Token刷新失败,请重新登录")
                        }
                        apiManager.pendingRequests.removeAll()
                        completion(.doNotRetry)
                    }
                }
            } else {
                // 将当前请求的重试逻辑加入等待队列
                apiManager.pendingRequests.append {
                    completion(.retry)
                }
            }
        }
        
        // 判断当前请求是否需要携带Token
        private func needsToken(for request: URLRequest) -> Bool {
            // 根据你的业务逻辑调整,比如匹配接口路径前缀
            let protectedPaths = ["/api/user", "/api/order", "/api/profile"]
            guard let path = request.url?.path else { return false }
            return protectedPaths.contains { path.hasPrefix($0) }
        }
    }
}

// 独立的Token管理类
class TokenManager {
    static let shared = TokenManager()
    var accessToken: String?
    var refreshToken: String?
    
    func refreshToken(completion: @escaping (Bool) -> Void) {
        guard let refreshToken = refreshToken else {
            completion(false)
            return
        }
        
        // 调用Refresh Token接口
        let url = URL(string: "https://your-api-domain.com/refresh-token")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.setValue("Bearer \(refreshToken)", forHTTPHeaderField: "Authorization")
        
        URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            guard let self = self, error == nil,
                  let response = response as? HTTPURLResponse, response.statusCode == 200,
                  let data = data else {
                completion(false)
                return
            }
            
            // 解析新的Token(根据你的接口返回格式调整)
            if let tokenModel = try? JSONDecoder().decode(TokenModel.self, from: data) {
                self.accessToken = tokenModel.accessToken
                self.refreshToken = tokenModel.refreshToken
                completion(true)
            } else {
                completion(false)
            }
        }.resume()
    }
    
    struct TokenModel: Codable {
        let accessToken: String
        let refreshToken: String
    }
}

3. 关键细节提醒

  • 线程安全:一定要用串行队列保护isRefreshingToken和pendingRequests的读写,避免多线程下的竞态条件
  • 重试次数限制:可以给重试逻辑加次数限制(比如最多重试1次),防止Refresh Token失效后无限循环重试
  • 边界情况处理:如果Refresh Token也过期了,要及时清空等待队列并跳转登录,避免用户卡在无效状态
  • 接口区分准确:needsToken方法要精准判断哪些接口需要Token,不要给公开接口做多余的Token处理
总结

这套方案通过“锁+队列”的模式,完美解决了并发401请求重复刷新Token的问题,同时保证了所有请求在Token更新后能正常重试,在多个项目中都验证过稳定性,你可以直接参考调整。

内容的提问来源于stack exchange,提问作者Renjish C

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.22 08:26:17