← Tất cả bài viết
Hướng dẫn

Chống phát lại webhook HMAC: timestamp, retry và idempotency

Chống phát lại webhook HMAC cần hai lớp kiểm soát riêng biệt. Trước hết, hãy xác minh HMAC-SHA256 trên timestamp chính xác cộng với raw body và từ chối delivery nằm ngoài cửa sổ thời gian do bạn quy định. Sau đó, loại trùng event ID ổn định vì một delivery xác thực vẫn có thể được retry. Xác minh chữ ký chứng minh tính toàn vẹn và việc bên gửi biết shared secret; nó không biến delivery thành exactly-once.

Cơ chế chống phát lại webhook HMAC hoạt động thế nào

Một receiver an toàn trả lời bốn câu hỏi theo đúng thứ tự:

  1. Các header ký có hiện diện và đúng định dạng không? Khi endpoint được cấu hình ký, hãy từ chối request thiếu timestamp, chữ ký hoặc event ID.
  2. Request còn đủ mới không? Parse timestamp RFC 3339 và áp dụng cửa sổ thời gian phù hợp với hạ tầng của bạn.
  3. Các byte chính xác có khớp chữ ký không? Tính HMAC-SHA256 trên <timestamp>.<raw body> và so sánh digest theo constant time.
  4. Event này đã được chấp nhận chưa? Lưu event ID ổn định với unique constraint trước khi acknowledge delivery.

Bước cuối rất quan trọng vì retry và replay không giống nhau. Retry là lần giao lại hợp lệ sau lỗi kết nối hoặc phản hồi không phải 2xx. Replay là việc dùng lại một request đã được ký hợp lệ trước đó ngoài luồng xử lý mà bạn dự kiến. Kiểm tra độ mới của timestamp giới hạn khoảng thời gian một request bị ghi lại còn được chấp nhận; idempotency bền vững ngăn retry hợp lệ tạo cùng một ticket, câu trả lời hoặc workflow hai lần.

Điểm chính

  • Xác minh raw request bytes trước khi parse JSON hoặc serialize lại.
  • Xem độ mới của timestamp là chính sách ứng dụng; UnifyPort không áp đặt một ngưỡng cố định cho mọi hệ thống.
  • Dùng phép so sánh constant-time cho các buffer digest có cùng độ dài.
  • Loại trùng X-Device-Event-Id, vì delivery của UnifyPort là at-least-once.
  • Chỉ trả về 2xx sau khi event đã được chấp nhận bền vững, không cần đợi toàn bộ tác vụ phía sau hoàn tất.

Hợp đồng chữ ký UnifyPort chính xác

Khi webhook endpoint có signing_secret, UnifyPort gửi X-Device-Signature được mã hóa dạng hex. Giá trị được ký là:

<X-Device-Timestamp>.<raw request body>

X-Device-Timestamp là giá trị UTC theo RFC 3339, không phải số nguyên Unix. X-Device-Event-Id giữ nguyên qua các lần retry của cùng một event, còn X-Device-Delivery-Id nhận diện từng lần delivery riêng lẻ. Nếu signing_secret bị bỏ trống hoặc là chuỗi rỗng, chức năng ký bị tắt và header chữ ký không được gửi.

Hợp đồng này tuân theo mục đích của HMAC được định nghĩa trong RFC 2104: hai bên dùng chung một secret có thể kiểm tra tính toàn vẹn của thông điệp và xác thực bên gửi biết secret đó. HMAC không mã hóa body, không tự xác lập độ mới và không hứa delivery chỉ một lần. Những bảo đảm đó đến từ HTTPS, chính sách timestamp và lưu trữ idempotent bao quanh bước kiểm tra HMAC.

Nếu bạn đang xây dựng một inbound workflow hoàn chỉnh, hướng dẫn webhook WhatsApp với n8n cho thấy event đã ký đi vào luồng tự động hóa thế nào, còn hướng dẫn hàng đợi live DM TikTok giải thích vì sao cùng một envelope đã xác minh cần được lưu trước khi routing.

Xác minh timestamp và raw body trong Node.js

Receiver dưới đây giữ body dưới dạng Buffer, đọc ngưỡng thời gian từ cấu hình triển khai, so sánh binary digest bằng crypto.timingSafeEqual của Node.js và chuyển event đã xác minh vào durable inbox. durableInbox.insertIfAbsent đại diện cho thao tác insert vào database được bảo vệ bởi unique key trên event ID; hãy triển khai nó bằng datastore mà service của bạn đang sử dụng.

