Tài liệu / Webhook/ Xác thực chữ ký Webhook

Xác thực chữ ký Webhook

Webhook Cập nhật: 22/03/2026

HMAC-SHA256 là gì?

HMAC (Hash-based Message Authentication Code) sử dụng SHA-256 để tạo chữ ký số đảm bảo:

  • Tính toàn vẹn — Payload không bị thay đổi trong quá trình truyền
  • Xác thực nguồn gốc — Request đến từ ThueAPI, không phải bên thứ ba

Cơ chế hoạt động

ThueAPI Server:
  signature = HMAC-SHA256(raw_payload_bytes, webhook_secret)
  Header: X-Webhook-Signature: <signature_hex>

Your Server:
  expected  = HMAC-SHA256(raw_payload_bytes, webhook_secret)
  valid     = constant_time_compare(expected, received_signature)
Quan trọng: Luôn dùng so sánh thời gian hằng số (constant-time comparison) để tránh timing attack. Không dùng == hoặc === thông thường.

Các bước xác thực

  1. Lấy raw body của request (chưa parse JSON)
  2. Tính HMAC-SHA256 của raw body với Webhook Secret
  3. So sánh kết quả với header X-Webhook-Signature bằng constant-time compare
  4. Từ chối nếu không khớp (HTTP 401)

Code xác thực theo ngôn ngữ

PHP

<?php
function verifyWebhookSignature(string $payload, string $signature, string $secret): bool
{
    // Tính HMAC-SHA256 từ raw body
    $expected = hash_hmac('sha256', $payload, $secret);

    // So sánh thời gian hằng số — KHÔNG dùng $expected === $signature
    return hash_equals($expected, $signature);
}

// Sử dụng trong webhook handler
$payload   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
$secret    = 'YOUR_WEBHOOK_SECRET';

if (!verifyWebhookSignature($payload, $signature, $secret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Xử lý webhook...
echo json_encode(['success' => true]);

Node.js

const crypto = require('crypto');

/**
 * Xác thực chữ ký webhook từ ThueAPI.VN
 * @param {Buffer} rawBody - Raw request body (chưa parse)
 * @param {string} signature - Giá trị header X-Webhook-Signature
 * @param {string} secret - Webhook secret từ Dashboard
 */
function verifyWebhookSignature(rawBody, signature, secret) {
    const expected = crypto
        .createHmac('sha256', secret)
        .update(rawBody)
        .digest('hex');

    // timingSafeEqual yêu cầu cùng độ dài
    const expectedBuf = Buffer.from(expected);
    const receivedBuf = Buffer.from(signature);

    if (expectedBuf.length !== receivedBuf.length) return false;

    // Constant-time comparison
    return crypto.timingSafeEqual(expectedBuf, receivedBuf);
}

// Sử dụng với Express
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
    const isValid = verifyWebhookSignature(
        req.body,
        req.headers['x-webhook-signature'],
        process.env.WEBHOOK_SECRET
    );

    if (!isValid) return res.status(401).json({ error: 'Invalid signature' });

    res.json({ success: true });
});

Python

import hmac
import hashlib

def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
    """
    Xác thực chữ ký HMAC-SHA256 từ ThueAPI.VN

    Args:
        payload: Raw request body (bytes)
        signature: Giá trị header X-Webhook-Signature
        secret: Webhook secret từ Dashboard
    """
    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    # hmac.compare_digest thực hiện constant-time comparison
    return hmac.compare_digest(expected, signature)

# Sử dụng với Flask
from flask import request, jsonify

@app.route('/webhook', methods=['POST'])
def webhook():
    payload   = request.get_data()  # Raw bytes
    signature = request.headers.get('X-Webhook-Signature', '')

    if not verify_webhook_signature(payload, signature, 'YOUR_WEBHOOK_SECRET'):
        return jsonify({'error': 'Invalid signature'}), 401

    return jsonify({'success': True})

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

// VerifyWebhookSignature xác thực chữ ký HMAC-SHA256 từ ThueAPI.VN
func VerifyWebhookSignature(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))

    // hmac.Equal thực hiện constant-time comparison
    return hmac.Equal([]byte(expected), []byte(signature))
}

C#

using System.Security.Cryptography;
using System.Text;

/// <summary>
/// Xác thực chữ ký HMAC-SHA256 từ ThueAPI.VN
/// </summary>
public static bool VerifyWebhookSignature(string payload, string signature, string secret)
{
    var key  = Encoding.UTF8.GetBytes(secret);
    using var hmac = new HMACSHA256(key);
    var hash     = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
    var expected = Convert.ToHexString(hash).ToLower();

    // FixedTimeEquals thực hiện constant-time comparison
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(expected),
        Encoding.UTF8.GetBytes(signature)
    );
}

Java

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;

/**
 * Xác thực chữ ký HMAC-SHA256 từ ThueAPI.VN
 */
public static boolean verifyWebhookSignature(String payload, String signature, String secret)
        throws Exception {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
    String expected = HexFormat.of().formatHex(hash);

    // MessageDigest.isEqual thực hiện constant-time comparison
    return MessageDigest.isEqual(expected.getBytes(), signature.getBytes());
}

Checksum per-transaction

Mỗi giao dịch có trường checksum là mã duy nhất được sinh ra từ nguồn dữ liệu gốc. Dùng để:

  • Xác định giao dịch duy nhất — Kiểm tra trùng lặp khi webhook được gửi lại (retry)
  • Đối chiếu — So khớp với dữ liệu ngân hàng
// PHP: Kiểm tra trùng lặp bằng checksum
$exists = Transaction::where('checksum', $tx['checksum'])->exists();
if ($exists) {
    return response()->json(['success' => true]); // Bỏ qua, đã xử lý
}

Best Practices bảo mật

  • Lưu Webhook Secret trong biến môi trường, không hardcode trong code
  • Luôn xác thực chữ ký trước khi xử lý bất kỳ dữ liệu nào
  • Sử dụng constant-time comparison để tránh timing attack
  • Log các request có chữ ký không hợp lệ để phát hiện tấn công
  • Whitelist IP của ThueAPI nếu có thể (hỏi support để lấy danh sách IP)
ThueAPI.VN
Đăng nhập với Google
hoặc đăng nhập bằng email
Quên mật khẩu?

Chưa có tài khoản?

Đăng ký với Google
hoặc đăng ký bằng email

Bằng cách đăng ký, bạn đồng ý với Điều khoản dịch vụChính sách bảo mật của chúng tôi.

Đã có tài khoản?