ตัวอย่าง Signature และการตรวจสอบ Callback

หน้านี้มีตัวอย่างการทำงานจริงเพื่อช่วยให้คุณเข้าใจวิธี สร้าง Signature สำหรับ request และ ตรวจสอบ Signature ของ Callback

สำคัญ: คุณต้องใช้ raw HTTP body ในการตรวจสอบ Signature การใช้ json_encode() กับข้อมูลที่ parse แล้วจะสร้าง Signature ที่แตกต่างเนื่องจากรูปแบบ JSON ที่ต่างกัน


ข้อมูลรับรองสำหรับทดสอบ

สำหรับการทดสอบ ใช้:

พารามิเตอร์ค่า
merchant_idTH00000000
secret_keyaaaaaaaaaaaaaaaaaaaa

ส่วนที่ 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)ผิด (อาจใช้ได้โดยบังเอิญ)

ประเด็นสำคัญ

  1. เก็บ raw HTTP body ไว้เสมอ ก่อน parse JSON
  2. อย่า encode ใหม่ สำหรับ JSON ที่ parse แล้วเพื่อตรวจสอบ Signature
  3. 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');