FCM从旧HTTP迁移至HTTP v1:Swift获取OAuth2.0令牌咨询
FCM HTTP v1迁移:OAuth2访问令牌的Swift实现方案
前置准备
从Firebase/Google Cloud控制台下载服务账号JSON密钥文件,将其添加到项目中(注意保密,不要提交至版本控制系统)。
1. 实现getOAuth2AccessToken()函数
由于令牌获取涉及异步网络请求,以下是带Completion Handler的异步实现:
import Foundation import CryptoKit typealias TokenCompletion = (Result<String, Error>) -> Void func getOAuth2AccessToken(completion: @escaping TokenCompletion) { // 替换为你的服务账号JSON文件名 guard let serviceAccountPath = Bundle.main.path(forResource: "service-account", ofType: "json"), let serviceAccountData = try? Data(contentsOf: URL(fileURLWithPath: serviceAccountPath)), let serviceAccount = try? JSONSerialization.jsonObject(with: serviceAccountData) as? [String: Any], let privateKey = serviceAccount["private_key"] as? String, let clientEmail = serviceAccount["client_email"] as? String else { completion(.failure(NSError(domain: "FCMTokenError", code: -1, userInfo: [NSLocalizedDescriptionKey: "服务账号密钥读取失败"]))) return } // 构建JWT头部 let header: [String: Any] = ["alg": "RS256", "typ": "JWT"] // 构建JWT载荷(有效期1小时,为Google允许的最大值) let currentTime = Date().timeIntervalSince1970 let payload: [String: Any] = [ "iss": clientEmail, "scope": "https://www.googleapis.com/auth/firebase.messaging", "aud": "https://oauth2.googleapis.com/token", "exp": Int(currentTime + 3600), "iat": Int(currentTime) ] do { // Base64URL编码头部与载荷 let headerData = try JSONSerialization.data(withJSONObject: header) let payloadData = try JSONSerialization.data(withJSONObject: payload) let base64Header = base64URLEncode(data: headerData) let base64Payload = base64URLEncode(data: payloadData) let signingInput = "\(base64Header).\(base64Payload)" // 使用RSA私钥签名 let cleanedPrivateKey = privateKey.replacingOccurrences(of: "-----BEGIN PRIVATE KEY-----\n", with: "") .replacingOccurrences(of: "\n-----END PRIVATE KEY-----", with: "") guard let keyData = Data(base64Encoded: cleanedPrivateKey) else { throw NSError(domain: "FCMTokenError", code: -2, userInfo: [NSLocalizedDescriptionKey: "私钥编码失败"]) } let privateKey = try P256.Signing.PrivateKey(pemRepresentation: privateKey) let signature = try privateKey.signature(for: signingInput.data(using: .utf8)!) let base64Signature = base64URLEncode(data: signature.rawRepresentation) // 生成完整JWT let jwt = "\(base64Header).\(base64Payload).\(base64Signature)" // 请求访问令牌 let tokenUrl = URL(string: "https://oauth2.googleapis.com/token")! var request = URLRequest(url: tokenUrl) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") let bodyParams = [ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": jwt ] request.httpBody = bodyParams.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)" } .joined(separator: "&") .data(using: .utf8) URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { completion(.failure(error)) return } guard let data = data, let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let accessToken = json["access_token"] as? String else { completion(.failure(NSError(domain: "FCMTokenError", code: -3, userInfo: [NSLocalizedDescriptionKey: "令牌解析失败"]))) return } completion(.success(accessToken)) }.resume() } catch { completion(.failure(error)) } } // Base64URL编码工具函数 private func base64URLEncode(data: Data) -> String { return data.base64EncodedString() .replacingOccurrences(of: "+", with: "-") .replacingOccurrences(of: "/", with: "_") .replacingOccurrences(of: "=", with: "") }
2. 关键注意事项
- 令牌缓存:令牌有效期为1小时,建议缓存令牌避免重复请求,不要每次发送通知都重新获取。
- FCM v1请求格式修正:你的新版代码中
postMessage格式不符合v1规范,需调整为如下结构:
let postMessage: [String: Any] = [ "message": [ "token": userIdToken, "notification": [ "badge": 1, "body": body, "title": title ], "android": [ "priority": "high" ], "apns": [ "payload": [ "aps": [ "content-available": true ] ], "headers": [ "apns-priority": "10" ] ] ] ]
旧版的priority和content_available需分别对应Android和APNs的专属配置字段。
3. 使用示例
在发送通知的函数中调用令牌获取逻辑:
func sendFCMNotification(title: String, body: String, userIdToken: String) { getOAuth2AccessToken { result in switch result { case .success(let accessToken): let url = URL(string: "https://fcm.googleapis.com/v1/projects/你的项目ID/messages:send")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") // 使用修正后的消息结构 let postMessage: [String: Any] = [ "message": [ "token": userIdToken, "notification": [ "badge": 1, "body": body, "title": title ], "android": [ "priority": "high" ], "apns": [ "payload": [ "aps": [ "content-available": true ] ], "headers": [ "apns-priority": "10" ] ] ] ] request.httpBody = try? JSONSerialization.data(withJSONObject: postMessage) URLSession.shared.dataTask(with: request) { data, response, error in // 处理通知发送结果 }.resume() case .failure(let error): print("令牌获取失败:\(error.localizedDescription)") } } }
内容的提问来源于stack exchange,提问作者福島悠樹
相关产品推荐
相关产品推荐

