Bỏ qua để đến nội dung

Chữ ký và mẫu xác thực

Mọi yêu cầu webhook mang header Hd-Signature. Bạn phải xác thực chữ ký trước khi tin nội dung: nếu không, bất kỳ ai biết URL nhận cũng có thể giả danh hoadon.pro.

Hd-Signature: t=1760000000,v1=ba718eea26d2803db5a211030e2a9e1f28ded05e44ec92c362516fef2fa058ff
  • t: thời điểm gửi, tính bằng giây Unix.
  • v1: chữ ký, là hex(HMAC_SHA256(secret, t + "." + body)) — nối t, một dấu chấm và nội dung gốc (raw body) của yêu cầu.

secret là secret ký của điểm nhận, hiển thị khi thêm webhook. Mỗi điểm nhận có secret riêng.

  1. Đọc nội dung gốc của yêu cầu dưới dạng byte hoặc chuỗi, trước khi phân tích JSON. Phân tích rồi tuần tự hoá lại sẽ làm đổi nội dung và chữ ký sai.
  2. Tách t và v1 từ header.
  3. Tính HMAC-SHA256 của chuỗi t + "." + body bằng secret, ra chuỗi hex.
  4. So sánh bằng hàm so sánh thời gian cố định với v1.
  5. Kiểm tra t không cũ quá một ngưỡng (khuyến nghị 5 phút) để chống phát lại yêu cầu cũ.
  6. Chỉ khi cả hai đều đạt mới xử lý sự kiện; ngược lại trả 400 và bỏ qua.
import crypto from 'node:crypto';
const TOLERANCE_SECONDS = 300;
export function verifyHoadonSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
const t = parts.t;
const v1 = parts.v1;
if (!t || !v1) return false;
const age = Math.abs(Date.now() / 1000 - Number(t));
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(v1, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Với Express, nhận nội dung gốc bằng express.raw({ type: 'application/json' }) và truyền req.body.toString('utf8').

Dùng bộ giá trị dưới đây để kiểm tra mã của bạn trước khi nhận webhook thật (bỏ bước kiểm tra độ cũ của t khi chạy thử).

MụcGiá trị
Secretwhsec_test_secret_0123456789
t1760000000
Nội dung gốc{"id":"evt_01J9ZQ8K","type":"invoice.created","created_at":"2026-10-15T09:30:12Z","org_id":"org_01J9Z","data":{}}
Chữ ký v1 mong đợiba718eea26d2803db5a211030e2a9e1f28ded05e44ec92c362516fef2fa058ff
  • Chữ ký luôn sai: bạn đang ký lại nội dung đã phân tích JSON thay vì nội dung gốc, hoặc framework đã tự đổi mã hoá ký tự.
  • Sai secret: mỗi điểm nhận có secret riêng; kiểm tra bạn đang dùng đúng điểm nhận.
  • Sai múi giờ khi kiểm tra t: t là giây Unix (UTC), không phải giờ địa phương.