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
相关产品推荐
相关产品推荐

