跳转到内容

验证签名

任何知道你端点 URL 的人都可以向它发送请求。在信任载荷之前,请务必验证签名。

每次投递都带有:

webhook-id: 0192e1c4-0000-7000-8000-0000000000cc
webhook-timestamp: 1761228131
webhook-signature: v1,K5oZfzN0mYI+Hx3Yb0v8j3H5kq2tqZ2dA8s0r6sF9Vw=

签名是对 "{webhook-id}.{webhook-timestamp}.{raw body}" 计算的 base64 HMAC-SHA256,密钥为密钥字符串解码后的字节:取你的 signing_secret,去掉 whsec_ 前缀,再对其余部分做 base64 解码。请拒绝时间戳与你本机时钟相差超过 5 分钟的请求,以防止重放。

SpacePost SDK 使用 WebCrypto 验证,并返回解析后的事件。如果签名或时间戳无效,它会抛出异常。

webhook-handler.ts
import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
/** Works as a Bun, Deno, Cloudflare Workers or Next.js route handler. */
export async function POST(request: Request): Promise<Response> {
const payload = await request.text(); // the RAW body: do not parse it first
try {
const event = await spacepost.webhooks.verify({
payload,
headers: request.headers,
secret: process.env.SPACEPOST_WEBHOOK_SECRET!,
});
switch (event.type) {
case "email.bounced":
case "email.complained":
console.warn("Stop mailing", event.data.to);
break;
case "email.delivered":
console.log("Delivered", event.data.email_id);
break;
}
return new Response("ok");
} catch {
return new Response("invalid signature", { status: 400 });
}
}

标头可以是 Headers 对象或普通对象(例如 Express 的 req.headers)。使用 Express 时,请在 Webhook 路由上挂载 express.raw({ type: "application/json" }),使 req.body 为 Buffer。

使用 standardwebhooks 包:

import { Webhook } from "standardwebhooks";
const wh = new Webhook(process.env.SPACEPOST_WEBHOOK_SECRET!); // "whsec_..." is accepted
const event = wh.verify(rawBody, {
"webhook-id": req.headers["webhook-id"] as string,
"webhook-timestamp": req.headers["webhook-timestamp"] as string,
"webhook-signature": req.headers["webhook-signature"] as string,
});

使用 svix(或 resend 包的验证器):请使用 svix-* 标头,SpacePost 同样会发送它们。

import { Webhook } from "svix";
const wh = new Webhook(process.env.SPACEPOST_WEBHOOK_SECRET!);
const event = wh.verify(rawBody, {
"svix-id": req.headers["svix-id"] as string,
"svix-timestamp": req.headers["svix-timestamp"] as string,
"svix-signature": req.headers["svix-signature"] as string,
});

官方 resend 包的 Webhook 验证使用相同的 Standard Webhooks 标头,因此可以配合你的 SpacePost signing_secret 使用。

每个 Webhook 的 signing_secret 是固定的。如需轮换,请用相同的端点创建一个新的 Webhook,让你的处理程序改为接受新密钥(切换期间两个都尝试),然后删除旧的 Webhook。

把隧道 URL 注册为端点,然后向 delivered@test.spacepost.dev 发送一封邮件。参见测试。