กลับไปหน้าบทความ
#ระบบชำระเงิน#Webhook#Idempotency#ความปลอดภัยเว็บ#ใบเสร็จ

ทำระบบชำระเงินบนเว็บไซต์ให้ปลอดภัยแบบทีละขั้น

การทำระบบรับชำระเงินที่ปลอดภัยไม่ได้จบแค่พาผู้ใช้ไปหน้า Gateway แต่ต้องออกแบบตั้งแต่โครงสร้างข้อมูล Webhook, Idempotency, Signature Verification ไปจนถึงการออกใบเสร็จและอัปเดตสถานะออเดอร์อย่างถูกต้อง. บทความนี้สรุปแนวทางส

30 มกราคม 2569อ่านประมาณ 3 นาที

แชร์บทความ

ทำระบบชำระเงินบนเว็บไซต์ให้ปลอดภัยแบบทีละขั้น

ทำระบบชำระเงินบนเว็บไซต์ให้ปลอดภัยแบบทีละขั้น

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

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

1) เริ่มจากการออกแบบภาพรวมของระบบ

ก่อนเขียนโค้ด ควรมองภาพรวมของการชำระเงินให้ชัดเจน โดยทั่วไปจะมี 2 เส้นทางหลักเสมอ

  • เส้นทาง A: ผู้ใช้กดจ่าย แล้วถูกพาไปหน้า Gateway หรือเปิดหน้าชำระเงินในเว็บไซต์
  • เส้นทาง B: ระบบของเราได้รับผลยืนยันจริงผ่าน Webhook จากผู้ให้บริการ

จุดสำคัญคือ เส้นทาง B สำคัญกว่าเส้นทาง A เพราะเป็นข้อมูลจากผู้ให้บริการที่ส่งเข้ามาโดยตรง ส่วนหน้า Thank you หรือ Success ใช้เพื่อสื่อสารกับผู้ใช้ได้ แต่ไม่ควรนำมาเป็นตัวตัดสินว่าชำระสำเร็จแล้ว

2) ออกแบบฐานข้อมูลให้รองรับเหตุการณ์ซ้ำและมาช้า

ระบบชำระเงินมักเจอกรณี Webhook ส่งซ้ำ ผู้ใช้กดปุ่มซ้ำ หรือข้อมูลเดินทางมาถึงล่าช้า ดังนั้นโครงสร้างข้อมูลต้องรองรับเรื่องเหล่านี้ตั้งแต่ต้น

ตารางที่ควรมี ได้แก่

  • orders สำหรับเก็บสถานะออเดอร์และยอดรวม
  • payments สำหรับเก็บข้อมูลการชำระเงินแต่ละครั้ง เพราะ 1 ออเดอร์อาจมีหลาย attempt
  • payment_events หรือ webhook_logs สำหรับเก็บ payload ดิบที่ได้รับจาก Gateway เพื่อใช้ audit
  • receipts สำหรับเก็บเลขใบเสร็จหรือเลขเอกสารที่ใช้จริงตามข้อกำหนดขององค์กรหรือกฎหมาย

หลักคิดที่สำคัญคือ แยก “ความจริงจาก Gateway” ออกจาก “สถานะที่ UI อยากเห็น” เพื่อป้องกันการปนกันของข้อมูลธุรกิจกับข้อมูลที่ใช้แสดงผล

3) กำหนดสถานะออเดอร์ให้ชัดและไม่สับสน

การตั้งสถานะที่ชัดเจนจะช่วยลดบั๊กและลดความกำกวมในระบบ ตัวอย่างสถานะที่ใช้ได้จริง เช่น

  • CREATED รอจ่าย
  • PENDING กำลังรอผล หลังจากสร้าง charge แล้ว
  • PAID จ่ายสำเร็จ
  • FAILED จ่ายไม่สำเร็จ
  • CANCELLED ยกเลิก
  • REFUNDED คืนเงินแล้ว

