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
- Lấy raw body của request (chưa parse JSON)
- Tính HMAC-SHA256 của raw body với Webhook Secret
- So sánh kết quả với header
X-Webhook-Signaturebằng constant-time compare - 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)