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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 18:14:53