การตรวจสอบ HMAC
การตรวจสอบ HMAC เป็นกลไกที่ FastOG ใช้เพื่อยืนยันว่าคำขอ OG image แบบ subscription ทุกรายการมาจากเซิร์ฟเวอร์ของคุณเอง — การเซ็น URL ช่วยให้เครดิตของคุณปลอดภัย ลงทะเบียน จากนั้นเลือกแผน Dynamic API และสร้าง API key พร้อม HMAC secret ในแดชบอร์ด (dashboard) การเรนเดอร์ API แบบชำระเงินจะสะอาดและไม่มีลายน้ำ หน้านี้ครอบคลุมกฎฝั่งเซิร์ฟเวอร์เบื้องหลัง dynamic Open Graph images ตั้งแต่การเซ็น query แบบ canonical ไปจนถึงการเปรียบเทียบแบบ constant-time
ภาพรวม
การตรวจสอบลายเซ็น HMAC-SHA256 ได้ถูกเพิ่มเข้าไปในจุดสิ้นสุด /api/v1/og เพื่อป้องกันการใช้โควตา API โดยไม่ได้รับอนุญาต ซึ่งช่วยให้มั่นใจได้ว่ามีเพียงคำขอจากเว็บไซต์ที่ถูกต้องเท่านั้นที่สามารถสร้างภาพ OG ได้
สถาปัตยกรรม
การตรวจสอบสิทธิ์แบบสองชั้น:
- ลายเซ็น HMAC (ชั้นแรก) - ตรวจสอบว่าคำขอมาจากแหล่งที่เชื่อถือได้ (เว็บไซต์)
- คีย์ API (ชั้นที่สอง) - ระบุบัญชีผู้ใช้สำหรับการคิดโควตาการเรนเดอร์
มิดเดิลแวร์เดียว (ApiKeyAuth): ตรวจสอบลายเซ็น (พารามิเตอร์คิวรี) และแก้ไขคีย์ API สำหรับการคิดโควตาการเรนเดอร์ในรอบเดียว — ตรวจพบลายเซ็นที่ไม่ถูกต้องและหยุดทันที
พารามิเตอร์คิวรี ไม่ใช่ส่วนหัว: เนื่องจากรูปภาพ OG ถูกดึงโดยครอว์เลอร์ของโซเชียลเน็ตเวิร์ก (LinkedIn, Twitter, Facebook) ซึ่งไม่ส่งส่วนหัวแบบกำหนดเอง ลายเซ็นจึงถูกส่งผ่านพารามิเตอร์คิวรีของ URL:
GET /api/v1/og?title=Hello&key=sk_quick_123abc&s=<hmac>
การนำไปใช้งาน
ไฟล์ที่สร้าง/แก้ไข
app/Http/Middleware/HmacSignature.php- มิดเดิลแวร์ใหม่สำหรับตรวจสอบลายเซ็น HMAC (พารามิเตอร์ query)routes/api.php- เพิ่มมิดเดิลแวร์ HMAC ในเส้นทาง OGbootstrap/app.php- ลงทะเบียนนามแฝงมิดเดิลแวร์hmacapp/Models/ApiKey.php- เพิ่มฟิลด์hmac_secret(ความลับเฉพาะผู้ใช้)app/Http/Controllers/DashboardController.php- สร้าง HMAC secret เมื่อสร้างคีย์ APIdatabase/migrations/2026_08_03_214931_add_hmac_secret_to_api_keys_table.php- Migration สำหรับคอลัมน์ hmac_secretresources/js/Pages/Dashboard/ApiKeys.svelte- แสดง HMAC secret ให้ผู้ใช้ (ครั้งเดียว)docs/hmac-signature-guide.md- คู่มือการใช้งานฝั่งไคลเอนต์tests/Feature/HmacSignatureTest.php- เทสต์ผ่าน 8 รายการ
อัลกอริทึมลายเซ็น
method = HTTP method (e.g., "GET")
path = request path (e.g., "/api/v1/og")
query = the request's DECODED query params
canonical = every param except `s`, keys sorted (array values sorted too),
each `key=value` RFC-3986 encoded (unreserved-only), joined with "&"
message = method + "/" + path + "\n" + canonical
signature = HMAC-SHA256(message, hmac_secret)
Query parameters:
key=<apiKey>
s=<signature>
(no timestamp)
key เป็นส่วนหนึ่งของ canonical query ดังนั้นลายเซ็นจึงผูกกับคีย์ API การเปลี่ยนพารามิเตอร์ใดๆ (title, template, image, …) หรือตัวคีย์เองจะทำให้ canonical เปลี่ยนไป → ลายเซ็นไม่ตรงกัน → 401 → ไม่มีการเรนเดอร์ ไม่นับ quota พารามิเตอร์เหมือนกัน → ลายเซ็นเหมือนกัน → URL เหมือนกัน → แคชได้ที่ edge ตลอดไป และเล่นซ้ำได้โดยไม่นับจำนวน
คุณสมบัติด้านความปลอดภัย
- ลายเซ็นแบบ content-addressed - การเปลี่ยนพารามิเตอร์ใดๆ → 401 → ไม่มีการเรนเดอร์ ไม่นับ quota (ตัว URL เองคือหลักฐาน)
- ไม่มี timestamp - URL ถูกฝังใน HTML สาธารณะและถูกแคชตลอดไป จึงต้องมีอายุเกิน 5 นาที
- การเปรียบเทียบแบบเวลาคงที่ (
hash_equals()) - ป้องกันการโจมตีแบบ timing attack - คีย์ API อยู่ในข้อความ - ลายเซ็นผูกกับคีย์ การสลับคีย์จะล้มเหลว
- เฉพาะเส้นทาง - ลายเซ็นใช้ได้เฉพาะเส้นทางที่แน่นอนเท่านั้น
- เฉพาะเมธอด - ลายเซ็น GET จะใช้กับ POST ไม่ได้
- ความลับเฉพาะผู้ใช้ - คีย์ API แต่ละตัวมี HMAC secret ของตัวเอง สามารถเพิกถอนได้อย่างอิสระ (การหมุนคีย์ = สวิตช์ปิดสำหรับ URL ที่รั่วไหล)
การกำหนดค่า
สร้าง Secret (อัตโนมัติ)
HMAC secret จะถูกสร้างขึ้นโดยอัตโนมัติเมื่อสร้าง API key ในแดชบอร์ด (dashboard) โดยจะแสดงเพียงครั้งเดียวพร้อมกับ API key และจะไม่แสดงอีกในภายหลัง
การสร้างด้วยตนเอง (หากจำเป็น):
openssl rand -hex 32
โครงสร้างฐานข้อมูล (Database Schema)
ALTER TABLE api_keys ADD COLUMN hmac_secret VARCHAR(64)
COMMENT 'HMAC-SHA256 secret for signing OG image URLs (hex, 64 chars)';
ตัวแปรสภาพแวดล้อม (Environment Variables)
ไม่จำเป็นต้องใช้ตัวแปรสภาพแวดล้อมระดับโลก - secret จะถูกจัดเก็บต่อผู้ใช้ในตาราง api_keys
ตัวอย่างการใช้งาน (แบ็กเอนด์เว็บไซต์)
// Node.js example - generate signed OG image URL
const crypto = require("crypto");
// RFC-3986 encode (encodeURIComponent leaves !'()* unencoded).
const enc = (s) =>
encodeURIComponent(s).replace(
/[!'()*]/g,
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
);
function generateOgImageUrl(baseUrl, params, apiKey, hmacSecret) {
const all = { ...params, key: apiKey };
// Canonical: every param (except s), sorted, RFC-3986 encoded, joined with "&".
const pairs = [];
for (const [k, v] of Object.entries(all)) {
for (const vv of Array.isArray(v) ? [...v].sort() : [v])
pairs.push(`${enc(k)}=${enc(vv)}`);
}
pairs.sort();
const canonical = pairs.join("&");
const message = `GET/api/v1/og\n${canonical}`;
const signature = crypto
.createHmac("sha256", hmacSecret)
.update(message)
.digest("hex");
const url = new URL(baseUrl);
for (const [k, v] of Object.entries(all)) {
const isArray = Array.isArray(v);
for (const vv of isArray ? v : [v])
url.searchParams.append(isArray ? `${k}[]` : k, vv);
}
url.searchParams.set("s", signature);
return url.toString();
}
// Usage in meta tag
const ogUrl = generateOgImageUrl(
"https://fastog.com/api/v1/og",
{ title: "Como criar um SaaS", template: "blog" },
"sk_quick_123abc",
process.env.HMAC_SECRET,
);
// <meta property="og:image" content="{ogUrl}" />
การตอบสนองข้อผิดพลาด
| สถานะ | รหัส | คำอธิบาย |
|---|---|---|
| 401 | missing_signature |
ไม่พบพารามิเตอร์คำค้นหา key หรือ s |
| 401 | invalid_signature |
ลายเซ็นไม่ตรงกับคำค้นหามาตรฐาน (ถูกแก้ไขหรือใช้คีย์ลับผิด) |
การทดสอบ
รันการทดสอบ:
php artisan test --filter HmacSignatureTest
การทดสอบทั้งหมด 8 รายการผ่าน:
- ✅ คำขอที่ไม่มีลายเซ็นถูกปฏิเสธ
- ✅ คำขอที่ไม่มีพารามิเตอร์คีย์ถูกปฏิเสธ
- ✅ พารามิเตอร์เนื้อหาที่ถูกแก้ไข (เปลี่ยน title) ถูกปฏิเสธ
- ✅ พารามิเตอร์ที่ถูกเพิ่มถูกปฏิเสธ
- ✅ ลายเซ็นที่ไม่ถูกต้องถูกปฏิเสธ
- ✅ ลายเซ็นที่ถูกต้องผ่านการตรวจสอบ HMAC
- ✅ ลายเซ็นเฉพาะเจาะจงต่อ path
- ✅ ลายเซ็นเฉพาะเจาะจงต่อ method
- ✅ คีย์ API ที่ต่างกันมี secret ที่ต่างกัน
ผลกระทบด้านประสิทธิภาพ
- น้อยมาก - การคำนวณ HMAC-SHA256 ใช้เวลาประมาณ 0.1 มิลลิวินาทีต่อคำขอ
- ไม่มีการเรียกเครือข่ายเพิ่มเติม - การตรวจสอบความถูกต้องดำเนินการในเครื่อง
- ล้มเหลวอย่างรวดเร็ว - ลายเซ็นที่ไม่ถูกต้องจะถูกปฏิเสธก่อนการค้นหาคีย์ API
ข้อควรพิจารณาด้านความปลอดภัย
- ห้ามเปิดเผย secret ในโค้ดฝั่ง client โดยเด็ดขาด - ลงนามบนเซิร์ฟเวอร์เท่านั้น
- ใช้ HTTPS - HMAC ป้องกันคำขอที่ไม่ได้รับอนุญาต ส่วน HTTPS ป้องกันการโจมตีแบบ MITM
- หมุนเวียน secret เป็นระยะ - โดยเฉพาะหากมีการรั่วไหล (ต้องสร้าง API key ใหม่)
- ตรวจสอบลายเซ็นที่ล้มเหลว - ความล้มเหลวซ้ำๆ อาจบ่งชี้ถึงการโจมตี
- แยกตามผู้ใช้ - แต่ละ API key มี HMAC secret ของตัวเอง การรั่วไหลของหนึ่งรายการไม่กระทบรายการอื่น
เอกสารอ้างอิง
- คู่มือลายเซ็น HMAC สำหรับไคลเอนต์
- คู่มือเริ่มต้นใช้งานอย่างรวดเร็ว
- OWASP: เอกสารสรุปการจัดเก็บข้อมูลแบบเข้ารหัส
- PHP: hash_hmac()