iOS Swift应用通过Refresh Token获取Google Access Token的REST API方案
我之前在iOS Swift项目里正好实现过用Google Refresh Token静默刷新AccessToken,不用跳登录页,给你一步步讲清楚,包括REST端点细节和完整代码:
先确认核心前提
- 你已经集成Google SignIn SDK,那必须确保登录时请求了
offline_access权限,否则Google不会返回Refresh Token。 - 第一次登录(或用户重新授权)时才能拿到Refresh Token,后续静默登录如果没触发授权弹窗,可能拿不到,所以必要时可以在登录请求里加
prompt: "consent"强制弹出授权页,确保获取到Refresh Token。 - 把Refresh Token存在Keychain里(别用UserDefaults,不安全),后续刷新时从Keychain读取。
iOS端可用的REST API端点细节
Google的Token刷新端点是固定的,不需要额外配置,直接调用即可:
- 请求URL:
https://oauth2.googleapis.com/token - 请求方法:POST
- Content-Type:
application/x-www-form-urlencoded - 必填参数:
grant_type: 固定值refresh_tokenclient_id: 你的iOS应用对应的OAuth 2.0客户端ID(从Google Cloud控制台的「凭据」里找类型为「iOS」的那个ID)refresh_token: 你之前保存的有效Refresh Token
注意:iOS客户端不需要传
client_secret,Google的移动应用OAuth客户端没有这个字段。
完整Swift实现步骤
1. 确保获取并安全存储Refresh Token
登录成功后,从GIDAuthentication中提取Refresh Token,并存入Keychain:
import GoogleSignIn import Security // 登录请求时要包含offline_access scope let signInConfig = GIDConfiguration(clientID: "你的iOS客户端ID") signInConfig.additionalScopes = ["https://www.googleapis.com/auth/cloud-platform", "offline_access"] GIDSignIn.sharedInstance.signIn(with: signInConfig, presenting: self) { user, error in guard let user = user, let auth = user.authentication else { // 处理登录错误 return } // 获取Refresh Token if let refreshToken = auth.refreshToken { // 存入Keychain(示例用原生Security框架,也可以用第三方Keychain库) let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: "GoogleRefreshToken", kSecValueData: refreshToken.data(using: .utf8)!, kSecAttrAccessible: kSecAttrAccessibleWhenUnlockedThisDeviceOnly ] SecItemDelete(query as CFDictionary) // 先删除旧的 let status = SecItemAdd(query as CFDictionary, nil) if status != errSecSuccess { print("存储Refresh Token失败") } } }
2. 实现静默刷新AccessToken的方法
用URLSession发送POST请求到Google的Token端点,解析响应获取新的AccessToken:
func refreshGoogleAccessToken(completion: @escaping (Result<String, Error>) -> Void) { // 先从Keychain读取Refresh Token guard let refreshToken = getRefreshTokenFromKeychain() else { completion(.failure(NSError(domain: "RefreshTokenError", code: -1, userInfo: [NSLocalizedDescriptionKey: "未找到Refresh Token"]))) return } let clientID = "你的iOS客户端ID" let url = URL(string: "https://oauth2.googleapis.com/token")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") // 构造请求参数 let parameters = [ "grant_type": "refresh_token", "client_id": clientID, "refresh_token": refreshToken ] // 编码为url-encoded格式 let parameterString = parameters.map { key, value in "\(key)=\(value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)" }.joined(separator: "&") request.httpBody = parameterString.data(using: .utf8) // 发送请求 let task = URLSession.shared.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let error = error { completion(.failure(error)) return } guard let data = data else { completion(.failure(NSError(domain: "RefreshTokenError", code: -2, userInfo: [NSLocalizedDescriptionKey: "未收到响应数据"]))) return } do { guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else { completion(.failure(NSError(domain: "RefreshTokenError", code: -3, userInfo: [NSLocalizedDescriptionKey: "响应解析失败"]))) return } // 提取新的AccessToken guard let accessToken = json["access_token"] as? String else { // 如果这里失败,大概率是Refresh Token失效,需要跳转登录 completion(.failure(NSError(domain: "RefreshTokenError", code: -4, userInfo: [NSLocalizedDescriptionKey: "Refresh Token已失效"]))) return } // 可选:保存AccessToken的过期时间(json里的expires_in是秒数) if let expiresIn = json["expires_in"] as? TimeInterval { let expirationDate = Date().addingTimeInterval(expiresIn) // 可以把过期时间也存在Keychain,下次提前刷新 } completion(.success(accessToken)) } catch { completion(.failure(error)) } } } task.resume() } // 从Keychain读取Refresh Token的辅助方法 private func getRefreshTokenFromKeychain() -> String? { let query: [CFString: Any] = [ kSecClass: kSecClassGenericPassword, kSecAttrAccount: "GoogleRefreshToken", kSecReturnData: kCFBooleanTrue!, kSecMatchLimit: kSecMatchLimitOne ] var data: AnyObject? let status = SecItemCopyMatching(query as CFDictionary, &data) if status == errSecSuccess, let tokenData = data as? Data { return String(data: tokenData, encoding: .utf8) } return nil }
3. 使用刷新后的AccessToken
拿到新的AccessToken后,你可以:
- 更新Google SignIn的当前用户认证信息,让SDK自动处理后续API调用:
refreshGoogleAccessToken { result in switch result { case .success(let accessToken): if let currentUser = GIDSignIn.sharedInstance.currentUser { let auth = GIDAuthentication(accessToken: accessToken, refreshToken: getRefreshTokenFromKeychain()!) currentUser.authentication = auth // 现在可以正常调用Google Cloud Prediction API了 } case .failure(let error): // 刷新失败,比如Refresh Token失效,跳转到登录页 GIDSignIn.sharedInstance.signIn(with: signInConfig, presenting: self) } }
- 或者直接用这个AccessToken调用Prediction API,在请求头里加
Authorization: Bearer \(accessToken)
关键注意事项
- Refresh Token失效场景:如果用户撤销了应用权限、超过6个月未使用该Refresh Token、用户更改了Google账号密码,Refresh Token会失效,此时必须跳转登录页重新获取。
- scope配置:一定要包含
offline_access,否则Google不会返回Refresh Token。 - 安全存储:Refresh Token是敏感信息,必须存在Keychain,绝对不能存在UserDefaults或明文存储。
- 客户端ID正确性:必须使用iOS类型的OAuth客户端ID,不要混用Web/服务器端的ID,否则会请求失败。
内容的提问来源于stack exchange,提问作者vishnu venugopal
相关产品推荐
相关产品推荐

