跳转至

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_key client_email project_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生命周期


七、陷阱清单

  1. 不要把 Service Account JSON commit 到 git ⚠️ 用 .gitignore 屏蔽,或直接放 Vault / GCP Secret Manager / AWS Secrets Manager。包括 private repo 也不行

  2. Access Token 1 小时过期 手撸代码要管理刷新逻辑。用 Admin SDK 完全免操心(下一节)。

  3. message.data 的 value 必须是 string 数字也要写成 "123",布尔值写 "true",否则 400。

  4. apns.headers.apns-priority "10" = 立即送达,"5" = 节能模式。运营推送一律 "10"

  5. iOS 静默推送 apns.payload.aps.content-available = 1,且不能有 alert。否则 Apple 限频严重。

  6. 不要在前端跑这段代码 Service Account 一旦进了浏览器就泄漏了。只能在受信任的服务端环境

  7. message.tokenmessage.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