ตัวอย่าง Signature และการตรวจสอบ Callback
หน้านี้มีตัวอย่างการทำงานจริงเพื่อช่วยให้คุณเข้าใจวิธี สร้าง Signature สำหรับ request และ ตรวจสอบ Signature ของ Callback
สำคัญ: คุณต้องใช้ raw HTTP body ในการตรวจสอบ Signature การใช้
json_encode()กับข้อมูลที่ parse แล้วจะสร้าง Signature ที่แตกต่างเนื่องจากรูปแบบ JSON ที่ต่างกัน
ข้อมูลรับรองสำหรับทดสอบ
สำหรับการทดสอบ ใช้:
| พารามิเตอร์ | ค่า |
|---|---|
| merchant_id | TH00000000 |
| secret_key | aaaaaaaaaaaaaaaaaaaa |
ส่วนที่ 1: จำลองการส่ง Callback (ฝั่งผู้ส่ง)
สคริปต์เหล่านี้จำลองการที่ระบบส่ง Callback ไปยังเซิร์ฟเวอร์ของคุณ
Node.js (ฝั่งผู้ส่ง)
บันทึกเป็น simulate-callback.js:
const crypto = require('crypto');
const http = require('http');
// ข้อมูลรับรองสำหรับทดสอบ
const MERCHANT_ID = 'TH00000000';
const SECRET_KEY = 'aaaaaaaaaaaaaaaaaaaa';
// Callback URL (เซิร์ฟเวอร์ของคุณ)
const CALLBACK_HOST = 'localhost';
const CALLBACK_PORT = 3000;
const CALLBACK_PATH = '/callback';
// จำลองข้อมูล Callback
const callbackData = {
merchant_id: MERCHANT_ID,
platform_order_id: 'THBP2024042821130000000001',
client_order_id: 'ORDER123456789',
mode: 'PAYMENT',
amount: '1000.00',
status: 'PAID',
timestamp: Math.floor(Date.now() / 1000)
};
// แปลงเป็น JSON string (นี่คือ raw body)
const rawBody = JSON.stringify(callbackData);
// คำนวณ Signature จาก raw body
const signature = crypto
.createHmac('sha256', SECRET_KEY)
.update(rawBody)
.digest('hex');
console.log('=== จำลองการส่ง Callback ===');
console.log('Raw Body:', rawBody);
console.log('Signature:', signature);
console.log('');
// ส่ง HTTP request
const options = {
hostname: CALLBACK_HOST,
port: CALLBACK_PORT,
path: CALLBACK_PATH,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Signature': signature
}
};
const req = http.request(options, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
console.log('Response Status:', res.statusCode);
console.log('Response Body:', data);
});
});
req.on('error', (e) => {
console.error('Error:', e.message);
console.log('ตรวจสอบให้แน่ใจว่า callback server ทำงานอยู่บน port', CALLBACK_PORT);
});
req.write(rawBody);
req.end();
PHP (ฝั่งผู้ส่ง)
บันทึกเป็น simulate-callback.php:
<?php
// ข้อมูลรับรองสำหรับทดสอบ
$merchant_id = 'TH00000000';
$secret_key = 'aaaaaaaaaaaaaaaaaaaa';
// Callback URL (เซิร์ฟเวอร์ของคุณ)
$callback_url = 'http://localhost:8080/callback.php';
// จำลองข้อมูล Callback
$callback_data = [
'merchant_id' => $merchant_id,
'platform_order_id' => 'THBP2024042821130000000001',
'client_order_id' => 'ORDER123456789',
'mode' => 'PAYMENT',
'amount' => '1000.00',
'status' => 'PAID',
'timestamp' => time()
];
// แปลงเป็น JSON string (นี่คือ raw body)
$raw_body = json_encode($callback_data);
// คำนวณ Signature จาก raw body
$signature = hash_hmac('SHA256', $raw_body, $secret_key);
echo "=== จำลองการส่ง Callback ===\n";
echo "Raw Body: " . $raw_body . "\n";
echo "Signature: " . $signature . "\n\n";
// ส่ง HTTP request
$ch = curl_init($callback_url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $raw_body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Signature: ' . $signature
]
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
echo "Error: " . $error . "\n";
echo "ตรวจสอบให้แน่ใจว่า callback server ทำงานอยู่\n";
} else {
echo "Response Status: " . $http_code . "\n";
echo "Response Body: " . $response . "\n";
}
รัน: php simulate-callback.php
ส่วนที่ 2: รับและตรวจสอบ Callback (ฝั่งเซิร์ฟเวอร์)
Node.js + Express (ฝั่งเซิร์ฟเวอร์)
บันทึกเป็น callback-server.js:
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = 3000;
const SECRET_KEY = 'aaaaaaaaaaaaaaaaaaaa';
// สำคัญ: เราต้องเก็บ raw body ไว้สำหรับตรวจสอบ Signature
// เก็บ raw body ก่อนที่จะ parse JSON
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
app.post('/callback', (req, res) => {
console.log('\n=== ได้รับ Callback ===');
const receivedSignature = req.headers['x-signature'];
const rawBody = req.rawBody;
const parsedData = req.body;
console.log('Signature ที่ได้รับ:', receivedSignature);
console.log('Raw Body:', rawBody);
console.log('ข้อมูลที่ parse แล้ว:', JSON.stringify(parsedData, null, 2));
// =====================================================
// วิธีที่ถูกต้อง: ใช้ raw body ในการคำนวณ Signature
// =====================================================
const correctSignature = crypto
.createHmac('sha256', SECRET_KEY)
.update(rawBody) // ใช้ raw body!
.digest('hex');
console.log('\n--- การตรวจสอบที่ถูกต้อง (ใช้ raw body) ---');
console.log('Signature ที่คำนวณ:', correctSignature);
console.log('ตรงกัน:', correctSignature === receivedSignature ? 'ใช่' : 'ไม่ใช่');
// =====================================================
// วิธีที่ผิด: encode JSON ใหม่จากข้อมูลที่ parse แล้ว
// อาจสร้าง JSON ที่มีรูปแบบต่างกัน!
// =====================================================
const reEncodedBody = JSON.stringify(parsedData);
const wrongSignature = crypto
.createHmac('sha256', SECRET_KEY)
.update(reEncodedBody) // ผิด: ใช้ body ที่ encode ใหม่
.digest('hex');
console.log('\n--- การตรวจสอบที่ผิด (encode JSON ใหม่) ---');
console.log('Body ที่ encode ใหม่:', reEncodedBody);
console.log('Signature ที่คำนวณ:', wrongSignature);
console.log('ตรงกัน:', wrongSignature === receivedSignature ? 'ใช่' : 'ไม่ใช่');
// ในตัวอย่างนี้อาจตรงกัน แต่ลองคิดถึงกรณี:
// - ลำดับ key ใน JSON ต่างกัน
// - ช่องว่าง/รูปแบบต่างกัน
// - การ escape Unicode ต่างกัน
// - รูปแบบตัวเลขต่างกัน (1000 vs 1000.00)
// ตรวจสอบ Signature (วิธีที่ถูกต้อง)
if (correctSignature === receivedSignature) {
console.log('\n*** SIGNATURE ถูกต้อง ***');
// ประมวลผล Callback
console.log('กำลังดำเนินการชำระเงิน:', parsedData.client_order_id);
console.log('จำนวนเงิน:', parsedData.amount);
console.log('สถานะ:', parsedData.status);
res.status(200).json({ success: true, message: 'Callback processed' });
} else {
console.log('\n*** SIGNATURE ไม่ถูกต้อง ***');
res.status(403).json({ error: 'Invalid signature' });
}
});
app.listen(PORT, () => {
console.log(`Callback server ทำงานอยู่ที่ http://localhost:${PORT}`);
console.log('รอรับ Callback ที่ POST /callback');
});
ติดตั้งและรัน:
npm init -y
npm install express
node callback-server.js
PHP (ฝั่งเซิร์ฟเวอร์)
บันทึกเป็น callback.php:
<?php
header('Content-Type: application/json');
$secret_key = 'aaaaaaaaaaaaaaaaaaaa';
echo "=== ได้รับ Callback ===\n";
// รับ Signature จาก header
$received_signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
// =====================================================
// สำคัญ: รับ RAW body ก่อนดำเนินการใดๆ
// =====================================================
$raw_body = file_get_contents('php://input');
// Parse JSON สำหรับการประมวลผล
$parsed_data = json_decode($raw_body, true);
echo "Signature ที่ได้รับ: " . $received_signature . "\n";
echo "Raw Body: " . $raw_body . "\n";
echo "ข้อมูลที่ parse แล้ว: " . print_r($parsed_data, true) . "\n";
// =====================================================
// วิธีที่ถูกต้อง: ใช้ raw body ในการคำนวณ Signature
// =====================================================
$correct_signature = hash_hmac('SHA256', $raw_body, $secret_key);
echo "\n--- การตรวจสอบที่ถูกต้อง (ใช้ raw body) ---\n";
echo "Signature ที่คำนวณ: " . $correct_signature . "\n";
echo "ตรงกัน: " . ($correct_signature === $received_signature ? 'ใช่' : 'ไม่ใช่') . "\n";
// =====================================================
// วิธีที่ผิด: encode JSON ใหม่จากข้อมูลที่ parse แล้ว
// อาจสร้าง JSON ที่มีรูปแบบต่างกัน!
// =====================================================
$re_encoded_body = json_encode($parsed_data);
$wrong_signature = hash_hmac('SHA256', $re_encoded_body, $secret_key);
echo "\n--- การตรวจสอบที่ผิด (encode JSON ใหม่) ---\n";
echo "Body ที่ encode ใหม่: " . $re_encoded_body . "\n";
echo "Signature ที่คำนวณ: " . $wrong_signature . "\n";
echo "ตรงกัน: " . ($wrong_signature === $received_signature ? 'ใช่' : 'ไม่ใช่') . "\n";
// ตรวจสอบ Signature (วิธีที่ถูกต้อง)
if (hash_equals($correct_signature, $received_signature)) {
echo "\n*** SIGNATURE ถูกต้อง ***\n";
// ประมวลผล Callback
echo "กำลังดำเนินการชำระเงิน: " . $parsed_data['client_order_id'] . "\n";
echo "จำนวนเงิน: " . $parsed_data['amount'] . "\n";
echo "สถานะ: " . $parsed_data['status'] . "\n";
http_response_code(200);
echo json_encode(['success' => true, 'message' => 'Callback processed']);
} else {
echo "\n*** SIGNATURE ไม่ถูกต้อง ***\n";
http_response_code(403);
echo json_encode(['error' => 'Invalid signature']);
}
รันด้วย PHP built-in server:
php -S localhost:8080
ส่วนที่ 3: สาธิต - ทำไม Raw Body ถึงสำคัญ
ตัวอย่างนี้แสดงให้เห็นว่าทำไมการ encode JSON ใหม่ถึงทำให้การตรวจสอบ Signature ผิดพลาด:
Node.js สาธิต
บันทึกเป็น demo-difference.js:
const crypto = require('crypto');
const SECRET_KEY = 'aaaaaaaaaaaaaaaaaaaa';
// JSON ต้นฉบับจากผู้ส่ง (มีรูปแบบเฉพาะ)
const originalJson = '{"merchant_id":"TH00000000","amount":"1000.00","status":"PAID"}';
// Parse แล้ว encode ใหม่
const parsed = JSON.parse(originalJson);
const reEncoded = JSON.stringify(parsed);
// เพิ่มช่องว่าง (พบบ่อยใน pretty-printed JSON)
const prettyJson = JSON.stringify(parsed, null, 2);
console.log('=== เปรียบเทียบ JSON ===');
console.log('ต้นฉบับ :', originalJson);
console.log('Encode ใหม่:', reEncoded);
console.log('Pretty :', prettyJson.replace(/\n/g, '\\n'));
console.log('');
// คำนวณ Signature
const sigOriginal = crypto.createHmac('sha256', SECRET_KEY).update(originalJson).digest('hex');
const sigReEncoded = crypto.createHmac('sha256', SECRET_KEY).update(reEncoded).digest('hex');
const sigPretty = crypto.createHmac('sha256', SECRET_KEY).update(prettyJson).digest('hex');
console.log('=== Signatures ===');
console.log('ต้นฉบับ :', sigOriginal);
console.log('Encode ใหม่:', sigReEncoded);
console.log('Pretty :', sigPretty);
console.log('');
console.log('ต้นฉบับ === Encode ใหม่:', sigOriginal === sigReEncoded);
console.log('ต้นฉบับ === Pretty :', sigOriginal === sigPretty);
// ตัวอย่างเมื่อลำดับ key ต่างกัน
const differentOrder = '{"amount":"1000.00","merchant_id":"TH00000000","status":"PAID"}';
const sigDifferentOrder = crypto.createHmac('sha256', SECRET_KEY).update(differentOrder).digest('hex');
console.log('');
console.log('=== ลำดับ Key มีผล ===');
console.log('ต้นฉบับ :', originalJson);
console.log('ลำดับต่างกัน :', differentOrder);
console.log('Signature ตรงกัน:', sigOriginal === sigDifferentOrder);
รัน: node demo-difference.js
ผลลัพธ์ที่คาดหวัง:
=== เปรียบเทียบ JSON ===
ต้นฉบับ : {"merchant_id":"TH00000000","amount":"1000.00","status":"PAID"}
Encode ใหม่: {"merchant_id":"TH00000000","amount":"1000.00","status":"PAID"}
Pretty : {\n "merchant_id": "TH00000000",\n "amount": "1000.00",\n "status": "PAID"\n}
=== Signatures ===
ต้นฉบับ : abc123...
Encode ใหม่: abc123...
Pretty : xyz789... <- ต่างกัน!
ต้นฉบับ === Encode ใหม่: true
ต้นฉบับ === Pretty : false
=== ลำดับ Key มีผล ===
ต้นฉบับ : {"merchant_id":"TH00000000","amount":"1000.00","status":"PAID"}
ลำดับต่างกัน : {"amount":"1000.00","merchant_id":"TH00000000","status":"PAID"}
Signature ตรงกัน: false <- ลำดับ KEY ต่างกัน = SIGNATURE ต่างกัน!
สรุป
| วิธีการ | ผลลัพธ์ |
|---|---|
ใช้ file_get_contents('php://input') หรือ req.rawBody | ถูกต้อง |
ใช้ json_encode($parsed_data) หรือ JSON.stringify(parsed) | ผิด (อาจใช้ได้โดยบังเอิญ) |
ประเด็นสำคัญ
- เก็บ raw HTTP body ไว้เสมอ ก่อน parse JSON
- อย่า encode ใหม่ สำหรับ JSON ที่ parse แล้วเพื่อตรวจสอบ Signature
- JSON ไม่มีรูปแบบมาตรฐาน (canonical) - ข้อมูลเดียวกันอาจมี string ที่แตกต่างกัน:
- ลำดับ key อาจต่างกัน
- ช่องว่าง/รูปแบบอาจต่างกัน
- การ escape Unicode อาจต่างกัน
- รูปแบบตัวเลขอาจต่างกัน (1000 vs 1000.0)
อ้างอิงด่วน
PHP:
$raw_body = file_get_contents('php://input');
$signature = hash_hmac('SHA256', $raw_body, $secret_key);
Node.js (Express):
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString();
}
}));
// ใน route handler:
const signature = crypto.createHmac('sha256', SECRET_KEY)
.update(req.rawBody)
.digest('hex');