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

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,提问作者福島悠樹

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 09:17:07