跳转到正文
Monster Cone
返回

FCM 迁移 HTTP v1 API:鉴权、发送与避坑

编辑页面

从 2024 年 6 月 20 日之后就无法使用旧版的 FCM API 了,必须迁移到 V1 版本才能使用,今天介绍国内如何迁移到 V1 版本。

1. 生成服务账号

进入你的 Firebase Messaging 项目 > 项目设置 > 服务账号 > 生成新的私钥

2. 获取 accessToken

我是 node 项目,所以使用 google-auth-library 库

npm install google-auth-library
const { GoogleAuth } = require("google-auth-library");
const SERVICE_ACCOUNT_FILE = process.env.FIREBASE_ADMIN_SDK; // 服务账号JSON路径
const serviceAccount = requrie(SERVICE_ACCOUNT_FILE);

const auth = new GoogleAuth({
  credentials: serviceAccount,
  scopes: ["https://www.googleapis.com/auth/cloud-platform"], // 直接使用示例授权范围即可
});

async function getAccessToken() {
  const accessToken = await auth.getAccessToken();
  return accessToken;
}

getAccessToken().then(accessToken => console.log(accessToken));

本地开发的话,国内需要使用代理,google-auth-library 并不提供 proxy 配置,但支持 HTTPS_PROXY 环境代理,Windows 使用以下命令

set HTTPS_PROXY=http://xxx.xxx.xxx.xxx:xxx # 配置代理
set HTTPS_PROXY= # 取消代理

3. 发送通知

与之前的接口相比,URL、授权、请求参数都发生了些许变化

URL 变化

原 URL

https://fcm.googleapis.com/fcm/send // 原URL

V1 新 URL

https://fcm.googleapis.com/v1/projects/{project-name}/messages:send // projects-name为firebase项目名

授权变化

原来使用服务器密钥

Authorization: key=AIzaSyZ-1u...0GBYzPu7Udno5aA

现在使用 accessToken

Authorization: Bearer ya29.ElqKBGN2Ri_Uz...HnS_uNreA

请求参数

原请求参数

{
  // 旧请求参数
  "to": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...", // or "/topics/news"
  "registration_ids": ["bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1..."],
  "notification": {
    "title": "Breaking News",
    "body": "New news story available."
  },
  "data": {
    "story_id": "story_12345"
  }
}

V1 新请求参数

{ // 新请求参数
	"message": {
		"token": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1..."
		"topic": "news",
		"notification": {
			"title": "Breaking News",
			"body": "New news story available."
		},
		"data": {
			"story_id": "story_12345"
		}
	}
}

返回结果:projects/{project_id}/messages/{message_id}格式字符串

{
	"name": "projects/myproject-b5ae1/messages/0:1500415314455276%31bd1c9631bd1c96"
}

4. 订阅和退订主题

之前多设备都是用的 registration_ids 参数,但移除了不得不使用主题通知

订阅通知

URL: https://iid.googleapis.com/iid/v1:batchAdd

{
  "to": "/topics/{your_topic}",
  "registration_tokens": [
    "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "..."
  ]
}

返回结果

{ "results": [{}] }

退订通知

URL: https://iid.googleapis.com/iid/v1:batchRemove

{
  "to": "/topics/{your_topic}",
  "registration_tokens": [
    "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...",
    "..."
  ]
}

返回结果

{ "results": [{}] }

5. 其他参数调整

详细配置参考:https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=zh-cn

6. 请求封装

我在 Node 项目中使用,所以使用 axios 封装,可参考

// axiosInstance.js
const axios = require("axios");
const { GoogleAuth } = require("google-auth-library");
const { HttpsProxyAgent } = require("https-proxy-agent");
const fs = require("fs");
const dotenv = require("dotenv");

const envFile = process.env.NODE_ENV ? `.env.${process.env.NODE_ENV}` : ".env";
dotenv.config({ path: envFile });

const httpsAgent = process.env.NODE_ENV
  ? new HttpsProxyAgent("http://xxx.xxx.xxx.xxx:xxxx")
  : null;

const SERVICE_ACCOUNT_FILE = process.env.FIREBASE_ADMIN_SDK;
const serviceAccount = require(SERVICE_ACCOUNT_FILE);

const auth = new GoogleAuth({
  credentials: serviceAccount,
  scopes: ["https://www.googleapis.com/auth/cloud-platform"],
});

// 获取访问令牌
async function getAccessToken() {
  const accessToken = await auth.getAccessToken();
  return accessToken;
}

let accessToken = "";

const axiosInstance = axios.create({
  timeout: 10000,
  headers: {
    "Content-Type": "application/json",
    access_token_auth: true,
  },
  httpsAgent,
  proxy: false,
});

axiosInstance.interceptors.request.use(async config => {
  if (!accessToken) {
    accessToken = await getAccessToken();
  }
  config.headers["Authorization"] = `Bearer ${accessToken}`;

  return config;
});

axiosInstance.interceptors.response.use(
  async response => {
    return [null, response.data];
  },
  async error => {
    const { status = 0, statusText = "" } = error.response;
    if (status === 400) {
      const { message } = error.response.data.error;
      return [{ status, statusText: message }, null];
    }
    if (status === 401) {
      const originConfig = error.config;
      accessToken = await getAccessToken();
      originConfig.headers["Authorization"] = `Bearer ${accessToken}`;
      const result = await axios.request(originConfig);
      return [null, result.data];
    }
    return [{ status, statusText }, null];
  }
);

module.exports = axiosInstance;

封装接口调用

const axiosInstance = require("./axiosInstance");

const sendNotification = (data, config) =>
  axiosInstance.post(
    "https://fcm.googleapis.com/v1/projects/xxxxxxx/messages:send",
    data,
    config
  );
const batchAdd = (data, config) =>
  axiosInstance.post(
    "https://iid.googleapis.com/iid/v1:batchAdd",
    data,
    config
  );
const batchRemove = (data, config) =>
  axiosInstance.post(
    "https://iid.googleapis.com/iid/v1:batchRemove",
    data,
    config
  );

module.exports = {
  sendNotification,
  batchAdd,
  batchRemove,
};

接口使用

const { sendNotification, batchAdd, batchRemove } = require("../xx/index");
// 发送通知
const params = {
  message: {
    token: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    notification: {
      title,
      body: content,
    },
    data: {
      messageId: String(newMessage.id),
    },
  },
};

const [err, result] = await sendNotification(params);
if (err) {
  // do something
}

// 设备订阅
let batchData = {
  to: `/topics/{your_topic}`,
  registration_tokens: ["xxxxxx"],
};
const [err, result] = await batchAdd(batchData);
if (err) {
  // do something
}

// 设备退订
let batchData = {
  to: `/topics/{your_topic}`,
  registration_tokens: ["xxxxxx"],
};
const [err, result] = await batchRemove(batchData);
if (err) {
  // do something
}

注意事项:


编辑页面
分享这篇文章:

上一篇
Vite + React + TypeScript 实战:从零搭建组件库
下一篇
Flutter Dio 封装实践:统一请求、解析与错误处理