กติกาที่ควรยึดเป็นหลักคือ สถานะ PENDING ควรเปลี่ยนเป็น PAID จาก Webhook เป็นหลัก ไม่ใช่จากการ redirect กลับมาที่หน้าเว็บ

4) ใช้ Idempotency ตั้งแต่ตอนสร้าง Payment Intent หรือ Charge

เวลาระบบของเรายิง API ไปที่ Gateway ควรส่ง idempotency key ไปทุกครั้ง เพื่อกันการสร้างรายการซ้ำจากการกดซ้ำ รีเฟรช หรือปัญหาเครือข่าย

ตัวอย่างรูปแบบของ key เช่น

  • orderId + attemptNo
  • orderId + hash(cart)

และควรเก็บ idempotency_key นี้ลงในตาราง payments ด้วย เพื่อใช้ตรวจสอบย้อนหลังและเชื่อมโยงเหตุการณ์ให้ชัดเจน

5) อย่าเชื่อยอดเงินจากฝั่ง client

ยอดเงินจริงต้องถูกคำนวณจากฝั่ง server เท่านั้น โดยใช้ข้อมูลจากฐานข้อมูล เช่น

  • ราคาสินค้า
  • ส่วนลด
  • ภาษี
  • ค่าจัดส่ง

ฝั่ง client ควรส่งมาเพียง orderId หรือ token ที่ไม่เปิดเผยตัวเลข ไม่ควรเปิดช่องให้ผู้ใช้แก้ไขยอดผ่าน devtools หรือการดัดแปลง request เอง

6) หน้าชำระเงินมีหน้าที่เริ่มจ่าย ไม่ใช่ยืนยันว่าจ่ายแล้ว

หลังจากสร้าง charge หรือ payment intent แล้ว ระบบสามารถพาผู้ใช้ไปหน้า Gateway หรือเปิด checkout ได้ตามรูปแบบที่ใช้งาน แต่เมื่อผู้ใช้กลับมาที่หน้า success ควรแสดงข้อความลักษณะว่า

  • “ระบบกำลังตรวจสอบการชำระเงิน”
  • “กรุณารอสถานะยืนยันจากระบบ”

จากนั้นให้หน้าเว็บคอยดึงสถานะออเดอร์จาก server เป็นระยะ หรือใช้ websocket เพื่ออัปเดตผลแบบเรียลไทม์

7) ออกแบบ Webhook Endpoint ให้ปลอดภัยเหมือนประตูนิรภัย

Webhook คือจุดสำคัญที่สุดของระบบ จึงควรออกแบบอย่างระมัดระวัง เช่น

  • ใช้เส้นทางแยกชัดเจน เช่น /webhooks/payment
  • ไม่ต้องใช้ session login
  • รับเฉพาะ POST
  • จำกัดขนาด request body
  • บันทึก raw body เพื่อใช้ตรวจสอบลายเซ็น
  • ใช้ HTTPS เท่านั้น

แนวทางนี้ช่วยลดพื้นที่โจมตีและเพิ่มความน่าเชื่อถือของข้อมูลที่รับเข้ามา

8) ตรวจสอบ Signature Verification ให้ถูกวิธี

ผู้ให้บริการชำระเงินมักมี secret สำหรับใช้ตรวจสอบว่า Webhook ที่ส่งมานั้นเป็นของจริง วิธีที่ถูกต้องคือคำนวณ HMAC จาก raw body แล้วเปรียบเทียบกับค่าใน header ที่ผู้ให้บริการส่งมา

ข้อควรระวังคือ ห้าม parse JSON แล้ว stringify ใหม่ก่อนตรวจลายเซ็น เพราะรูปแบบข้อความอาจเปลี่ยน ทำให้ลายเซ็นไม่ตรง

หากตรวจสอบแล้ว signature ไม่ผ่าน ควร

  • ตอบกลับ 401
  • ไม่แตะต้องสถานะออเดอร์
  • บันทึกเหตุการณ์ไว้เพื่อตรวจสอบ

