หัวข้อ

คู่มือลายเซ็น HMAC

คู่มือลายเซ็น HMAC นี้จะแนะนำคุณในการสร้างลายเซ็น HMAC-SHA256 สำหรับ FastOG เพื่อให้คุณสามารถสร้างรูปภาพ Open Graph แบบไดนามิกได้ทันที — แบบสมัครสมาชิก พร้อมบัญชีฟรีและแผน Dynamic API (การเรนเดอร์แบบชำระเงินจะสะอาด) คุณสามารถเขียนลายเซ็นด้วยมือในภาษาใดก็ได้ที่นี่ หรือใช้ตัวทดสอบรูปภาพ OG ฟรีเพื่อดูรูปแบบ URL ในการทำงานก่อน

ภาพรวม

คำขอทั้งหมดที่ส่งไปยัง /api/v1/og ต้องมีลายเซ็น HMAC-SHA256 เพื่อยืนยันว่าคำขอนั้นมาจากเว็บไซต์ที่ถูกต้องตามกฎหมาย ซึ่งช่วยป้องกันไม่ให้บุคคลที่ไม่ได้รับอนุญาตใช้โควตาการเรนเดอร์ API ของคุณ

การยืนยันตัวตนสองชั้น:

  1. API Key (ใน query: key=sk_quick_123abc) — ใช้ระบุบัญชีผู้ใช้ (สำหรับการคิดโควตาการเรนเดอร์)
  2. HMAC Signature (ใน query: s=<signature>) — ใช้ยืนยันว่าคำขอนั้นมาจากเว็บไซต์ของคุณ

ทั้งสองอย่างต้องผ่านการตรวจสอบ การมีเพียง API Key อย่างเดียวนั้นไม่เพียงพอ

ทำไมต้องใช้ query parameters? OG images ถูกดึงข้อมูลโดย crawler ของโซเชียลเน็ตเวิร์ก (LinkedIn, Twitter, Facebook) ซึ่งไม่ส่ง custom headers ดังนั้นลายเซ็นจึงต้องอยู่ใน URL

การสร้างลายเซ็น

อัลกอริทึม

method = "GET"
path = "/api/v1/og"
canonicalQuery = for every query param EXCEPT `s` (including `key`):
  - array values are sorted too
  - each pair: rfc3986Encode(key)=rfc3986Encode(value)
  - pairs sorted lexicographically and joined with "&"

message = method + path + "\n" + canonicalQuery
signature = hex(HMAC-SHA256(message, hmac_secret))

Query parameters:
  key=<apiKey>
  s=<signature>

ข้อกำหนด

  • รูปแบบลายเซ็น: สตริงเลขฐานสิบหก 64 ตัวอักษร (SHA256)
  • ใช้ hash_equals() สำหรับการเปรียบเทียบแบบเวลาคงที่ (ป้องกันการโจมตีแบบจับเวลา)
  • ความลับเฉพาะผู้ใช้: คีย์ API แต่ละตัวมีฟิลด์ hmac_secret ของตัวเอง

ตัวอย่างการนำไปใช้งาน

สำหรับบทช่วยสอนแบบครบถ้วนที่ครอบคลุมหลายเฟรมเวิร์กและรูปแบบการใช้งาน ดูได้ที่:

JavaScript (Node.js / Frontend)

javascript
const crypto = require("crypto");

// rfc3986-encode: everything except A-Za-z0-9-._~
const enc = (s) =>
    encodeURIComponent(s).replace(
        /[!'()*]/g,
        (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
    );

function generateOgImageUrl(baseUrl, params, apiKey, hmacSecret) {
    // every query param except `s`; key included; arrays expand to one pair each
    const all = { ...params, key: apiKey };
    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 signature = crypto
        .createHmac("sha256", hmacSecret)
        .update(`GET/api/v1/og\n${canonical}`)
        .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: "default" },
    "sk_quick_123abc",
    process.env.HMAC_SECRET,
);

// <meta property="og:image" content="{ogUrl}" />

PHP (Laravel)

php
function generateOgImageUrl(string $baseUrl, array $params, string $apiKey, string $hmacSecret): string
{
    // every query param except `s`; key included; array values sorted too
    $all = array_merge($params, ['key' => $apiKey]);

    $pairs = [];
    foreach ($all as $k => $v) {
        $values = is_array($v) ? $v : [$v];
        sort($values, SORT_STRING);
        foreach ($values as $vv) {
            $pairs[] = rawurlencode((string)$k) . '=' . rawurlencode((string)$vv);
        }
    }
    sort($pairs, SORT_STRING);
    $canonical = implode('&', $pairs);

    $signature = hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret);

    // URL uses bracket keys for arrays (features[]=a&features[]=b); canonical signs plain keys
    $urlPairs = [];
    foreach ($all as $k => $v) {
        $isArray = is_array($v);
        $values = $isArray ? $v : [$v];
        $urlKey = $isArray ? $k . '[]' : $k;
        foreach ($values as $vv) {
            $urlPairs[] = rawurlencode($urlKey) . '=' . rawurlencode((string)$vv);
        }
    }

    return $baseUrl . '?' . implode('&', $urlPairs) . '&s=' . $signature;
}

