การตรวจสอบลายเซ็นและความปลอดภัย
Webhook
การตรวจสอบลายเซ็น
HMAC
ความปลอดภัย
replay attack
บทความนี้เหมาะสำหรับใคร
เหมาะสำหรับนักพัฒนาที่ต้องการสร้าง Webhook endpoint บทความนี้อธิบายวิธีการตรวจสอบลายเซ็นเพื่อยืนยันว่าคำขอที่เข้ามานั้นถูกส่งมาจาก ReceiptRoller จริง รวมถึงข้อควรระวังด้านความปลอดภัย
เหมาะสำหรับนักพัฒนาที่ต้องการสร้าง Webhook endpoint บทความนี้อธิบายวิธีการตรวจสอบลายเซ็นเพื่อยืนยันว่าคำขอที่เข้ามานั้นถูกส่งมาจาก ReceiptRoller จริง รวมถึงข้อควรระวังด้านความปลอดภัย
เนื่องจาก Webhook endpoint เป็น URL สาธารณะ โดยหลักการแล้วบุคคลที่สามสามารถส่ง POST ปลอมเข้ามาได้ เพื่อป้องกันปัญหานี้ ReceiptRoller จึงแนบลายเซ็นที่ใช้ HMAC-SHA256 ไปกับทุกคำขอ Webhook ฝั่งผู้รับต้องตรวจสอบลายเซ็นก่อนประมวลผลเสมอ endpoint ที่ไม่ตรวจสอบลายเซ็นจะไร้การป้องกันต่อการโจมตี
กลไกของลายเซ็น
ReceiptRoller จะคำนวณลายเซ็นตามขั้นตอนต่อไปนี้ก่อนส่ง
- สร้างสตริงเป้าหมายของลายเซ็น:
{timestamp}.{request_body} - คำนวณ HMAC-SHA256 โดยใช้ Webhook Secret เป็นคีย์
- เข้ารหัสผลลัพธ์เป็น hex
- แนบไปกับ HTTP header แล้วส่ง
Header ที่เกี่ยวข้องที่ถูกส่งมา
| ชื่อ Header | เนื้อหา |
|---|---|
X-RR-Signature |
สตริง hex ของ HMAC-SHA256 (64 ตัวอักษร) |
X-RR-Timestamp |
เวลาที่ส่ง (Unix วินาที) |
X-RR-Event-Id |
Event ID (คีย์สำหรับ idempotency) |
X-RR-Signature-Version |
เวอร์ชันของลายเซ็น (ปัจจุบันคือ v1) |
ขั้นตอนการตรวจสอบฝั่งผู้รับ
- ดึง
X-RR-Timestampและตัวคำขอดิบ (raw request body) ออกมา (ไม่ใช่อ็อบเจกต์หลังจากพาร์ส แต่เป็นลำดับไบต์ที่ได้รับมานั้นเอง) - ประกอบสตริงเป้าหมายของลายเซ็น
{timestamp}.{body} - คำนวณ HMAC-SHA256 ด้วย Webhook Secret ที่เก็บไว้ แล้วเข้ารหัสเป็น hex
- เปรียบเทียบผลลัพธ์ที่คำนวณได้กับ
X-RR-Signatureด้วยการเปรียบเทียบแบบเวลาคงที่ (constant-time comparison) (ป้องกัน timing attack) - ตรวจสอบว่า timestamp อยู่ในช่วง ±5 นาทีจากเวลาปัจจุบัน (ป้องกัน replay attack)
โค้ดตัวอย่าง
Node.js (Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
const SECRET = process.env.RR_WEBHOOK_SECRET;
const TOLERANCE_SEC = 300; // 5 นาที
// สำคัญ:ใช้ express.raw เพื่อเก็บ body ดิบไว้
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.header("X-RR-Signature");
const ts = req.header("X-RR-Timestamp");
const body = req.body; // Buffer
// ตรวจสอบ timestamp
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(ts, 10)) > TOLERANCE_SEC) {
return res.status(401).send("timestamp out of tolerance");
}
// ตรวจสอบลายเซ็น
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${ts}.${body.toString("utf8")}`)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send("invalid signature");
}
// ตรวจสอบผ่าน:ประมวลผลอีเวนต์
const event = JSON.parse(body.toString("utf8"));
// ... ตรวจสอบ idempotency, ประมวลผลทางธุรกิจ ...
res.status(200).send("ok");
});
Python (Flask)
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["RR_WEBHOOK_SECRET"].encode()
TOLERANCE_SEC = 300
@app.route("/webhook", methods=["POST"])
def webhook():
sig = request.headers.get("X-RR-Signature", "")
ts = request.headers.get("X-RR-Timestamp", "")
body = request.get_data() # ลำดับไบต์ดิบ
# ตรวจสอบ timestamp
if abs(int(time.time()) - int(ts)) > TOLERANCE_SEC:
abort(401)
# ตรวจสอบลายเซ็น
payload = f"{ts}.".encode() + body
expected = hmac.new(SECRET, payload, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401)
# ตรวจสอบผ่าน:ประมวลผลอีเวนต์
event = request.get_json()
# ... ตรวจสอบ idempotency, ประมวลผลทางธุรกิจ ...
return "ok", 200
C# (ASP.NET Core)
[HttpPost("/webhook")]
public async Task<IActionResult> Receive()
{
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body);
var body = await reader.ReadToEndAsync();
var sig = Request.Headers["X-RR-Signature"].ToString();
var ts = Request.Headers["X-RR-Timestamp"].ToString();
// ตรวจสอบ timestamp
var nowSec = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Math.Abs(nowSec - long.Parse(ts)) > 300) return Unauthorized();
// ตรวจสอบลายเซ็น
var secret = Encoding.UTF8.GetBytes(_config["RR_WEBHOOK_SECRET"]);
using var h = new HMACSHA256(secret);
var expected = Convert.ToHexString(
h.ComputeHash(Encoding.UTF8.GetBytes($"{ts}.{body}"))
).ToLowerInvariant();
if (!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(sig),
Encoding.UTF8.GetBytes(expected)))
return Unauthorized();
// ตรวจสอบผ่าน:ประมวลผลอีเวนต์
// ... ตรวจสอบ idempotency, ประมวลผลทางธุรกิจ ...
return Ok();
}
ข้อผิดพลาดในการพัฒนาที่พบบ่อย
| ข้อผิดพลาด | ผลกระทบ | การแก้ไข |
|---|---|---|
| คำนวณลายเซ็นจากสตริงหลังพาร์ส JSON | ล้มเหลวทุกครั้งเมื่อช่องว่างหรือการขึ้นบรรทัดเปลี่ยนไป | ใช้ลำดับไบต์ดิบเสมอ |
เปรียบเทียบด้วย == ปกติ |
อาจถูกคาดเดาลายเซ็นได้จาก timing attack | ใช้ฟังก์ชันเปรียบเทียบแบบเวลาคงที่ |
| ไม่ตรวจสอบ timestamp | คำขอที่เคยถูกต้องในอดีตอาจถูกส่งซ้ำ (replay attack) | ทำช่วงยอมรับ ±5 นาที |
| เขียน secret ลงในโค้ดโดยตรง | รั่วไหลเมื่อ repository หลุด | ใช้ตัวแปรสภาพแวดล้อมหรือ secret manager |
คืน 2xx เมื่อการตรวจสอบล้มเหลว |
ทำให้ผู้โจมตีเข้าใจผิดว่า "รับสำเร็จ" | คืน 401 |
การจัดการ secret อย่างปลอดภัย
- ที่จัดเก็บ:ตัวแปรสภาพแวดล้อม, AWS Secrets Manager, Azure Key Vault, GCP Secret Manager หรือบริการจัดการ secret อื่น ๆ
- การควบคุมการเข้าถึง:ให้ secret ของ production อ่านได้เฉพาะ service account ของสภาพแวดล้อม production เท่านั้น
- การหมุนเวียน (rotation):อย่างน้อยปีละครั้ง และทันทีเมื่อสงสัยว่ารั่วไหล รายละเอียดดูที่ 「การหมุนเวียนและการจัดเก็บ secret อย่างปลอดภัย」
- การแยกสภาพแวดล้อม:ใช้ secret แยกกันสำหรับ production / staging / development
- ห้ามเขียนลง log:อย่าเขียน secret หรือค่าลายเซ็นลงใน error log หรือ audit log
มาตรการเครือข่ายเพิ่มเติม (เลือกได้)
การตรวจสอบลายเซ็นเพียงอย่างเดียวก็เพียงพอแล้ว แต่หากต้องการดำเนินการอย่างเข้มงวดยิ่งขึ้น สามารถพิจารณาสิ่งต่อไปนี้ได้
- IP allow list:เพิ่มช่วง IP ต้นทางที่ส่งจาก ReceiptRoller ลงใน allow list (เนื่องจากช่วง IP อาจเปลี่ยนแปลงได้โดยไม่แจ้งล่วงหน้า ควรใช้เป็นมาตรการเสริม ไม่ใช่แทนที่การตรวจสอบลายเซ็น)
- WAF:วาง WAF ไว้หน้า endpoint เพื่อสกัดคำขอที่น่าสงสัย
- การจำกัดอัตรา (rate limit):จำกัดคำขอที่มากเกินไปจาก IP เดียว
คู่มือที่เกี่ยวข้อง
วันที่เผยแพร่: 2569-04-27
วันที่อัปเดต: 2569-07-05
หมวดหมู่
แท็ก
API (22)
OAuth (15)
Android (10)
iOS (9)
Webhook (8)
api (7)
oauth (5)
getting-started (4)
การลงทะเบียนแอป (4)
การแก้ปัญหา (4)
บทความที่เกี่ยวข้อง
-
สารบัญความช่วยเหลือสำหรับนักพัฒนาสารบัญความช่วยเหลือสำหรับนักพัฒนา ReceiptRoller รวบรวมตั้งแต่การสมัครเป็นนักพัฒนา การลงทะเบียนแอปพลิเคชัน การยืนยันตัวตน OAuth และสโคป คู่มือการพัฒนา (แอปวอลเล็ต, Webhook สำหรับร้านค้า, Survey API) คู่มือแยกตามโดเมนข้อมูล การใช้งานและความปลอดภัย คอมมูนิตี ไปจนถึงการแก้ปัญหา
-
การออกแบบการส่งซ้ำ・ลำดับ・idempotencyอธิบายนโยบายการส่งซ้ำของ Webhook ของ ReceiptRoller, เหตุผลที่ไม่รับประกันลำดับการส่ง, การอิมพลิเมนต์ idempotency ด้วย event_id, การจัดการ dead letter และแอนตี้แพตเทิร์นที่พบบ่อย
-
วิธีลงทะเบียน Webhookอธิบายขั้นตอนการลงทะเบียน Webhook endpoint ในพอร์ทัลนักพัฒนาของ ReceiptRoller วิธีเลือกอีเวนต์ที่ต้องการสมัครรับ การส่งทดสอบ การแบ่งใช้หลาย endpoint และวิธีลบหรือปิดใช้งานชั่วคราว
-
การมอนิเตอร์และการจัดการเมื่อล้มเหลวอธิบายวิธีดูประวัติการส่ง Webhook ของ ReceiptRoller, ตัวชี้วัดที่ควรมอนิเตอร์และการออกแบบอะเลิร์ต, การส่ง dead letter ซ้ำ, รูปแบบเหตุขัดข้องที่พบบ่อยและขั้นตอนการกู้คืน
-
ภาพรวมของ Webhookอธิบายชนิดอีเวนต์หลักที่ Webhook ของ ReceiptRoller ส่งมา เหตุผลที่ควรใช้ Webhook แทน polling รูปแบบการส่ง (HTTPS POST + JSON) และแนวคิดเรื่องการรับประกันการส่ง