iOS中Swift实现每小时自动刷新OAuth访问令牌方案咨询
解决iOS中1小时有效期OAuth令牌自动刷新的问题
嘿,我来帮你搞定这个令牌自动刷新的事儿!因为你用的是Reddit的installed_client授权类型,确实不需要刷新令牌,所以我们的核心思路就是在令牌过期前/过期时重新调用获取令牌的接口,同时要注意几个关键细节,我一步步给你讲:
首先修正你现有代码的小问题
你当前的代码每次生成新的UUID作为device_id,这其实不符合Reddit API的要求——同一个设备应该复用同一个device_id,不然可能会被API限制。另外,你没存储令牌的过期时间,这是判断令牌是否失效的关键。先把代码改一下:
func fetchAccessToken(completion: (() -> Void)? = nil) { guard let url = URL(string: "https://www.reddit.com/api/v1/access_token") else { completion?() return } // 复用已有的device_id,没有再生成新的 let uuid: String if let savedUUID = UserDefaults.standard.string(forKey: "userID") { uuid = savedUUID } else { uuid = UUID().uuidString UserDefaults.standard.set(uuid, forKey: "userID") } let params: Parameters = [ "grant_type" : "https://oauth.reddit.com/grants/installed_client", "device_id" : uuid ] let username = "MY_CLIENT_ID" let password = "" let loginString = String(format: "%@:%@", username, password) guard let loginData = loginString.data(using: .utf8) else { completion?() return } let base64EncodedString = loginData.base64EncodedString() let headers = [ "Content-Type" : "application/x-www-form-urlencoded", "Authorization" : "Basic \(base64EncodedString)" ] Alamofire.request(url, method: .post, parameters: params, encoding: URLEncoding.httpBody, headers: headers).responseJSON { response in if let JSON = response.result.value as? [String:Any] { guard let accessToken = JSON["access_token"] as? String else { completion?() return } // 存储令牌到钥匙串 KeychainWrapper.standard.set(accessToken, forKey: "oauthToken") // 计算并存储令牌过期时间(API返回的expires_in是秒数,默认3600即1小时) let expiresIn = JSON["expires_in"] as? Int ?? 3600 let expirationDate = Date().addingTimeInterval(TimeInterval(expiresIn)) KeychainWrapper.standard.set(expirationDate, forKey: "oauthTokenExpiration") } completion?() } }
两种实现自动刷新的方案
方案1:定时提前刷新(适合前台持续运行的场景)
既然令牌有效期是1小时,我们可以设置一个55分钟的定时器,提前5分钟刷新令牌,避免刚好过期时请求失败。注意:App进入后台后定时器可能会暂停,所以这种方式适合前台一直在用的场景。
func startTokenRefreshTimer() { // 每55分钟刷新一次(提前5分钟,避免令牌刚好过期) let refreshInterval = 55 * 60.0 Timer.scheduledTimer(withTimeInterval: refreshInterval, repeats: true) { [weak self] _ in self?.fetchAccessToken() } } // 在App启动时调用这个方法开启定时器 // 比如在AppDelegate的application(_:didFinishLaunchingWithOptions:)里
方案2:请求前检测令牌是否过期(更可靠,推荐)
这种方式是每次发起API请求前,先检查令牌是否即将过期/已经过期,如果是就先刷新令牌,再继续请求。配合Alamofire的RequestInterceptor可以完美实现这个逻辑,即使App在后台暂停后回到前台,也能自动处理:
第一步:创建令牌拦截器
import Alamofire class TokenInterceptor: RequestInterceptor { // 避免多个请求同时触发刷新,加个锁和标志位 private let lock = NSLock() private var isRefreshingToken = false func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { lock.lock() defer { lock.unlock() } guard let accessToken = KeychainWrapper.standard.string(forKey: "oauthToken"), let expirationDate = KeychainWrapper.standard.date(forKey: "oauthTokenExpiration") else { // 没有令牌或过期时间,先获取令牌 refreshToken(completion: { [weak self] in self?.adapt(urlRequest, for: session, completion: completion) }) return } // 检查令牌是否即将过期(提前1分钟判断,留缓冲时间) let currentDate = Date() let timeUntilExpiration = expirationDate.timeIntervalSince(currentDate) if timeUntilExpiration < 60 { // 即将过期,刷新令牌 refreshToken(completion: { [weak self] in self?.adapt(urlRequest, for: session, completion: completion) }) } else { // 令牌有效,添加到请求头 var adaptedRequest = urlRequest adaptedRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") completion(.success(adaptedRequest)) } } private func refreshToken(completion: @escaping () -> Void) { lock.lock() guard !isRefreshingToken else { lock.unlock() completion() return } isRefreshingToken = true lock.unlock() fetchAccessToken { [weak self] in self?.lock.lock() self?.isRefreshingToken = false self?.lock.unlock() completion() } } }
第二步:用带拦截器的Session发起请求
// 创建全局Session,所有请求都用这个Session let apiSession = Session(interceptor: TokenInterceptor()) // 之后发起请求时用apiSession,就会自动处理令牌刷新 apiSession.request("https://oauth.reddit.com/api/v1/me").responseJSON { response in // 处理请求结果 }
关键注意事项
- 复用device_id:一定要用同一个
device_id,不能每次生成新的UUID,否则Reddit API可能会拒绝你的请求或者创建多个设备记录。 - 线程安全:多个请求同时触发刷新时,要避免重复调用获取令牌的接口,上面的拦截器里已经加了锁和标志位处理这个问题。
- 后台兼容性:定时刷新的方式在后台可能失效,而请求前检测的方式更可靠,因为只要发起请求就会检查令牌状态。
内容的提问来源于stack exchange,提问作者Garret Koontz
相关产品推荐
相关产品推荐