9) ทำ Idempotency ฝั่ง Webhook ซ้ำอีกชั้น

แม้จะมี idempotency ตอนสร้าง charge แล้ว แต่ฝั่ง Webhook ก็ยังต้องมี idempotency อีก เพราะผู้ให้บริการหลายรายออกแบบให้ส่งซ้ำได้เป็นเรื่องปกติ

วิธีที่แนะนำคือ

  • สร้างตาราง webhook_logs
  • เก็บ event_id ที่มาจาก Gateway
  • ตั้ง unique constraint บน event_id

หาก event เดิมถูกส่งเข้ามาอีก ให้ตอบ 200 ทันทีโดยไม่ทำงานซ้ำ นี่เป็นกันชนสำคัญที่ช่วยป้องกันการอัปเดตข้อมูลซ้ำ การออกใบเสร็จซ้ำ หรือการส่งอีเมลซ้ำ

10) ตรวจสอบความถูกต้องของข้อมูลการชำระก่อนอัปเดตสถานะ

ก่อนจะเปลี่ยนสถานะออเดอร์เป็น PAID ควรตรวจสอบข้อมูลอย่างน้อย 5 จุด ได้แก่

  • reference ต้องตรงกับ orderId ที่ระบบสร้างไว้
  • amount ต้องตรงกับ order.total และควรเทียบในหน่วยเล็ก เช่น satang หรือ cent
  • currency ต้องตรง
  • status ต้องเป็นค่าที่สเปกระบุว่า success หรือ paid
  • merchant/account id ต้องเป็นของเราจริง

หากข้อมูลไม่ผ่านการตรวจ ควร mark เหตุการณ์นั้นเป็น suspicious และแจ้งเตือนทีมที่เกี่ยวข้องทันที

11) อัปเดตสถานะด้วย transaction และ row lock

เมื่อได้รับ Webhook ว่าชำระสำเร็จ กระบวนการอัปเดตข้อมูลควรทำภายใต้ database transaction เพื่อป้องกัน race condition โดยลำดับที่เหมาะสมคือ

  1. เริ่ม transaction
  2. SELECT ... FOR UPDATE ล็อกแถวของ order
  3. ตรวจสอบว่า order เป็น PAID อยู่แล้วหรือไม่
  4. ถ้ายังไม่ PAID ให้สร้าง receipt
  5. อัปเดต order เป็น PAID
  6. commit

วิธีนี้ช่วยให้ระบบปลอดภัยแม้มี Webhook หลายตัวเข้ามาชนกันพร้อมกัน

12) ออกใบเสร็จอย่างเป็นระบบและตรวจสอบย้อนหลังได้

เลขใบเสร็จไม่ควรเป็นเลขสุ่ม เพราะจะทำให้ audit ยาก ควรใช้รูปแบบลำดับ เช่น

  • ปี + เดือน + running number

และการสร้างเลขใบเสร็จควรอยู่ใน transaction เดียวกับการอัปเดตสถานะเป็น PAID เพื่อป้องกันปัญหาเลขซ้ำหรือการออกใบเสร็จซ้ำ

13) แยก “รับเงินสำเร็จ” ออกจาก “งานหลังบ้าน”

หลังออเดอร์กลายเป็น PAID แล้ว ค่อยเริ่มงานอื่นต่อ เช่น

  • สร้างสิทธิ์ใช้งาน
  • ปล่อยไฟล์ดาวน์โหลด
  • ตัดสต็อก
  • ส่งอีเมลใบเสร็จ

งานเหล่านี้ควรถูกส่งไปทำใน job queue แทนการทำใน Webhook ตรง ๆ เพื่อลดความเสี่ยงเรื่อง timeout และทำให้ระบบตอบกลับผู้ให้บริการได้เร็ว

14) ตอบ Webhook ให้เร็ว แล้วค่อยทำงานหนักภายหลัง

