HMAC 验证
HMAC 验证是 FastOG 用来确认每个基于订阅的 OG 图片请求均来自您自己服务器的方式——签名 URL 可确保您的额度安全。请先注册,然后在仪表板中选择动态 API 套餐并创建您的 API 密钥和 HMAC 密钥;付费 API 渲染是干净的。本页涵盖动态 Open Graph 图片背后的服务器端规则,从规范查询签名到恒定时间比较。
概述
/api/v1/og 端点已加入 HMAC-SHA256 签名验证,以防止 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 签名(查询参数)routes/api.php- 为 OG 路由添加 HMAC 中间件bootstrap/app.php- 注册hmac中间件别名app/Models/ApiKey.php- 添加hmac_secret字段(每个用户的密钥)app/Http/Controllers/DashboardController.php- 在创建 API 密钥时生成 HMAC 密钥database/migrations/2026_08_03_214931_add_hmac_secret_to_api_keys_table.php- 用于 hmac_secret 列的迁移resources/js/Pages/Dashboard/ApiKeys.svelte- 向用户显示 HMAC 密钥(仅一次)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 是规范查询的一部分,因此签名与 API 密钥绑定。更改任何参数(标题、模板、图片等)或密钥本身都会改变规范 → 签名不匹配 → 401 → 不渲染,不计算配额。相同参数 → 相同签名 → 相同 URL → 可在边缘永久缓存,重放时不计数。
安全特性
- 内容寻址签名 - 任何参数更改 → 401 → 不渲染,不计算配额(URL 本身就是证明)
- 无时间戳 - URL 嵌入公共 HTML 并永久缓存;它们必须比 5 分钟更持久
- 恒定时间比较(
hash_equals())- 防止时序攻击 - 消息中包含 API 密钥 - 签名与密钥绑定;更换密钥会失败
- 路径特定 - 签名仅对精确路径有效
- 方法特定 - GET 签名不适用于 POST
- 每个用户的密钥 - 每个 API 密钥都有自己的 HMAC 密钥,可以独立撤销(轮换 = 已泄露 URL 的紧急开关)
配置
生成密钥(自动)
在仪表盘中创建 API 密钥时,HMAC 密钥会自动生成。它仅与 API 密钥一同显示一次,之后不再显示。
手动生成(如需):
bash
openssl rand -hex 32
数据库结构
sql
ALTER TABLE api_keys ADD COLUMN hmac_secret VARCHAR(64)
COMMENT 'HMAC-SHA256 secret for signing OG image URLs (hex, 64 chars)';
环境变量
无需全局环境变量——密钥按用户存储在 api_keys 表中。
使用示例(网站后端)
javascript
// 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 |
签名与规范查询不匹配(可能被篡改或密钥错误) |
测试
运行测试:
bash
php artisan test --filter HmacSignatureTest
全部 8 项测试均通过:
- ✅ 无签名的请求被拒绝
- ✅ 缺少 key 参数的请求被拒绝
- ✅ 内容参数被篡改(标题已更改)的请求被拒绝
- ✅ 添加参数的请求被拒绝
- ✅ 无效签名被拒绝
- ✅ 有效签名通过 HMAC 校验
- ✅ 签名与路径相关
- ✅ 签名与请求方法相关
- ✅ 不同的 API 密钥具有不同的密钥值
性能影响
- 可忽略不计 - HMAC-SHA256 计算每次请求约 0.1 毫秒
- 无额外网络调用 - 验证在本地完成
- 快速失败 - 无效签名在 API 密钥查找之前即被拒绝
安全注意事项
- 切勿在客户端代码中暴露密钥 - 仅在服务器端进行签名
- 使用 HTTPS - HMAC 可防止未经授权的请求,HTTPS 可防止中间人攻击(MITM)
- 定期轮换密钥 - 尤其是在密钥泄露的情况下(需要重新生成 API 密钥)
- 监控签名失败情况 - 反复失败可能表明存在攻击
- 按用户隔离 - 每个 API 密钥拥有独立的 HMAC 密钥,单个密钥泄露不会影响其他用户