02 · HTTP v1 API 与认证¶
目标读者:想理解"HTTP API + OAuth"底层机制的后端工程师,或不能/不想用 Admin SDK 的环境(如 Cloudflare Worker) 核心问题:怎么配 Service Account?怎么拿 Access Token?怎么用 curl 发第一条推送? 前置: 01-FCM核心概念与架构 后续: 03-Admin-SDK实战 最后更新:2026-04-11
一、为什么是 v1 —— Legacy API 已废弃¶
1.1 两版对比¶
| 维度 | Legacy API(已废弃) | HTTP v1 API(当前唯一选择) |
|---|---|---|
| 端点 | fcm.googleapis.com/fcm/send |
fcm.googleapis.com/v1/projects/{PROJECT_ID}/messages:send |
| 认证 | 静态 Server Key | OAuth 2.0 Bearer(Service Account) |
| 平台特化 | 字段散乱 | 结构化 android / apns / webpush 子对象 |
| 跨平台一致性 | 差 | 好 |
| 安全性 | Key 泄漏 = 灾难,无法快速轮换 | Token 1 小时自动过期 |
| 状态 | 2024-06-20 已停用 ❌ | 当前唯一选择 ✅ |
1.2 ⚠️ 不要踩的坑¶
网上能搜到的 90% 教程都是 Legacy API —— Authorization: key=AIza... 这种形式的一律过时,照抄会直接 401。看到 https://fcm.googleapis.com/fcm/send 这个端点也要立刻警觉。
本文档的所有内容都是 v1 版本。
二、Service Account 配置¶
2.1 核心概念¶
- Service Account(服务账号):Google Cloud 概念,一种"非人类"的 Google 账号,用于服务器到服务器的认证。Firebase Project 默认有一个
firebase-adminsdk-xxxx@<project>.iam.gserviceaccount.com - Service Account JSON Key:从 Firebase Console 下载的私钥文件,含
private_keyclient_emailproject_id等字段。这是最敏感的凭据,泄漏 = 整个 Firebase Project 沦陷 ⚠️ - OAuth Scope:
https://www.googleapis.com/auth/firebase.messaging是 FCM 唯一需要的 scope
2.2 下载 Service Account JSON(手把手)¶
1. 进入 Firebase Console (https://console.firebase.google.com)
▼
2. 选中目标项目(每个 iGaming 平台一个项目)
▼
3. 点击左上齿轮 → 项目设置 (Project Settings)
▼
4. 切到【服务账号 (Service accounts)】标签
▼
5. 看到 "Firebase Admin SDK" 区块
→ 点击【生成新的私钥 (Generate new private key)】
▼
6. 弹窗确认
▼
7. 浏览器下载 <project>-firebase-adminsdk-xxxx.json 文件
▼
8. 立刻放进密码管理器 / Secret Manager,不要 commit 到 git ⚠️
注意事项:
- 每次点"生成新的私钥"会新建一对 key,老的不会自动失效(除非手动在 GCP IAM 删除)
- 建议每个环境(dev / staging / prod)只生成 1 次
- 多平台场景:每个 Firebase 项目下载 1 份,命名用 <platform>-sa.json 区分
2.3 JSON 文件结构¶
{
"type": "service_account",
"project_id": "your-platform-prod",
"private_key_id": "abc123...",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEv...\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-xxxx@your-platform-prod.iam.gserviceaccount.com",
"client_id": "1234567890",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-xxxx%40your-platform-prod.iam.gserviceaccount.com"
}
private_key是真正的 RSA 私钥 —— 视同 SSH key 一样保护project_id必须和 Firebase Project ID 完全一致
三、获取 Access Token(两种方式)¶
FCM v1 API 需要 OAuth 2.0 Bearer Token,有效期 1 小时。
3.1 方式 A:用 Google Auth Library(推荐)¶
库自动处理 JWT 签名 + token 交换 + 到期刷新。
Node.js:
// 安装: npm install google-auth-library
const { GoogleAuth } = require('google-auth-library');
async function getAccessToken() {
const auth = new GoogleAuth({
keyFile: './service-account.json',
scopes: ['https://www.googleapis.com/auth/firebase.messaging'],
});
const client = await auth.getClient();
const tokenResponse = await client.getAccessToken();
return tokenResponse.token; // 1 小时有效
}
// 用法
(async () => {
const token = await getAccessToken();
console.log('Bearer token:', token);
})();
Python:
# 安装: pip install google-auth google-auth-httplib2 requests
from google.oauth2 import service_account
from google.auth.transport.requests import Request
SCOPES = ['https://www.googleapis.com/auth/firebase.messaging']
def get_access_token():
credentials = service_account.Credentials.from_service_account_file(
'service-account.json', scopes=SCOPES)
credentials.refresh(Request())
return credentials.token # 1 小时有效
if __name__ == '__main__':
print(get_access_token())
3.2 方式 B:手动 JWT 签名(学习/无依赖场景)¶
不推荐生产使用,但对理解底层机制有帮助。
# pip install pyjwt cryptography requests
import jwt
import time
import requests
import json
with open('service-account.json') as f:
sa = json.load(f)
now = int(time.time())
claims = {
"iss": sa['client_email'],
"scope": "https://www.googleapis.com/auth/firebase.messaging",
"aud": "https://oauth2.googleapis.com/token",
"iat": now,
"exp": now + 3600,
}
signed_jwt = jwt.encode(claims, sa['private_key'], algorithm='RS256')
resp = requests.post(
"https://oauth2.googleapis.com/token",
data={
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": signed_jwt,
},
)
access_token = resp.json()['access_token']
print('Access token:', access_token)
流程图:
Service Account JSON (含私钥)
│
▼
① 构造 JWT claims
{ iss, scope, aud, iat, exp }
│
▼
② 用 RSA 私钥对 claims 签名 → signed JWT
│
▼
③ POST oauth2.googleapis.com/token
{ grant_type: jwt-bearer, assertion: <JWT> }
│
▼
④ 收到 { access_token, expires_in: 3600 }
│
▼
⑤ 用 "Authorization: Bearer <token>" 调用 FCM API
四、messages:send 请求体完整 Schema¶
4.1 顶层结构¶
{
"validate_only": false,
"message": {
"name": "string (read-only, response only)",
"data": { "key1": "string value" },
"notification": {
"title": "string",
"body": "string",
"image": "https://..."
},
"android": { ... },
"apns": { ... },
"webpush": { ... },
"fcm_options": { "analytics_label": "string" },
// 三选一 (oneof target)
"token": "device_registration_token",
"topic": "vip_users",
"condition": "'TopicA' in topics && 'TopicB' in topics"
}
}
关键点:
- token / topic / condition 三选一,不能同时出现
- validate_only: true 只校验 payload 不真发,调试时极有用
4.2 Android 平台字段¶
"android": {
"collapse_key": "string",
"priority": "NORMAL | HIGH",
"ttl": "3600s",
"restricted_package_name": "com.example.app",
"data": { "k": "v" },
"notification": {
"title": "string (overrides top-level)",
"body": "string",
"icon": "string",
"color": "#rrggbb",
"sound": "default",
"tag": "string",
"click_action": "string",
"body_loc_key": "string",
"body_loc_args": ["string"],
"title_loc_key": "string",
"title_loc_args": ["string"],
"channel_id": "string",
"image": "https://..."
},
"fcm_options": { "analytics_label": "string" }
}
关键字段:
- priority: "HIGH" —— 运营推送必须设,决定是否立刻送达
- notification.channel_id —— Android 8+ 必需,否则不响
- notification.image —— 图片 URL,限制 1 MB
4.3 iOS (APNs) 平台字段¶
"apns": {
"headers": {
"apns-priority": "10",
"apns-expiration": "0",
"apns-collapse-id": "string"
},
"payload": {
"aps": {
"alert": { "title": "string", "body": "string" },
"badge": 1,
"sound": "default",
"content-available": 1,
"mutable-content": 1,
"category": "string",
"thread-id": "string"
},
"custom_key": "custom_value"
},
"fcm_options": { "analytics_label": "string", "image": "https://..." }
}
关键字段:
- headers.apns-priority: "10" —— 立即送达;"5" 是省电模式。运营推送一律 "10"
- payload.aps.content-available: 1 —— 静默推送(不能同时有 alert)
- payload.aps.mutable-content: 1 —— 富通知(可以被 App 的 Notification Service Extension 修改)
4.4 Web Push 平台字段¶
"webpush": {
"headers": { "TTL": "60", "Urgency": "high" },
"data": { "k": "v" },
"notification": {
"title": "string",
"body": "string",
"icon": "https://...",
"actions": [{ "action": "open", "title": "Open" }],
"badge": "https://...",
"image": "https://..."
},
"fcm_options": { "link": "https://...", "analytics_label": "string" }
}
五、完整 curl 示例(从认证到发送)¶
目标:用纯 bash + Python 发送一条到 all_users topic 的推送。
#!/bin/bash
# send-push.sh
# 作者: Bob · 2026-04-11
PROJECT_ID="your-platform-prod"
SA_FILE="./service-account.json"
# Step 1: 用 Python 拿 Access Token
ACCESS_TOKEN=$(python3 -c "
from google.oauth2 import service_account
from google.auth.transport.requests import Request
c = service_account.Credentials.from_service_account_file(
'$SA_FILE',
scopes=['https://www.googleapis.com/auth/firebase.messaging'])
c.refresh(Request())
print(c.token)
")
# Step 2: POST 消息
curl -X POST "https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json; UTF-8" \
-d '{
"message": {
"topic": "all_users",
"notification": {
"title": "限时优惠码 SUPER88",
"body": "今晚 11 点前充值送 88% 红利"
},
"data": {
"promo_code": "SUPER88",
"deeplink": "myapp://promo/super88"
},
"android": {
"priority": "HIGH",
"notification": { "channel_id": "promo", "sound": "default" }
},
"apns": {
"headers": { "apns-priority": "10" },
"payload": { "aps": { "sound": "default", "badge": 1 } }
}
}
}'
成功响应:
{
"name": "projects/your-platform-prod/messages/0:1612345678901234%abcdef"
}
返回的 name 是消息 ID,可以存下来做追踪。
5.1 先校验不发(validate_only)¶
调试时加一个 URL 参数或 body 字段:
curl -X POST "https://fcm.googleapis.com/v1/projects/${PROJECT_ID}/messages:send" \
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"validate_only": true,
"message": { "topic": "test", "notification": { "title": "t", "body": "b" } }
}'
校验通过会返回 200,但不会真发送 —— 写脚本时一定要先跑一遍 validate。
六、常见错误码对照¶
| HTTP | error.status | 含义 | 处理 |
|---|---|---|---|
| 400 | INVALID_ARGUMENT |
payload 字段写错、超 4KB、token 格式无效 | 修代码,不重试 |
| 401 | UNAUTHENTICATED |
Access Token 过期/错误 | 刷新 token 再试 |
| 403 | PERMISSION_DENIED |
Service Account 没有 firebase.messaging 权限 / Project 错 |
查 GCP IAM 配置 |
| 404 | NOT_FOUND (UNREGISTERED) |
token 已失效,App 卸载/清数据 | 从 DB 删除该 token ⚠️ |
| 429 | QUOTA_EXCEEDED |
Fan-out 超限 | 退避重试 |
| 500 | INTERNAL |
FCM 内部错 | 指数退避重试 |
| 503 | UNAVAILABLE |
临时不可用 | 按响应头 Retry-After 重试 |
详细错误处理策略见 06-错误处理与Token生命周期。
七、陷阱清单¶
-
不要把 Service Account JSON commit 到 git ⚠️ 用
.gitignore屏蔽,或直接放 Vault / GCP Secret Manager / AWS Secrets Manager。包括 private repo 也不行。 -
Access Token 1 小时过期 手撸代码要管理刷新逻辑。用 Admin SDK 完全免操心(下一节)。
-
message.data的 value 必须是 string 数字也要写成"123",布尔值写"true",否则 400。 -
apns.headers.apns-priority"10"= 立即送达,"5"= 节能模式。运营推送一律"10"。 -
iOS 静默推送
apns.payload.aps.content-available = 1,且不能有 alert。否则 Apple 限频严重。 -
不要在前端跑这段代码 Service Account 一旦进了浏览器就泄漏了。只能在受信任的服务端环境。
-
message.token和message.tokens的区别 v1 HTTP API 只支持token(单数),没有tokens。批量发送需要通过 Admin SDK 的sendEachForMulticast()或循环调用。
八、何时用 HTTP API 而不用 Admin SDK¶
除非有特殊原因,永远用 Admin SDK(见 03)。
但以下场景必须用裸 HTTP:
- Cloudflare Worker / Deno / 边缘函数环境 —— Admin SDK 依赖 Node native 模块,装不上
- 完全不想引入依赖的一次性脚本
- 需要精准控制 HTTP 行为(自定义超时、代理、TLS)
其它场景一律 Admin SDK —— 少写 90% 代码。
九、权威来源¶
- https://firebase.google.com/docs/cloud-messaging/auth-server
- https://firebase.google.com/docs/cloud-messaging/send-message
- https://firebase.google.com/docs/reference/fcm/rest/v1/projects.messages
- https://firebase.google.com/docs/cloud-messaging/migrate-v1