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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 04:14:25