StableOps
SDK

Webhook 验签

跨运行时验证 Webhook 签名。

签名内容

每次投递携带三个 header:

X-Product-Signature: t=1780301011,v1=7b11b2c1…
X-Event-Id: evt_01JYA…
X-Delivery-Id: del_01JYA…

签名为 HMAC-SHA256(secret, "${timestamp}.${rawBody}"),hex 编码。 5 分钟时间窗防重放;签名比较常量时间,避免计时侧信道。

在 handler 中验证

SDK 直接 re-export 验签函数,框架用户不需要额外依赖:

import { verifySignature, SIGNATURE_HEADER } from '@stableops/api-sdk/webhooks'

export async function POST(req: Request) {
  // 必须用 *原始* body 验签。不要先 JSON.parse 再 stringify,
  // 即便对同一对象,key 顺序也可能变化。
  const rawBody = await req.text()

  const result = verifySignature({
    secrets: [process.env.STABLEOPS_WEBHOOK_SECRET!],
    header: req.headers.get(SIGNATURE_HEADER.toLowerCase()) ?? undefined,
    rawBody,
  })

  if (!result.ok) {
    return new Response(JSON.stringify({ reason: result.reason }), {
      status: 400,
    })
  }

  const event = JSON.parse(rawBody)
  // 业务逻辑,按 event.id 幂等
  return new Response('ok')
}

密钥轮换

轮换接口立即返回新 secret,并保留旧 secret 24 小时。期间把两条 secret 同时 传给 verifySignature

verifySignature({
  secrets: [
    process.env.STABLEOPS_WEBHOOK_SECRET!,
    process.env.STABLEOPS_WEBHOOK_SECRET_PREVIOUS!,
  ],
  header,
  rawBody,
})

失败原因

verifySignature 返回判别联合,ok: false 时读取 reason

reason含义
missing_header缺失 X-Product-Signature
invalid_formatheader 不符合 t=…,v1=…
timestamp_expired超出 5 分钟窗口
bad_signatureHMAC 与任一 secret 都不匹配

重复 delivery

验签通过后,按 X-Event-Id(推荐)或自家业务键去重。平台的重试调度 + 网络重试,意味着 同一事件可能落多次——handler 必须幂等。

Edge / Serverless

verifySignature 使用 node:cryptocreateHmactimingSafeEqualBuffer。 主流 edge 运行时都通过原生或兼容层支持 node:crypto:Deno、Vercel 原生支持, 无需额外 polyfill;Cloudflare Workers 需在 wrangler.toml 中启用 nodejs_compat 兼容性标志后即可使用。

本页内容