Kiểm tra chữ ký webhook
Kiểm tra trước khi tin, và kiểm tra trên phần thân thô.
SDK chưa phát hành. Nội dung dưới đây dùng HTTP thuần.
Mỗi lần gọi về đều mang chữ ký. Hãy kiểm tra trước khi xử lý nội dung, so sánh theo cách chống dò thời gian, và luôn tính trên phần thân thô của request.
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.ROUTE4GREEN_WEBHOOK_SECRET!;
/**
* HMAC-SHA256 over the four delivery parts joined by dots, in this order.
* Anything else, including a different separator, produces a digest that
* never matches.
*/
function expectedSignature(
timestamp: string,
eventId: string,
eventType: string,
rawBody: string,
): string {
const preimage = `${timestamp}.${eventId}.${eventType}.${rawBody}`;
return createHmac("sha256", SECRET).update(preimage).digest("hex");
}
function safeEqualHex(a: string, b: string): boolean {
const left = Buffer.from(a, "hex");
const right = Buffer.from(b, "hex");
// Compare lengths first: timingSafeEqual throws on a mismatch.
return left.length === right.length && timingSafeEqual(left, right);
}
export async function POST(request: Request): Promise<Response> {
// Raw bytes, not a parsed object. Re-serialising JSON reorders keys and
// every signature then fails.
const rawBody = await request.text();
const timestamp = request.headers.get("x-smartway-timestamp") ?? "";
const eventId = request.headers.get("x-smartway-event-id") ?? "";
const eventType = request.headers.get("x-smartway-event") ?? "";
const received = (request.headers.get("x-smartway-signature") ?? "").replace(/^sha256=/, "");
if (!timestamp || !eventId || !eventType || !received) {
return new Response("missing signature headers", { status: 400 });
}
if (!safeEqualHex(received, expectedSignature(timestamp, eventId, eventType, rawBody))) {
return new Response("invalid signature", { status: 401 });
}
// Your own freshness window. No server-side replay window is documented,
// so choose one and reject anything older.
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) {
return new Response("stale delivery", { status: 401 });
}
// Deliveries can repeat, so key your handler on eventId and make it safe to
// run twice. Acknowledge first: the delivery timeout is 5 seconds.
void handleTerminalEvent(eventId, eventType, JSON.parse(rawBody));
return new Response("ok");
}Chuỗi được ký là timestamp, mã sự kiện, loại sự kiện và phần thân thô, nối bằng dấu chấm theo đúng thứ tự đó. Hàm kiểm tra đầy đủ nằm trong phần webhook của hướng dẫn tối ưu tuyến.