Swift中如何创建OAuth2.0调用FCM v1 API?Vapor服务如何调用FCM发通知?
嘿,我刚好在Vapor项目里实现了FCM v1 API的OAuth2授权,给你一步步拆解怎么做,完全适配Swift环境:
在Vapor(Swift)中实现FCM v1 API的OAuth2授权
一、先搞懂核心前提
- FCM v1 API 不支持用Server key/Legacy key调用,必须使用基于服务账号的OAuth2访问令牌
- 我们需要通过JWT签名向Google的OAuth2服务器换取临时访问令牌,再用这个令牌调用FCM接口
二、准备Firebase服务账号密钥
- 登录Firebase控制台,进入你的项目
- 点击「设置」→「服务账号」→「生成新的私钥」
- 下载JSON格式的密钥文件,放到Vapor项目的
Resources目录下(记得把这个文件加入.gitignore!)
三、配置Vapor项目依赖
在Package.swift里添加必要的依赖:
dependencies: [ .package(url: "https://github.com/vapor/vapor.git", from: "4.0.0"), .package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0"), .package(url: "https://github.com/vapor/async-http-client.git", from: "1.0.0") ], targets: [ .target( name: "App", dependencies: [ .product(name: "Vapor", package: "vapor"), .product(name: "JWTKit", package: "jwt-kit"), .product(name: "AsyncHTTPClient", package: "async-http-client") ] ), // ...其他targets ]
四、实现令牌获取逻辑
1. 定义服务账号和令牌相关的结构体
import Vapor import JWTKit // 解析服务账号JSON的结构体 struct FirebaseServiceAccount: Codable { let type: String let projectId: String let privateKeyId: String let privateKey: String let clientEmail: String let clientId: String let authUri: String let tokenUri: String let authProviderX509CertUrl: String let clientX509CertUrl: String } // JWT断言结构体,用于生成签名 struct FCMAuthClaims: JWTPayload { let iss: String // 服务账号的client_email let scope: String // FCM权限范围 let aud: String // OAuth2令牌端点地址 let exp: Date // 令牌过期时间(最多1小时) let iat: Date // 令牌签发时间 // 验证过期时间 func verify(using signer: JWTSigner) throws { try self.exp.verifyNotExpired() } } // 令牌响应结构体 struct FCMAccessTokenResponse: Codable { let accessToken: String let tokenType: String let expiresIn: Int enum CodingKeys: String, CodingKey { case accessToken = "access_token" case tokenType = "token_type" case expiresIn = "expires_in" } }
2. 加载服务账号到Vapor应用
在configure.swift里添加:
func configure(_ app: Application) throws { // 加载服务账号JSON let serviceAccountPath = app.directory.resourcesDirectory.appending("service-account.json") guard let serviceAccountData = try? Data(contentsOf: serviceAccountPath) else { fatalError("找不到Firebase服务账号JSON文件,请检查Resources目录") } let serviceAccount = try JSONDecoder().decode(FirebaseServiceAccount.self, from: serviceAccountData) // 把服务账号存储到应用中,方便后续调用 app.storage.set(FirebaseServiceAccount.self, to: serviceAccount) // ...其他配置 }
3. 添加获取令牌的扩展方法
给Application加个异步方法,用来获取FCM访问令牌:
extension Application { func fetchFCMAccessToken() async throws -> String { guard let serviceAccount = self.storage.get(FirebaseServiceAccount.self) else { fatalError("服务账号未初始化") } // 构建JWT断言 let claims = FCMAuthClaims( iss: serviceAccount.clientEmail, scope: "https://www.googleapis.com/auth/firebase.messaging", aud: "https://oauth2.googleapis.com/token", exp: Date().addingTimeInterval(3600), // 1小时有效期 iat: Date() ) // 用服务账号的私钥初始化RS256签名器 let privateKey = try RSAKey.private(pem: serviceAccount.privateKey) let signer = JWTSigner.rs256(key: privateKey) // 生成签名后的JWT字符串 let jwt = try signer.sign(claims) // 向Google OAuth2服务器发送请求换取令牌 let client = self.http.client var request = ClientRequest(method: .POST, url: "https://oauth2.googleapis.com/token") // 提交表单数据 request.content = try .query([ "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer", "assertion": jwt ]) let response = try await client.send(request) // 解析响应获取access_token let tokenResponse = try await response.content.decode(FCMAccessTokenResponse.self) return tokenResponse.accessToken } }
五、调用FCM v1 API发送通知
实现一个发送通知的函数,用上上面获取的令牌:
func sendFCMNotification(app: Application, deviceToken: String, title: String, body: String) async throws { let accessToken = try await app.fetchFCMAccessToken() let projectId = app.storage.get(FirebaseServiceAccount.self)!.projectId // 构建FCM请求 var request = ClientRequest( method: .POST, url: "https://fcm.googleapis.com/v1/projects/\(projectId)/messages:send" ) // 设置Authorization头 request.headers.add(name: .authorization, value: "Bearer \(accessToken)") request.headers.add(name: .contentType, value: "application/json") // 构建通知内容 let notificationPayload: [String: Any] = [ "message": [ "token": deviceToken, "notification": [ "title": title, "body": body ], // 如果需要自定义数据,可以加"data"字段 "data": [ "custom_key": "custom_value" ] ] ] request.content = try .json(notificationPayload) // 发送请求 let response = try await app.http.client.send(request) // 检查响应状态 guard response.status.isSuccess else { let errorDetails = try await response.content.decode([String: Any].self) throw Abort(.internalServerError, reason: "FCM发送失败:\(errorDetails)") } }
六、优化建议
- 缓存令牌:因为令牌有效期是1小时,没必要每次请求都重新生成,可以用Vapor的
Cache系统缓存令牌,每次先检查缓存是否有效,无效再重新获取 - 权限配置:确保你的服务账号在Google Cloud IAM中拥有「Cloud Messaging Admin」角色,否则会出现权限错误
- 生产环境安全:不要把服务账号JSON硬编码或提交到版本控制,生产环境可以用环境变量读取密钥内容,或者使用Google Cloud Secret Manager
内容的提问来源于stack exchange,提问作者bibscy
相关产品推荐
相关产品推荐