ผู้ให้บริการหลายเจ้ากำหนดเวลา timeout ประมาณ 5–10 วินาที หากระบบตอบช้า อาจถูกยิงซ้ำทันที ดังนั้นใน Webhook ควรทำเฉพาะงานสำคัญเท่านั้น เช่น

  • validate ข้อมูล
  • ตรวจลายเซ็น
  • บันทึก event
  • เปลี่ยนสถานะหลัก

ส่วนงานหนัก เช่น ส่งอีเมล สร้าง PDF หรือประมวลผลเพิ่มเติม ควรส่งเข้า queue เพื่อไปทำทีหลัง

15) รองรับสถานะเพิ่มเติม เช่น partial capture, void และ refund

Gateway บางรายมี flow ที่ละเอียดกว่าแค่จ่ายสำเร็จหรือไม่สำเร็จ เช่น

  • authorized
  • captured
  • partial capture
  • void
  • refund

ดังนั้นโครงสร้าง payments ควรออกแบบให้รองรับ state เหล่านี้ได้ และต้องจำไว้ว่า refund ก็มักเข้ามาทาง Webhook เช่นกัน หมายความว่าออเดอร์ที่เคยเป็น PAID อาจเปลี่ยนเป็น REFUNDED ได้ในภายหลัง

16) ปิดช่องโหว่ที่มักถูกมองข้าม

หลายระบบมีจุดพลาดจากเรื่องพื้นฐานมากกว่าความซับซ้อน ควรหลีกเลี่ยงสิ่งต่อไปนี้

  • อย่าเปิด endpoint ที่เปลี่ยนสถานะออเดอร์จาก query string
  • อย่าใช้ return URL เป็นตัวชี้ขาดผลการจ่าย
  • อย่าเก็บ secret ไว้ใน frontend
  • ทำ rate limit ที่ webhook endpoint
  • แยก secret สำหรับ webhook ออกจาก secret ที่ใช้สร้าง charge

รายละเอียดเล็กน้อยเหล่านี้ช่วยลดความเสี่ยงด้านความปลอดภัยได้มาก

17) ทดสอบระบบให้เหมือนสถานการณ์จริง

ระบบที่ดูดีใน happy path อาจพังได้ทันทีเมื่อเจอเหตุการณ์จริง ดังนั้นควรทดสอบอย่างน้อยกรณีต่อไปนี้

  • Webhook เดิมถูกส่งซ้ำ 2–3 ครั้งติดกัน
  • Webhook มาช้าหลังจากจ่ายไปแล้ว 2 นาที
  • ผู้ใช้จ่ายสำเร็จแต่ปิดหน้าเว็บก่อน redirect กลับ
  • ยอดเงินไม่ตรง
  • signature ไม่ถูกต้อง

ระบบที่ดีควร “นิ่ง” ไม่ว่าจะเจอเหตุการณ์เหล่านี้แบบใด

18) เช็กลิสต์ก่อนขึ้นโปรดักชัน

ก่อนนำระบบขึ้นใช้งานจริง ควรตรวจสอบว่าอย่างน้อยมีสิ่งต่อไปนี้ครบถ้วน

  • มี idempotency ตอน create charge
  • มี signature verification ตอนรับ webhook
  • มี unique event_id กัน webhook ซ้ำ
  • มี database transaction กันออกใบเสร็จซ้ำ
  • มี audit log เก็บ payload ดิบ
  • มี queue สำหรับงานหนัก
  • มี dashboard หรือระบบติดตามเหตุการณ์ผิดปกติ

สรุป

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

หากออกแบบตั้งแต่โครงสร้างข้อมูล การใช้ idempotency การตรวจ signature การทำ transaction และการแยกงานหนักไปไว้ใน queue ระบบชำระเงินของคุณจะไม่เพียงแค่ “ใช้งานได้” แต่ยัง “เชื่อถือได้” ในระดับที่พร้อมสำหรับงานจริงและการตรวจสอบย้อนหลังในระยะยาว