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

Swift中FCM从Legacy API迁移至HTTP V1的Bearer令牌获取问题

解决FCM HTTP V1版本获取Bearer临时令牌的问题

FCM HTTP V1要求使用OAuth2 Bearer令牌(有效期1小时),推荐缓存令牌,过期后再重新获取,不用每次发送都请求。以下是两种可行实现方案:

方式一:使用Google官方Auth库(推荐,简化实现)

  1. 添加依赖:通过Swift Package Manager引入google-auth-library-swift,依赖地址:https://github.com/googleapis/google-auth-library-swift
  2. 令牌获取逻辑:
import GoogleAuthLibrary

// 加载从Google云控制台下载的服务账号JSON密钥文件
guard let serviceAccountPath = Bundle.main.path(forResource: "service-account", ofType: "json"),
      let credentials = ServiceAccountCredentials(fromFile: serviceAccountPath) else {
    print("加载服务账号密钥失败")
    return
}

// 指定FCM权限范围
let scopes = ["https://www.googleapis.com/auth/firebase.messaging"]

// 内部自动缓存令牌,过期自动刷新
credentials.getToken(withScopes: scopes) { result in
    switch result {
    case .success(let token):
        let bearerToken = token.accessToken
        self.sendFCMV1Push(to: "用户设备令牌", title: "推送标题", body: "推送内容", bearerToken: bearerToken)
    case .failure(let error):
        print("获取令牌失败:\(error.localizedDescription)")
    }
}
  1. 修正FCM V1推送请求格式(你原代码参数为Legacy格式,需调整):
func sendFCMV1Push(to token: String, title: String, body: String, bearerToken: String) {
    // 补全HTTPS前缀
    let urlString = "https://fcm.googleapis.com/v1/projects/myproject-77a88/messages:send"
    guard let url = URL(string: urlString) else {
        print("无效的推送URL")
        return
    }
    
    // FCM V1标准请求格式
    let paramString: [String: Any] = [
        "message": [
            "token": token,
            "notification": [
                "title": title,
                "body": body,
                "badge": "1",
                "sound": "default"
            ]
        ]
    ]
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.httpBody = try? JSONSerialization.data(withJSONObject: paramString)
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.setValue("Bearer \(bearerToken)", forHTTPHeaderField: "Authorization")
    
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            print("推送请求失败:\(error.localizedDescription)")
            return
        }
        do {
            if let jsonData = data,
               let jsonDict = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] {
                print("推送响应:\(jsonDict)")
            }
        } catch {
            print("响应解析失败:\(error.localizedDescription)")
        }
    }
    task.resume()
}

方式二:手动生成JWT并请求令牌(无第三方依赖,轻量场景)

如果不想引入库,可以手动生成JWT并调用Google OAuth2接口获取令牌:

  1. 生成JWT(需引入轻量库SwiftJWT处理RSA签名):
import SwiftJWT

// 定义Google OAuth2所需的JWT声明结构
struct GoogleClaims: Claims {
    let iss: String // 服务账号的client_email
    let scope: String // FCM权限范围:https://www.googleapis.com/auth/firebase.messaging
    let aud: String // 固定值:https://oauth2.googleapis.com/token
    let exp: Date // 过期时间,建议设为当前时间+3600秒(1小时)
    let iat: Date // 签发时间
}

func generateJWT() -> String? {
    let serviceAccountEmail = "your-service-account@myproject-77a88.iam.gserviceaccount.com"
    let privateKey = "-----BEGIN PRIVATE KEY-----\n你的服务账号私钥内容\n-----END PRIVATE KEY-----"
    
    let claims = GoogleClaims(
        iss: serviceAccountEmail,
        scope: "https://www.googleapis.com/auth/firebase.messaging",
        aud: "https://oauth2.googleapis.com/token",
        exp: Date().addingTimeInterval(3600),
        iat: Date()
    )
    
    var jwt = JWT(claims: claims)
    guard let privateKeyData = privateKey.data(using: .utf8),
          let signer = try? JWTSigner.rs256(privateKey: privateKeyData) else {
        return nil
    }
    return try? jwt.sign(using: signer)
}
  1. 请求Bearer令牌:
func fetchBearerToken(completion: @escaping (String?) -> Void) {
    guard let jwt = generateJWT() else {
        completion(nil)
        return
    }
    
    let urlString = "https://oauth2.googleapis.com/token"
    guard let url = URL(string: urlString) else {
        completion(nil)
        return
    }
    
    let params = [
        "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
        "assertion": jwt
    ]
    
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.httpBody = params.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)!)" }.joined(separator: "&").data(using: .utf8)
    request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
    
    let task = URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
            print("获取令牌失败:\(error.localizedDescription)")
            completion(nil)
            return
        }
        do {
            if let jsonData = data,
               let jsonDict = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
               let accessToken = jsonDict["access_token"] as? String {
                completion(accessToken)
            } else {
                completion(nil)
            }
        } catch {
            print("令牌响应解析失败:\(error.localizedDescription)")
            completion(nil)
        }
    }
    task.resume()
}

// 使用示例
fetchBearerToken { token in
    if let bearerToken = token {
        self.sendFCMV1Push(to: "用户设备令牌", title: "标题", body: "内容", bearerToken: bearerToken)
    }
}

注意事项

  • 令牌有效期1小时,建议缓存令牌,每次发送前检查是否过期,过期再重新获取,避免频繁请求令牌接口
  • 原代码中URL缺少https://前缀,必须补上否则请求会失败
  • FCM V1的请求参数必须包裹在message字段下,目标设备令牌用token字段而非Legacy版的to

内容的提问来源于stack exchange,提问作者Christine

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 02:24:52