iOS Swift应用从旧版FCM API迁移至HTTP v1的方法咨询
FCM 旧版API迁移至HTTP v1(Swift实现)
核心差异说明
旧版FCM API使用serverKey直接授权,HTTP v1版本则需要通过OAuth 2.0访问令牌完成身份验证,同时请求URL、请求体结构均有变更。
迁移步骤与代码实现
1. 准备服务账号密钥
从Firebase控制台下载服务账号JSON文件:
- 进入项目设置 → 服务账号 → 生成新的私钥,保存JSON文件到项目中(注意:客户端存储私钥存在安全风险,生产环境建议将发送逻辑移至后端)。
2. 实现访问令牌生成函数
HTTP v1需要Bearer令牌,需通过服务账号的私钥生成JWT并换取访问令牌:
import Foundation import CryptoKit func getFCMAccessToken(serviceAccountJSON: [String: Any]) async throws -> String { guard let privateKey = serviceAccountJSON["private_key"] as? String, let clientEmail = serviceAccountJSON["client_email"] as? String else { throw NSError(domain: "FCM", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid service account data"]) } let tokenURL = "https://oauth2.googleapis.com/token" // 构建JWT头部 let header = ["alg": "RS256", "typ": "JWT"] let headerBase64 = try JSONSerialization.data(withJSONObject: header) .base64EncodedString(options: .endLineWithLineFeed) // 构建JWT Payload let now = Date().timeIntervalSince1970 let payload = [ "iss": clientEmail, "scope": "https://www.googleapis.com/auth/firebase.messaging", "aud": tokenURL, "exp": now + 3600, // 令牌有效期1小时,建议缓存复用 "iat": now ] let payloadBase64 = try JSONSerialization.data(withJSONObject: payload) .base64EncodedString(options: .endLineWithLineFeed) // 生成签名 let signatureInput = "\(headerBase64).\(payloadBase64)" let cleanedPrivateKey = privateKey.replacingOccurrences(of: "-----BEGIN PRIVATE KEY-----\n", with: "") .replacingOccurrences(of: "\n-----END PRIVATE KEY-----", with: "") guard let signer = try? P256.Signing.PrivateKey(pemRepresentation: privateKey) else { throw NSError(domain: "FCM", code: -2, userInfo: [NSLocalizedDescriptionKey: "Failed to load private key"]) } let signature = try signer.signature(for: signatureInput.data(using: .utf8)!) let signatureBase64 = signature.rawRepresentation.base64EncodedString(options: .endLineWithLineFeed) // 组装JWT并请求访问令牌 let jwt = "\(headerBase64).\(payloadBase64).\(signatureBase64)" var request = URLRequest(url: URL(string: tokenURL)!) request.httpMethod = "POST" request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpBody = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=\(jwt)".data(using: .utf8) let (data, _) = try await URLSession.shared.data(for: request) guard let tokenResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any], let accessToken = tokenResponse["access_token"] as? String else { throw NSError(domain: "FCM", code: -3, userInfo: [NSLocalizedDescriptionKey: "Failed to get access token"]) } return accessToken }
3. 重构通知发送逻辑
更新请求URL与请求体结构,适配v1 API规范:
func sendFCMNotification(token: String, title: String, body: String, serviceAccountJSON: [String: Any]) async throws { guard let projectID = serviceAccountJSON["project_id"] as? String, let url = URL(string: "https://fcm.googleapis.com/v1/projects/\(projectID)/messages:send") else { throw NSError(domain: "FCM", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid project ID or URL"]) } // 获取访问令牌 let accessToken = try await getFCMAccessToken(serviceAccountJSON: serviceAccountJSON) // 构建v1格式的请求体 let message: [String: Any] = [ "token": token, "notification": [ "title": title, "body": body ], "apns": [ "headers": [ "apns-priority": "10" // 对应iOS高优先级通知 ] ] ] let requestBody = try JSONSerialization.data(withJSONObject: ["message": message]) var request = URLRequest(url: url) request.httpMethod = "POST" request.httpBody = requestBody request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) // 处理响应 if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 { let errorDetails = try JSONSerialization.jsonObject(with: data) as? [String: Any] let errorMessage = errorDetails?["error"]?["message"] as? String ?? "Unknown error" throw NSError(domain: "FCM", code: httpResponse.statusCode, userInfo: [NSLocalizedDescriptionKey: errorMessage]) } let responseDict = try JSONSerialization.jsonObject(with: data) as! [String: Any] print("FCM发送成功,响应:\(responseDict)") }
4. 使用示例
读取服务账号JSON并调用发送函数:
// 读取服务账号JSON文件 guard let jsonPath = Bundle.main.path(forResource: "your-service-account", ofType: "json"), let jsonData = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)), let serviceAccountJSON = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { fatalError("无法加载服务账号JSON文件") } // 发送通知示例 Task { do { try await sendFCMNotification( token: "目标设备令牌", title: "测试标题", body: "测试内容", serviceAccountJSON: serviceAccountJSON ) } catch { print("发送失败:\(error.localizedDescription)") } }
关键注意事项
- 令牌缓存:访问令牌有效期为1小时,建议缓存令牌避免重复生成,提升性能。
- 权限配置:确保服务账号拥有
Cloud Messaging Admin角色,否则会返回权限不足错误。 - 安全提示:客户端存储私钥存在泄露风险,生产环境建议将FCM发送逻辑部署在后端服务器。
内容的提问来源于stack exchange,提问作者Taimoor Arif
相关产品推荐
相关产品推荐