// Usage in Blade template:
$ogUrl = generateOgImageUrl(
    'https://fastog.com/api/v1/og',
    ['title' => 'Como criar um SaaS'],
    'sk_quick_123abc',
    env('HMAC_SECRET')
);

// <meta property="og:image" content="{{ $ogUrl }}" />

Python

python
import hmac
import hashlib
from urllib.parse import quote

def _enc(s):
    return quote(str(s), safe='-._~')

def generate_og_image_url(base_url, params, api_key, hmac_secret):
    # every query param except `s`; key included; array values sorted too
    all_params = {**params, 'key': api_key}

    pairs = []
    for k, v in all_params.items():
        for vv in sorted(v if isinstance(v, list) else [v]):
            pairs.append(f"{_enc(k)}={_enc(vv)}")
    canonical = '&'.join(sorted(pairs))

    signature = hmac.new(
        hmac_secret.encode(),
        f"GET/api/v1/og\n{canonical}".encode(),
        hashlib.sha256
    ).hexdigest()

    # URL uses bracket keys for arrays (features[]=a&features[]=b); canonical signs plain keys
    url_pairs = []
    for k, v in all_params.items():
        if isinstance(v, list):
            for vv in v:
                url_pairs.append(f"{_enc(f'{k}[]')}={_enc(vv)}")
        else:
            url_pairs.append(f"{_enc(k)}={_enc(v)}")

    return f"{base_url}?{'&'.join(url_pairs)}&s={signature}"

# Usage:
og_url = generate_og_image_url(
    'https://fastog.com/api/v1/og',
    {'title': 'Como criar um SaaS', 'template': 'default'},
    'sk_quick_123abc',
    os.environ['HMAC_SECRET']
)

# <meta property="og:image" content="{{ og_url }}" />

ตัวอย่างที่สมบูรณ์: เทมเพลต Laravel Blade

blade
{{-- In your blog post view --}}
@php
    $apiKey = 'sk_quick_123abc'; // Your full API key
    $hmacSecret = env('OG_RENDER_HMAC_SECRET');

    $params = [
        'template' => 'blog',
        'title' => $post->title,
        'subtitle' => $post->excerpt,
        'key' => $apiKey,
    ];

    // canonical query: sorted "k=v" pairs, rfc3986-encoded, every param except `s`
    $pairs = [];
    foreach ($params as $k => $v) {
        $pairs[] = rawurlencode((string)$k) . '=' . rawurlencode((string)$v);
    }
    sort($pairs, SORT_STRING);
    $canonical = implode('&', $pairs);
    $signature = hash_hmac('sha256', 'GET/api/v1/og' . "\n" . $canonical, $hmacSecret);

    $ogImageUrl = 'https://fastog.com/api/v1/og?' . $canonical . '&s=' . $signature;
@endphp

<head>
    <meta property="og:image" content="{{ $ogImageUrl }}" />
    <meta property="og:title" content="{{ $post->title }}" />
    <meta property="og:description" content="{{ $post->excerpt }}" />
</head>

แนวทางปฏิบัติที่ดีด้านความปลอดภัย

  1. จัดเก็บความลับอย่างปลอดภัย - ใช้ตัวแปรสภาพแวดล้อม (environment variables) หรือตัวจัดการความลับ (secrets manager)
  2. ห้ามเปิดเผยความลับในโค้ดฝั่งไคลเอนต์ - ลงนาม URL บนเซิร์ฟเวอร์เท่านั้น
  3. ใช้ HTTPS - ป้องกันการโจมตีแบบ MITM
  4. หมุนเวียนความลับเป็นระยะ - โดยเฉพาะเมื่อมีความเสี่ยงว่าจะถูกบุกรุก
  5. ติดตามการลงนามที่ล้มเหลว - ความล้มเหลวซ้ำๆ อาจบ่งชี้ถึงการโจมตี
  6. แยกผู้ใช้รายบุคคล - คีย์ API แต่ละตัวมี HMAC secret ของตัวเอง

