Hướng dẫn Chữ ký HMAC
Hướng dẫn chữ ký HMAC này sẽ giúp bạn xây dựng chữ ký HMAC-SHA256 cho FastOG, để bạn có thể tạo hình ảnh Open Graph động ngay lập tức — dựa trên đăng ký, với tài khoản miễn phí và gói Dynamic API (các bản render trả phí sẽ sạch). Bạn có thể tự tay viết chữ ký bằng bất kỳ ngôn ngữ nào ở đây, hoặc sử dụng Trình kiểm tra hình ảnh OG miễn phí để xem định dạng URL hoạt động trước tiên.
Tổng quan
Mọi yêu cầu đến /api/v1/og phải bao gồm chữ ký HMAC-SHA256 để chứng minh chúng đến từ trang web hợp lệ. Điều này ngăn chặn các bên không được phép sử dụng hạn mức API của bạn.
Hai lớp xác thực:
- API Key (trong tham số truy vấn:
key=sk_quick_123abc) — xác định tài khoản người dùng (để tính hạn mức render) - Chữ ký HMAC (trong tham số truy vấn:
s=<signature>) — chứng minh yêu cầu đến từ trang web của bạn
Cả hai đều phải vượt qua. Chỉ riêng API key là KHÔNG đủ.
Tại sao dùng tham số truy vấn? Ảnh OG được tải bởi các trình thu thập dữ liệu của mạng xã hội (LinkedIn, Twitter, Facebook) mà không gửi header tùy chỉnh. Chữ ký BẮT BUỘC phải nằm trong URL.
Tạo Chữ Ký
Thuật Toán
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>
Yêu Cầu
- Định dạng chữ ký: Chuỗi hex 64 ký tự (SHA256)
- Sử dụng
hash_equals()để so sánh trong thời gian cố định (ngăn chặn tấn công thời gian) - Bí mật riêng cho từng người dùng: Mỗi khóa API có trường
hmac_secretriêng
Ví dụ triển khai
Để có các hướng dẫn hoàn chỉnh với nhiều framework và mẫu, xem:
- Hướng dẫn JavaScript/Node.js - Next.js, Express, Node.js thuần
- Next.js App Router
- Next.js Pages Router
- React
- SvelteKit
- Vue/Nuxt
- Astro
- Express
- Node.js thuần
- PHP/Laravel
- Python
- Ruby/Rails
JavaScript (Node.js / Frontend)
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)
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
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 }}" />
Ví dụ Hoàn Chỉnh: Mẫu Blade trong Laravel
{{-- 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>
Các Thực Hành Bảo Mật Tốt Nhất
- Lưu trữ bí mật một cách an toàn - Sử dụng biến môi trường hoặc trình quản lý bí mật
- Không bao giờ tiết lộ bí mật trong mã phía máy khách - Chỉ ký URL trên máy chủ
- Sử dụng HTTPS - Ngăn chặn các cuộc tấn công MITM
- Xoay vòng bí mật định kỳ - Đặc biệt nếu bị xâm phạm
- Giám sát các chữ ký thất bại - Các lần thất bại lặp lại có thể cho thấy các cuộc tấn công
- Cô lập theo từng người dùng - Mỗi khóa API có bí mật HMAC riêng
Phản hồi Lỗi
Thiếu Chữ Ký
{
"error": "Missing signature parameters",
"code": "missing_signature",
"required_params": ["s", "key"],
"example": "/api/v1/og?title=Hello&key=sk_quick_123abc&s=<signature>"
}
Chữ Ký Không Hợp Lệ
{
"error": "Invalid signature",
"code": "invalid_signature"
}
Kiểm thử
Tạo một URL kiểm thử:
# 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>"
Các phản hồi dự kiến:
- ✅ Chữ ký hợp lệ → Trả về ảnh OG (hoặc lỗi xác thực nếu thiếu tham số mẫu)
- ❌ Chữ ký không hợp lệ →
{"error": "...", "code": "invalid_signature"} - ❌ Thiếu tham số →
{"error": "...", "code": "missing_signature"}
Xử lý sự cố
Chữ ký không khớp
- Xác minh bạn đang sử dụng đúng khóa bí mật HMAC (kiểm tra khóa API trong bảng điều khiển)
- Kiểm tra truy vấn chuẩn: mọi tham số truy vấn ngoại trừ
s(bao gồm cảkey), giá trị được mã hóa theo rfc3986, khóa được sắp xếp theo thứ tự từ điển - Xác minh chữ ký là mã hex viết thường (64 ký tự)
Lấy Khóa Bí Mật HMAC Của Bạn
- Truy cập Bảng điều khiển → Khóa API
- Tạo khóa API mới (hoặc sử dụng khóa hiện có)
- Sao chép Khóa bí mật HMAC hiển thị bên cạnh khóa API (chỉ hiển thị một lần!)
- Lưu trữ nó một cách an toàn trong các biến môi trường của bạn
# Add to .env
OG_RENDER_HMAC_SECRET=<your-64-char-hex-secret>
Kiểm thử bằng curl
# 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}"
Kỳ vọng: 200 OK kèm dữ liệu hình ảnh (hoặc 422 nếu tham số mẫu không hợp lệ).