บทความทั้งหมด

ตรวจสอบความถูกต้องของ Webhook ด้วยลายเซ็น HMAC

อ่าน 2 นาที

ไม่มีรูปภาพ
ตรวจสอบความถูกต้องของ Webhook ด้วยลายเซ็น HMAC

Webhook คือประตูที่เปิดให้โลกภายนอกเรียกเข้ามาที่ระบบของคุณ ถ้าไม่ตรวจว่าใครเป็นผู้ส่ง ใครก็ตามที่รู้ URL สามารถปลอมเหตุการณ์ "ชำระเงินสำเร็จ" ได้ บทความนี้อธิบายกลไกลายเซ็นที่ PPay ใช้กับ Webhook และเหตุผลของแต่ละข้อกำหนด เพื่อให้คุณเขียน handler ที่ปลอดภัยโดยไม่ต้องเดา

ทำไมต้องตรวจลายเซ็น

URL ของ Webhook เป็นที่อยู่สาธารณะ หากผู้ไม่หวังดีส่ง JSON ที่หน้าตาเหมือนผลตรวจสลิป โดยระบุว่า valid เป็น true แล้วระบบคุณเชื่อทันที ออเดอร์อาจถูกอนุมัติโดยไม่มีเงินเข้าจริง ลายเซ็นจึงเป็นข้อพิสูจน์ว่าข้อความมาจากระบบที่รู้ความลับร่วมกัน (คีย์ลายเซ็นขึ้นต้นด้วย whsec_) และเนื้อหาไม่ถูกแก้ระหว่างทาง

รูปแบบลายเซ็นตามคู่มือ

ทุกคำขอมี header X-PPay-Signature ในรูปแบบ t=<unix seconds>,v1=<hex> โดย v1 คือ HMAC-SHA256 ของข้อความ "<t>.<raw body>" ด้วยคีย์ลายเซ็นของคุณ ยังมี header อื่นที่เป็นประโยชน์ เช่น X-PPay-Event (ชนิดเหตุการณ์), X-PPay-Delivery (รหัสการส่ง) และ X-PPay-Attempt (ครั้งที่ส่ง) แต่ความน่าเชื่อถือต้องมาจากลายเซ็นเท่านั้น

คีย์ลายเซ็นแสดงให้เห็นครั้งเดียวตอนตั้งค่า หากหายให้กด "สร้างคีย์ใหม่" ในหน้า API สำหรับนักพัฒนา และเก็บคีย์ไว้ใน environment variable เช่นเดียวกับ API key

สามกฎที่ห้ามพลาด

  1. ใช้ raw body — ต้องคำนวณจากไบต์ที่ได้รับจริงก่อน parse JSON เพราะแค่ช่องว่างหรือลำดับคีย์ที่ต่างไปก็ทำให้ลายเซ็นไม่ตรง ข้อผิดพลาดยอดนิยมคือใช้ middleware ที่ parse JSON ไปก่อนแล้วนำวัตถุมาแปลงกลับเป็นข้อความ
  2. เทียบแบบ constant-time — ใช้ timingSafeEqual (Node.js), hash_equals (PHP) หรือ compare_digest (Python) เพราะการเทียบสตริงตรง ๆ อาจรั่วข้อมูลจากเวลาที่ใช้เทียบ
  3. ตรวจเวลา — ปฏิเสธคำขอที่ค่า t ต่างจากเวลาปัจจุบันเกิน 5 นาที เพื่อกันการนำคำขอเก่ามาส่งซ้ำ (replay) ดังนั้นนาฬิกาของเซิร์ฟเวอร์คุณต้องตรงโดยซิงก์กับ NTP

ตัวอย่างโค้ด Node.js

ตัวอย่างต่อไปนี้เป็นแนวทางตามคู่มือ ใช้ Express พร้อม express.raw เพื่อเก็บ body ตามที่ได้รับ

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.PPAY_WEBHOOK_SECRET; // whsec_...
const app = express();

function verify(raw, header, secret) {
  const parts = Object.fromEntries(String(header).split(",").map((p) => p.split("=")));
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(parts.t + "." + raw).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}

app.post("/webhooks/ppay", express.raw({ type: "application/json" }), (req, res) => {
  if (!verify(req.body.toString("utf8"), req.get("x-ppay-signature"), SECRET)) return res.sendStatus(400);
  const event = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);   // acknowledge first, then work
  // handle event.type here
});

คู่มือมีตัวอย่างสำหรับ PHP (hash_hmac กับ hash_equals และอ่าน body จาก php://input) และ Python Flask (hmac.compare_digest กับ request.get_data()) ในหลักการเดียวกัน

หลังตรวจลายเซ็นผ่านแล้ว

  • ตอบ 2xx ภายใน 8 วินาที ตอบก่อนแล้วค่อยประมวลผลต่อเบื้องหลัง
  • กันซ้ำด้วย id ของเหตุการณ์ หรือ data.jobId เพราะระบบส่งแบบ at-least-once อ่านต่อที่ การรับ webhook ซ้ำและล่าช้า
  • เทียบข้อมูลกับออเดอร์ของคุณ แม้ลายเซ็นถูก ก็ควรตรวจว่า reference มีอยู่จริง ยอดตรง และออเดอร์ยังรอชำระอยู่
  • ตอบ 400 เมื่อลายเซ็นผิด และบันทึก log (ไม่ใส่คีย์ลับลงใน log) เพื่อดูว่ามีความพยายามปลอมหรือไม่

ข้อผิดพลาดที่พบบ่อย

อาการสาเหตุที่เป็นไปได้
ลายเซ็นไม่ตรงทุกครั้งใช้ body ที่ parse แล้ว หรือใช้คีย์ผิดตัว (ใช้ API key แทนคีย์ whsec)
ผ่านบ้างไม่ผ่านบ้างตัวกลางเช่น proxy แก้ไข body หรือแปลงการเข้ารหัสอักขระ
ปฏิเสธเพราะเวลานาฬิกาเครื่องเซิร์ฟเวอร์คลาดเคลื่อนเกิน 5 นาที
ถูกส่งซ้ำตลอดตอบไม่ใช่ 2xx เช่นตอบ redirect ซึ่งระบบไม่ตามการ redirect

เช็กลิสต์

  • เก็บ whsec ใน secret manager
  • อ่าน raw body ก่อน parse
  • เทียบ constant-time
  • ตรวจ t ภายใน 5 นาที
  • กันซ้ำและเทียบกับออเดอร์ก่อนอนุมัติ

ลองทดสอบด้วยปุ่ม "ส่งทดสอบ" ก่อนใช้งานจริง ดูรายละเอียดครบที่ คู่มือ API และอ่านภาพรวมของ Webhook ที่ พื้นฐาน webhook สำหรับการชำระเงิน

ลองตรวจสลิปอัตโนมัติกับร้านของคุณ

สมัครแล้วเริ่มใช้งานได้ภายในไม่กี่นาที

บทความอื่น ๆ