การตอบสนองข้อผิดพลาด

ลายเซ็นหายไป

json
{
    "error": "Missing signature parameters",
    "code": "missing_signature",
    "required_params": ["s", "key"],
    "example": "/api/v1/og?title=Hello&key=sk_quick_123abc&s=<signature>"
}

ลายเซ็นไม่ถูกต้อง

json
{
    "error": "Invalid signature",
    "code": "invalid_signature"
}

การทดสอบ

สร้าง URL สำหรับการทดสอบ:

bash
# Node.js
node -e "
const crypto = require('crypto');
const enc = (s) => encodeURIComponent(s).replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase());
const all = { title: 'Test', key: 'sk_quick_123abc' };
const pairs = [];
for (const [k, v] of Object.entries(all)) for (const vv of Array.isArray(v) ? v : [v]) pairs.push(enc(k) + '=' + enc(vv));
pairs.sort();
const canonical = pairs.join('&');
const sig = crypto.createHmac('sha256', 'YOUR_HMAC_SECRET').update('GET/api/v1/og\n' + canonical).digest('hex');
console.log('https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=' + sig);
"

# Test with curl
curl "https://fastog.com/api/v1/og?title=Test&key=sk_quick_123abc&s=<signature>"

ผลลัพธ์ที่คาดหวัง:

  • ✅ ลายเซ็นที่ถูกต้อง → ส่งคืน OG image (หรือข้อผิดพลาดการตรวจสอบหากพารามิเตอร์เทมเพลตหายไป)
  • ❌ ลายเซ็นไม่ถูกต้อง → {"error": "...", "code": "invalid_signature"}
  • ❌ พารามิเตอร์หายไป → {"error": "...", "code": "missing_signature"}

การแก้ไขปัญหา

ลายเซ็นไม่ตรงกัน

  1. ตรวจสอบว่าคุณใช้ HMAC secret ที่ถูกต้อง (ตรวจสอบ API key ในแดชบอร์ด)
  2. ตรวจสอบ canonical query: พารามิเตอร์ query ทั้งหมดยกเว้น s (รวมถึง key) ค่าต่างๆ ถูกเข้ารหัสแบบ rfc3986 และคีย์ถูกจัดเรียงตามลำดับตัวอักษร
  3. ตรวจสอบว่าลายเซ็นเป็นเลขฐานสิบหกตัวพิมพ์เล็ก (64 ตัวอักษร)

การรับ HMAC Secret ของคุณ

  1. ไปที่ แดชบอร์ด → API Keys
  2. สร้างคีย์ API ใหม่ (หรือใช้คีย์ที่มีอยู่แล้ว)
  3. คัดลอก HMAC Secret ที่แสดงถัดจากคีย์ API (แสดงเพียงครั้งเดียวเท่านั้น!)
  4. เก็บไว้อย่างปลอดภัยในตัวแปรสภาพแวดล้อมของคุณ
bash
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>

การทดสอบด้วย curl

bash
# Bash script to generate signed URL
SECRET="your-hmac-secret-here"
API_KEY="sk_quick_123abc"  # Your full API key

# canonical query: sorted "k=v" pairs, every param except `s` (key included)
CANONICAL="key=${API_KEY}&template=default&title=Test"
MESSAGE=$(printf 'GET/api/v1/og\n%s' "$CANONICAL")
SIGNATURE=$(printf '%s' "$MESSAGE" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)

curl "http://localhost:8000/api/v1/og?template=default&title=Test&key=${API_KEY}&s=${SIGNATURE}"

ผลลัพธ์ที่คาดหวัง: 200 OK พร้อมข้อมูลรูปภาพ (หรือ 422 หากพารามิเตอร์เทมเพลตไม่ถูกต้อง)

เอกสารอ้างอิง API ฉบับเต็ม

ลองใช้ฟรี — ไม่ต้องสมัครสมาชิก

ดูตัวอย่างและทดสอบภาพ OG แบบไดนามิกได้ในไม่กี่วินาที

เปิดตัวทดสอบภาพ OG ฟรี

A BeCaSimulations site