import crypto from 'node:crypto';
import express from 'express';

const app = express();
const secret = process.env.WEBHOOK_SIGNING_SECRET;
const maxAgeMs = Number(process.env.WEBHOOK_MAX_AGE_MS);

if (!secret || !Number.isFinite(maxAgeMs) || maxAgeMs <= 0) {
  throw new Error('Configure WEBHOOK_SIGNING_SECRET and WEBHOOK_MAX_AGE_MS');
}

app.post(
  '/webhooks/unifyport',
  express.raw({ type: 'application/json' }),
  async (req, res) => {
    const timestamp = req.get('X-Device-Timestamp') ?? '';
    const signature = req.get('X-Device-Signature') ?? '';
    const eventId = req.get('X-Device-Event-Id') ?? '';

    if (!timestamp || !signature || !eventId) {
      return res.sendStatus(401);
    }

    const signedAtMs = Date.parse(timestamp);
    const ageMs = Math.abs(Date.now() - signedAtMs);
    if (!Number.isFinite(signedAtMs) || ageMs > maxAgeMs) {
      return res.sendStatus(401);
    }

    const expected = crypto
      .createHmac('sha256', secret)
      .update(timestamp + '.')
      .update(req.body)
      .digest();

    const validHex = /^[0-9a-f]{64}$/i.test(signature);
    const provided = validHex ? Buffer.from(signature, 'hex') : Buffer.alloc(0);
    const validSignature =
      provided.length === expected.length &&
      crypto.timingSafeEqual(provided, expected);

    if (!validSignature) {
      return res.sendStatus(401);
    }

    const event = JSON.parse(req.body.toString('utf8'));
    const accepted = await durableInbox.insertIfAbsent({
      id: eventId,
      occurredAt: event.occurred_at,
      payload: event,
    });

    return res.sendStatus(accepted ? 202 : 200);
  },
);

Kiểm tra độ mới xuất hiện trước phép so sánh HMAC, nhưng timestamp chỉ đáng tin sau khi cả hai bước đều thành công. Receiver chỉ loại bỏ sớm input rõ ràng đã quá cũ. Giá trị WEBHOOK_MAX_AGE_MS nên phản ánh độ đồng bộ đồng hồ, độ trễ delivery thông thường, quy trình xử lý sự cố và mô hình rủi ro của bạn. Đừng sao chép ngưỡng từ một provider không liên quan rồi giả định nó phù hợp với hàng đợi của mình.

Tài liệu Node.js xác nhận crypto.timingSafeEqual phù hợp để so sánh HMAC digest, đồng thời cảnh báo code bao quanh cũng phải tránh tạo rò rỉ timing. Hãy xác minh định dạng hex và độ dài byte trước vì timingSafeEqual yêu cầu hai input có cùng độ dài.

Làm cho đường acknowledge an toàn khi retry

UnifyPort coi mọi phản hồi 2xx là acknowledge và bỏ qua response body. Lỗi kết nối cùng HTTP 408, 429 và 5xx có thể được retry tối đa theo retry_policy.max_attempts đã cấu hình cho endpoint; mặc định là ba. Các phản hồi 4xx khác không được retry và event được đưa vào dead-letter.

Hành vi đó dẫn đến thiết kế receiver thực tế sau:

Kết quả tại receiverPhản hồiLý do
Chữ ký thiếu, quá cũ hoặc không hợp lệ401Request không được đi vào hàng đợi tin cậy.
Event đã xác minh và đã được lưu200Có thể acknowledge retry mà không lặp lại công việc.
Event đã xác minh và được insert bền vững202Worker có thể tiếp tục bất đồng bộ sau khi chấp nhận.
Durable inbox tạm thời không khả dụng503Retry an toàn hơn acknowledge một event chưa được lưu.

Hãy dùng unique index trên X-Device-Event-Id thay vì Set cục bộ trong process. Cache cục bộ biến mất khi restart và không phối hợp được nhiều instance receiver. Các hành động downstream cũng cần idempotent: queue consumer có thể dừng sau khi gọi CRM hoặc gửi câu trả lời nhưng trước khi ghi nhận hoàn tất.

