Swift中FCM从Legacy API迁移至HTTP V1的Bearer令牌获取问题
解决FCM HTTP V1版本获取Bearer临时令牌的问题
FCM HTTP V1要求使用OAuth2 Bearer令牌(有效期1小时),推荐缓存令牌,过期后再重新获取,不用每次发送都请求。以下是两种可行实现方案:
方式一:使用Google官方Auth库(推荐,简化实现)
- 添加依赖:通过Swift Package Manager引入
google-auth-library-swift,依赖地址:https://github.com/googleapis/google-auth-library-swift - 令牌获取逻辑:
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)") } }
- 修正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接口获取令牌:
- 生成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) }
- 请求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
相关产品推荐
相关产品推荐

