Webhook 推送(M7-01)

当主机的 L1 配置发生变更(无论从哪个入口改),中枢会主动 POST到主机配置的 report_url

适用场景:

  • 你的 SaaS 平台需要在主机 secret 变更后立即同步自己的密钥库
  • 多方系统协同管控同一台主机,需要事件通知
  • 审计/合规要求保留外部记录

1. 请求格式

POST {report_url}
Content-Type: application/json
User-Agent: gateway-new-webhook/1.0
X-Gateway-Event: host.l1_config.updated
X-Gateway-Delivery: wd_01HXX...
X-Gateway-Timestamp: 1736832000
X-Gateway-Signature: sha256=<hex>

{
  "event": "host.l1_config.updated",
  "delivery_id": "wd_01HXX...",
  "timestamp": "2026-05-14T08:30:00Z",
  "host_id": "uuid-of-host",
  "host_name": "dev-pc-01",
  "changed_by": {
    "kind": "user",
    "id": "uuid-of-user",
    "ip": "203.0.113.5"
  },
  "before": {
    "bid": 1001,
    "report_url": "https://old.example.com/cb"
  },
  "after": {
    "bid": 8848,
    "secret_key": "0123456789abcdef0123456789abcdef",
    "report_url": "https://your-business.example.com/gateway/report"
  }
}

注意:before 不含旧 secret_key(已用于签名),after新的 secret_key 明文, 接收方应在通道安全(HTTPS)的前提下立即落库到自己的密钥本。

2. 签名校验

中枢用旧的 secret_keyX-Gateway-Timestamp + "." + body 做 HMAC-SHA256, hex 编码后写入 X-Gateway-Signature

为什么用旧 secret_key? 你的系统在收到此 webhook 之前还不知道新 secret,但本地有旧值——用旧值签名意味着只有”知道旧 secret 的中枢”才能发出有效消息,防伪造。验证通过后,再把 after.secret_key 更新到本地。

Python 校验示例

import hmac, hashlib

def verify(old_secret: str, timestamp: str, body: bytes, signature: str) -> bool:
    msg = timestamp.encode() + b"." + body
    expect = "sha256=" + hmac.new(old_secret.encode(), msg, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expect, signature)

Go 校验示例

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
)

func Verify(oldSecret, timestamp string, body []byte, signature string) bool {
    mac := hmac.New(sha256.New, []byte(oldSecret))
    mac.Write([]byte(timestamp))
    mac.Write([]byte("."))
    mac.Write(body)
    expect := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expect), []byte(signature))
}

3. 重试策略

接收方需要在 5 秒内返回 HTTP 2xx,否则视为失败。

失败后按指数退避重试:

第 N 次延迟
1立即
230s
32min
410min
51h

5 次全失败后进入死信队列webhook_deliveries.status = 'dead'),不再自动重试。 用户可在 控制台 → 主机详情 → Webhook 历史 看到失败原因并手动 retry。

4. 幂等性建议

中枢可能因网络重传同一个 delivery_id 多次,接收方应按 delivery_id 去重

INSERT INTO webhook_inbox (delivery_id, event, payload)
VALUES ($1, $2, $3)
ON CONFLICT (delivery_id) DO NOTHING;

返回 200 即可,不要返回 4xx(会触发重试浪费资源)。

5. 查询投递历史

用户接口:

GET /v1/user/hosts/{host_id}/webhook-deliveries?limit=20
Authorization: Bearer ak_<key_id>.<secret>

返回最近 N 次投递记录:状态码、响应体摘要、首发/末次重试时间、是否落入死信。

6. 时间戳防重放

X-Gateway-Timestamp 是 Unix 秒。接收方建议拒绝 |now - timestamp| > 300s 的请求, 即使签名合法也不接受 —— 防止重放老旧投递。