Cách xác minh webhook Qvard

Hướng dẫn nhanh về tiêu đề X-Qvard-Signature: nó là gì và cách xác minh nó bằng một vài dòng mã.

Chữ ký là gì

Khi trạng thái thanh toán thay đổi, Qvard gửi thông báo (webhook) đến địa chỉ trong cài đặt cửa hàng của bạn. Bất cứ ai cũng có thể gửi yêu cầu đến địa chỉ đó, vì vậy bạn cần cách phân biệt thông báo thực từ thông báo giả.

Chữ ký là bằng chứng đó: một mã ngắn mà Qvard tính từ tin nhắn và khóa bí mật của bạn. Chỉ Qvard và bạn biết khóa đó, vì vậy chỉ Qvard mới có thể tạo ra mã đúng.

Bạn nhận được gì

Mỗi webhook đều mang một tiêu đề bổ sung và một nội dung JSON nhỏ:

X-Qvard-Signature: t=1760950000,v1=3f2a…c91e {"id":"order_1024","status":"paid"}

t là lúc tin nhắn được gửi (Unix, giây). v1 là mã. Trong 24 giờ sau khi bạn xoay khóa bí mật, tiêu đề mang hai giá trị v1: một cho khóa mới và một cho khóa cũ.

Các trường id và status giữ nguyên như trước. Thêm bốn trường số tiền vào cùng nội dung đã ký:

  • amount và currency: số tiền và đơn vị tiền như bạn đã gửi trong yêu cầu.
  • amountUsd: số tiền hạch toán bằng USD.
  • rate: số đơn vị tiền ứng với 1 USD tại thời điểm tạo thanh toán.
  • Tích hợp cũ vẫn chạy bình thường: chúng đọc id và status, bỏ qua các trường mới. Chữ ký vẫn được kiểm tra trên cùng nội dung như trước.

Cách kiểm tra nó

  1. 1
    Lấy nội dung yêu cầu chính xác như nó đến: văn bản thô, trước khi phân tích cú pháp JSON.
  2. 2
    Đọc t và mỗi v1 từ tiêu đề X-Qvard-Signature.
  3. 3
    Tính HMAC-SHA256 của văn bản t + "." + nội dung với khóa bí mật của bạn (qvard_sk_…) và viết dưới dạng số thập lục phân chữ thường.
  4. 4
    Nếu kết quả của bạn khớp với bất kỳ v1 (so sánh trong thời gian không đổi), tin nhắn là chính hãng.
  5. 5
    Kiểm tra rằng t không cũ hơn 5 phút, vì vậy một tin nhắn cũ không thể được phát lại.
  6. 6
    Chỉ sau đó mới sử dụng dữ liệu và xác nhận số tiền được trả cho đơn hàng đó trong cơ sở dữ liệu của riêng bạn.

Mã sẵn sàng

const crypto = require('crypto');

function verifyQvardSignature(rawBody, header, secret, toleranceSec = 300) {
  const parts = header.split(',').map((p) => p.trim());
  const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2));
  const sigs = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3));
  if (!t || sigs.length === 0) return false;
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  return sigs.some((s) => {
    const got = Buffer.from(s, 'hex');
    return got.length === expected.length && crypto.timingSafeEqual(got, expected);
  });
}

// Use the RAW body: express.raw, not express.json
app.post('/qvard-webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifyQvardSignature(raw, req.get('X-Qvard-Signature') || '', process.env.QVARD_SECRET_KEY)) {
    return res.sendStatus(400);
  }
  const { id, status } = JSON.parse(raw);
  // Confirm the paid amount for order `id` in your own database before fulfilling it.
  res.sendStatus(200);
});

Trong 24 giờ sau khi bạn xoay khóa bí mật, tiêu đề mang hai giá trị v1: chấp nhận webhook nếu bất kỳ giá trị nào khớp.

Những lỗi phổ biến

  • Phân tích cú pháp JSON và tuần tự hóa lại thay đổi văn bản. Luôn sử dụng nội dung thô.
  • Sử dụng khóa công khai (qvard_pk_…) thay vì khóa bí mật.
  • Đồng hồ máy chủ không đồng bộ làm cho việc kiểm tra 5 phút thất bại. Giữ nó đồng bộ.
  • So sánh với == thay vì so sánh thời gian không đổi.

Nơi tìm khóa bí mật của bạn

Trong bảng điều khiển: cửa hàng của bạn, Cài đặt, Khóa API. Chỉ giữ nó trên máy chủ của bạn. Nếu nó bị rò rỉ, hãy xoay nó ở đó: khóa cũ vẫn hoạt động trong 24 giờ để thanh toán (không để rút tiền).

Mở bảng điều khiển

Tài liệu tham khảo API

Khóa cũ

Nếu cửa hàng của bạn vẫn có khóa định dạng cũ (không có tiền tố qvard_sk_), webhook cũng mang tiêu đề x-api-key với khóa đó. Sau khi bạn xoay nó, tiêu đề đó biến mất. Chuyển sang kiểm tra chữ ký.

Muốn thử cửa sổ thanh toán?

Mở demo