Alamofire 4重试器与适配器无法识别更新后的accessToken问题
解决Alamofire Retrier/Adapt并发场景下Token刷新后的请求失败问题
嘿,我猜你遇到的大概率是并发请求触发的Token竞态条件——虽然你改成了同步请求获取新Token,但如果没做好Token的线程安全访问,或者Retrier/Adapt的逻辑里没正确复用已刷新的Token,就会出现部分请求还是拿着旧Token发送的情况。下面给你几个针对性的排查和解决方向:
1. 先把Token的读写改成线程安全的
不管你用什么方式存Token(单例变量、UserDefaults啥的),多线程下的读写必须加锁,不然很容易出现一个线程已经更新了Token,但另一个线程读到的还是旧值的情况。
给你个简单的实现例子,用NSLock来保护Token的访问:
class TokenManager { static let shared = TokenManager() private let lock = NSLock() private var _accessToken: String? // 线程安全的Token读写属性 var accessToken: String? { get { lock.lock() defer { lock.unlock() } return _accessToken } set { lock.lock() defer { lock.unlock() } _accessToken = newValue } } // 同步刷新Token的方法,同样加锁确保原子性 func refreshTokenSync() -> String? { lock.lock() defer { lock.unlock() } // 这里写你的同步请求逻辑,比如调用刷新接口 guard let newToken = fetchNewTokenFromAPI() else { return nil } _accessToken = newToken return newToken } private func fetchNewTokenFromAPI() -> String? { // 你的同步请求代码,比如用URLSession同步请求 return "new_valid_token" } }
2. 避免重复触发Token刷新
就算是同步请求,如果多个请求同时进入Adapt/Retrier,发现Token失效,还是可能重复调用刷新接口(毕竟同步请求也需要时间)。所以得加个“正在刷新”的标记,确保同一时间只有一个请求去刷新Token,其他请求等刷新完成后直接复用新Token。
可以用DispatchSemaphore来实现这个“互斥刷新”的逻辑:
class TokenManager { static let shared = TokenManager() private let lock = NSLock() private let refreshSemaphore = DispatchSemaphore(value: 1) private var _accessToken: String? private var isRefreshing = false var accessToken: String? { // 线程安全读写,同上 } func getValidToken() -> String? { refreshSemaphore.wait() defer { refreshSemaphore.signal() } // 先检查有没有已经刷新好的Token,避免重复刷新 if let validToken = accessToken { return validToken } // 执行同步刷新 guard let newToken = fetchNewTokenFromAPI() else { return nil } accessToken = newToken return newToken } }
然后在你的Adaptor里,就用这个getValidToken()方法来获取Token:
class CustomAdapter: RequestAdapter { func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { guard var request = urlRequest as? URLRequest else { completion(.failure(NSError(domain: "AdaptError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid request"]))) return } let tokenManager = TokenManager.shared guard let token = tokenManager.getValidToken() else { completion(.failure(NSError(domain: "AdaptError", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to get valid token"]))) return } request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") completion(.success(request)) } }
3. 检查Retrier的重试逻辑是否正确
有时候请求失败触发重试,但如果重试时没有重新获取最新的Token,还是会用旧Token发送,自然还是失败。所以在Retrier的重试逻辑里,一定要重新拿最新的Token再修改请求头:
class CustomRetrier: RequestRetrier { func should(_ session: Session, retry request: Request, for error: Error, dueTo failureReason: RetryFailureReason, completion: @escaping (RetryResult) -> Void) { // 先判断是不是401过期错误 guard let response = request.task?.response as? HTTPURLResponse, response.statusCode == 401 else { completion(.doNotRetry) return } let tokenManager = TokenManager.shared guard let newToken = tokenManager.getValidToken() else { completion(.doNotRetryWithError(NSError(domain: "RetryError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to refresh token"]))) return } // 更新请求的Authorization头 var newRequest = request.request! newRequest.setValue("Bearer \(newToken)", forHTTPHeaderField: "Authorization") request.request = newRequest // 重试请求 completion(.retry) } }
4. 从失败请求的细节里找线索
你提到会打印失败请求的响应,建议重点看这几个点:
- 失败请求的
Authorization头是不是旧的Token?如果是,那就是线程安全或者Token复用的问题 - 失败的状态码是不是还是401?如果是,可能刷新接口返回的Token本身就无效(比如Refresh Token也过期了,或者刷新逻辑有bug)
- 是不是多个请求同时触发刷新,导致某个请求拿到了中间状态的Token?
这些细节能帮你更快定位到底是哪里出了问题。
内容的提问来源于stack exchange,提问作者megha
相关产品推荐
相关产品推荐

