Logo
Published on

FCM 迁移到新 V1 API

Authors
  • avatar
    Name
    Monster Cone
    Twitter

从 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"
  }
}
  • 单设备或者主题通知使用 to 参数
  • 多设备使用 registration_ids 参数

V1 新请求参数

{ // 新请求参数
	"message": {
		"token": "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1..."
		"topic": "news",
		"notification": {
			"title": "Breaking News",
			"body": "New news story available."
		},
		"data": {
			"story_id": "story_12345"
		}
	}
}
  • 使用 message 包裹
  • 单设备使用 token
  • 主题通知使用 topic,但不需要/topics/,只用主题名
  • 删除了多设备通知,只能使用主题实现

返回结果: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...", "..."]
}
  • 授权方式和发送通知一样用 accessToken
  • to:订阅主题,/topics/ + 你的主题名
  • registration_tokens: 订阅的设备令牌数组,支持多个设备一起订阅

返回结果

{ "results": [{}] }
  • results 包含每个令牌的订阅结果
  • 如果结果中有 error 字段表示该令牌订阅失败

退订通知

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

{
  "to": "/topics/{your_topic}",
  "registration_tokens": ["bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1...", "..."]
}
  • 授权方式和发送通知一样用 accessToken
  • to:退订主题,/topics/ + 你的主题名
  • registration_tokens: 订阅的设备令牌数组,支持多个设备一起退订

返回结果

{ "results": [{}] }
  • results 包含每个令牌的退订结果
  • 如果结果中有 error 字段表示该令牌退订失败

5. 其他参数调整

  • notification 参数中不支持 icon 和 click_action 参数,需要在相关平台参数中配置,如 webpush 的 notificaition,android 的 notificaition
  • webpush 不支持 click_action,改为 fcm_options.link

详细配置参考: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
}

注意事项:

  • 因为我线上服务部署在香港,所以不需要使用代理,只有本地开发需要,所以 httpsAgent 自己调整
  • 如果你是使用 axios,并且需要使用代理,请将你的 proxy 配置设置为 false,否则在你配置 HTTPS_PROXY 后请求 https 时会受到影响
  • 如果你需要使用订阅和退订,需要在 headers 中使用 access_token_auth: true 配置