Thứ tự delivery không được bảo đảm. Hãy sắp xếp công việc làm thay đổi trạng thái theo occurred_at trong event payload, dùng event ID làm tiêu chí phụ, thay vì giả định thứ tự request HTTP. Điều này đặc biệt quan trọng khi read receipt có thể tới endpoint trước message mà nó tham chiếu.

UnifyPort đóng vai trò gì

UnifyPort giao cùng một standard event envelope qua các kênh được hỗ trợ, gồm id, type, provider, account_id, occurred_atdata riêng cho từng loại event. Vì vậy receiver phía trên bảo vệ một ingress path thay vì sáu handler riêng theo kênh. Chỉ cần đăng ký endpoint một lần, bật signing_secret, subscribe các event cần thiết và áp dụng cùng phép kiểm tra timestamp, chữ ký và idempotency trước khi routing theo provider hoặc type.

Ranh giới quan trọng là lưu trữ: UnifyPort không lưu message history để backfill về sau. Webhook event là bản ghi traffic, vì vậy receiver phải chấp nhận bền vững trước khi trả 2xx. Xác minh chữ ký bảo vệ bước bàn giao; inbox table hoặc queue của bạn giữ lại event.

Giới hạn và đánh đổi

  • HMAC xác thực và bảo vệ tính toàn vẹn; nó không mã hóa JSON body. Luôn dùng HTTPS và bảo vệ log, queue bằng cơ chế riêng.
  • HMAC hợp lệ không ngăn việc xử lý trùng. Bạn vẫn cần kiểm tra độ mới của timestamp và idempotency key.
  • UnifyPort không công bố một ngưỡng timestamp chung cho mọi hệ thống. Cửa sổ ngắn hơn hạn chế khả năng tái sử dụng nhưng kém linh hoạt hơn trước clock drift hoặc delivery chậm.
  • Nếu endpoint tắt chức năng ký, X-Device-Signature sẽ không có. Production receiver yêu cầu xác thực nên từ chối khi header này vắng mặt.
  • Webhook chính thức của từng provider có thể dùng header, encoding hoặc canonical string khác. Hãy tuân theo hợp đồng riêng của từng provider thay vì áp dụng định dạng chuỗi UnifyPort cho mọi nguồn webhook.

Câu hỏi thường gặp

Vì sao chữ ký HMAC webhook của tôi không khớp?

Nguyên nhân phổ biến nhất là xác minh JSON đã parse hoặc serialize lại thay vì raw bytes chính xác. Cũng cần kiểm tra timestamp RFC 3339, dấu chấm phân cách nguyên văn, đúng signing_secret, cách giải mã hex và liệu middleware đã đọc body trước bước xác minh hay chưa.

Chỉ HMAC có ngăn được việc phát lại webhook không?

Không. HMAC chứng minh các byte đã ký khớp với shared secret. Hãy bổ sung kiểm tra độ mới của X-Device-Timestamp và loại trùng bền vững theo X-Device-Event-Id để hạn chế tái sử dụng và xử lý lặp.

Event trùng có nên trả về lỗi không?

Không. Nếu cùng event ID đã được chấp nhận bền vững, hãy trả về 2xx. Trả lỗi chỉ mời thêm một retry hợp lệ mà không cải thiện tính đúng đắn.

Tôi có nên acknowledge trước khi xử lý event không?

Hãy acknowledge sau khi chấp nhận bền vững nhưng trước công việc downstream chậm. Insert event vào inbox dựa trên database hoặc durable queue, trả về 2xx, rồi để worker xử lý ghi CRM, AI hoặc trả lời theo cách idempotent.

Nên dùng cửa sổ thời gian bao lâu?

Hãy chọn và ghi lại một cửa sổ dựa trên đồng hồ đã đồng bộ, độ trễ delivery quan sát được, cách xử lý sự cố và mô hình rủi ro. Hợp đồng chữ ký UnifyPort yêu cầu từ chối timestamp lệch quá xa đồng hồ của bạn nhưng không quy định một giá trị cố định.

Bước tiếp theo

Triển khai chính xác hợp đồng header và retry trong hướng dẫn delivery webhook và xác minh chữ ký. Khi cần kiểm tra từng byte để chẩn đoán mismatch, hãy dùng HMAC Signature Generator làm công cụ hỗ trợ duy nhất.

Nguồn

Các nguồn chính thức được kiểm tra ngày 17 tháng 7 năm 2026: