如何在 firebase 上实现 callback endpoint
明确结论
可以用 Cloud Functions for Firebase 的 HTTP 函数实现 callback endpoint。函数部署后会生成一个公开的 HTTPS 地址,把它作为 callbackUrl 传给第三方支付 API 即可。
Firebase Hosting 只能托管静态资源,无法直接处理支付回调。接收请求、校验签名和更新 Realtime Database 等操作,都应放在可信的服务端环境中,例如 Cloud Functions 或 Cloud Run。
也不要让 Android App 携带支付密钥直接调用第三方 API。建议采用下面的流程:
Android App
↓ 发起支付
Cloud Function
↓ 调用第三方支付 API,并传入 callbackUrl
第三方支付平台
↓ POST 支付结果
Callback Cloud Function
↓ 校验签名并更新数据
Realtime Database
创建 HTTP callback
先在 Firebase 项目中初始化 Functions:
firebase init functions
下面以 JavaScript 和 Cloud Functions v2 为例。支付通知的字段名、签名请求头和签名算法,需要按照第三方 API 的文档调整。
const { onRequest } = require("firebase-functions/v2/https");
const { defineSecret } = require("firebase-functions/params");
const { initializeApp } = require("firebase-admin/app");
const { getDatabase } = require("firebase-admin/database");
const crypto = require("node:crypto");
initializeApp();
const callbackSecret = defineSecret("PAYMENT_CALLBACK_SECRET");
exports.paymentCallback = onRequest(
{
secrets: [callbackSecret],
region: "asia-east1",
},
async (req, res) => {
if (req.method !== "POST") {
res.set("Allow", "POST");
res.status(405).send("Method Not Allowed");
return;
}
try {
/*
* 这里只是 HMAC-SHA256 签名校验示例。
* 仅当支付平台明确采用这种算法时才能直接使用。
* 请求头名称、原始报文格式和编码方式应以平台文档为准。
*/
const receivedSignature = req.get("x-signature");
if (
!receivedSignature ||
!verifySignature(
req.rawBody,
receivedSignature,
callbackSecret.value()
)
) {
res.status(401).send("Invalid signature");
return;
}
const notification = req.body;
// 以下字段名称仅为示例,应替换成支付平台实际返回的字段。
const eventId = notification.eventId;
const orderId = notification.orderId;
const paymentStatus = notification.status;
if (
typeof eventId !== "string" ||
typeof orderId !== "string" ||
typeof paymentStatus !== "string"
) {
res.status(400).send("Invalid payload");
return;
}
// Realtime Database 路径不能包含这些字符。
if (/[.#$\[\]\/]/.test(eventId) || /[.#$\[\]\/]/.test(orderId)) {
res.status(400).send("Invalid identifier");
return;
}
const paymentRef = getDatabase().ref(`payments/${orderId}`);
/*
* 第三方平台可能重复发送同一个通知。
* 使用 transaction 记录 eventId,可以避免同一事件被重复处理。
*/
const result = await paymentRef.transaction((current) => {
const payment = current || {};
const processedEvents = payment.processedEvents || {};
if (processedEvents[eventId]) {
return; // 中止事务,表示已经处理过
}
return {
...payment,
status: paymentStatus,
providerPaymentId:
notification.paymentId ?? payment.providerPaymentId ?? null,
processedEvents: {
...processedEvents,
[eventId]: true,
},
updatedAt: Date.now(),
};
});
if (!result.committed) {
// 已处理过的重复通知也返回 2xx,避免平台持续重试。
res.status(200).send("Already processed");
return;
}
res.status(200).send("OK");
} catch (error) {
console.error("Payment callback failed", error);
// 返回非 2xx 通常会触发支付平台重试,具体行为取决于平台规则。
res.status(500).send("Internal Server Error");
}
}
);
function verifySignature(rawBody, receivedSignature, secret) {
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
const expected = Buffer.from(expectedSignature, "utf8");
const received = Buffer.from(receivedSignature, "utf8");
return (
expected.length === received.length &&
crypto.timingSafeEqual(expected, received)
);
}
配置回调密钥
不要把支付平台密钥写入代码,也不要提交到 Git 仓库。可以通过 Firebase CLI 设置 Secret:
firebase functions:secrets:set PAYMENT_CALLBACK_SECRET
然后部署函数:
firebase deploy --only functions:paymentCallback
部署完成后,CLI 会显示函数的 HTTPS 地址,通常类似下面这样:
https://REGION-PROJECT_ID.cloudfunctions.net/paymentCallback
请以实际部署输出为准,并将完整地址作为第三方 API 所需的 callbackUrl。
使用 Firebase Hosting 域名转发
如果希望使用自己的 Firebase Hosting 域名作为回调地址,例如:
https://example.web.app/api/payment/callback
可以在 firebase.json 中配置 rewrite:
{
"hosting": {
"rewrites": [
{
"source": "/api/payment/callback",
"function": {
"functionId": "paymentCallback",
"region": "asia-east1"
}
}
]
}
}
配置后,部署 Hosting 和 Function:
firebase deploy --only hosting,functions:paymentCallback
这项配置并非必需。多数情况下,直接使用函数生成的 HTTPS 地址更简单。
发起支付时传入 callbackUrl
支付请求也最好通过另一个 Cloud Function 发起,避免第三方 API 密钥暴露在 Android 安装包中:
const callbackUrl =
"https://REGION-PROJECT_ID.cloudfunctions.net/paymentCallback";
const response = await fetch("https://third-party.example/payments", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${paymentApiKey}`,
},
body: JSON.stringify({
amount: 1000,
currency: "CNY",
callbackUrl,
}),
});
这里的字段名只是示例,实际参数应以第三方 API 的规定为准。
需要特别注意的安全问题
不能只相信通知正文
callback endpoint 是公开地址,任何人都可以向它发送请求。必须通过支付平台提供的签名、公钥验签、Webhook Secret 或其他认证机制验证请求来源。
不能仅凭请求中出现 "status": "paid",就把订单标记为已支付。
必须处理重复通知
当网络超时或支付平台没有收到成功响应时,同一通知通常会被再次发送。应使用平台提供的唯一事件 ID、交易 ID 或通知 ID 做幂等处理,避免重复发货、充值或记账。
如果平台没有提供事件 ID,需要按照其文档选择稳定的幂等键,例如交易 ID 与事件类型的组合。
不要依赖 Android 客户端确认支付结果
客户端可能断网、退出,也可能被篡改。最终支付状态应以通过签名验证的服务端通知为准。必要时,还可以调用第三方支付平台的订单查询接口进行二次确认。
尽快返回响应
回调函数只需完成必要的校验和数据更新,随后立即返回平台要求的 2xx 响应。耗时较长的业务操作可以交给队列或其他后台任务处理。
CORS 通常与支付回调无关
支付通知属于第三方服务器与 Cloud Function 之间的请求,不受浏览器 CORS 限制。无需为了处理支付回调而无条件开放跨域请求。
备注:内容仅供参考。
