foren.dev

Webhooks(事件推送)

Webhook 是一种"服务器主动通知合作伙伴"的机制:当 License 发生关键事件(激活、续期、吊销、过期等)时,服务器会向您注册的 URL 发送 HTTPS POST 请求。

适用场景

  • 实时集成:您的服务需要立即响应 license 变化(例如下发新功能、停止服务)
  • 审计同步:把事件流同步到您自己的审计系统
  • 数据仓库:把事件喂给数据仓库做 BI 分析

怎么注册

仅管理员可注册:

  1. 后台登录 → 侧边栏 Webhooks → + Register
  2. 输入 URL(必须是 HTTPS,公网可访问)
  3. 选择 scope:
  • all — 接收所有事件(推荐)
  • 特定事件名(见下表)

注册时显示一次性共享密钥,这是验证签名的关键。

事件类型

事件名触发时机
------
license.activated设备激活成功
license.renewedLicense 续期
license.upgradedPlan 升级
license.revokedLicense 被吊销
license.expiringLicense 即将到期(剩余 < 7 天)
license.in_graceLicense 进入宽限期
license.expiredLicense 完全失效
certificate.generated客户端证书签发
certificate.revoked客户端证书吊销

投递格式

服务器 POST JSON 体到您的端点:

POST /your/webhook HTTP/1.1
Host: your-app.example.com
Content-Type: application/json
X-License-Webhook-Signature: sha256=9c8a7b6c...
X-License-Event: license.activated
X-License-Delivery: 1

{
  "event": "license.activated",
  "timestamp": "2026-09-29T12:00:00Z",
  "license_id": 2,
  "license_key": "COF-20260521-CC30C5",
  "holder": "Beta",
  "product": "CoForen",
  "plan": "team",
  "machine_fingerprint": "sha256:abc...",
  "issued_at": "2026-05-21T10:00:00Z",
  "expires_at": "2027-05-21T10:00:00Z"
}

不同事件类型 payload 字段略有不同,核心字段一致。

验证签名

必须验证 X-License-Webhook-Signature 头防止伪造。

签名是请求 body 的 HMAC-SHA256,使用您注册时获得的密钥。

Python

import hmac, hashlib

def verify(secret: str, body: bytes, sig_header: str) -> bool:
    expected = hmac.new(
        secret.encode('utf-8'),
        body,
        hashlib.sha256
    ).hexdigest()
    # sig_header: "sha256=<hex>"
    received = sig_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)

# 在 Flask 中:
@app.post('/webhook')
def receive():
    body = request.get_data()  # raw bytes
    sig = request.headers.get('X-License-Webhook-Signature', '')
    if not verify(SECRET, body, sig):
        return 'invalid signature', 401
    event = request.json
    process(event)
    return 'ok', 200

Node.js

const crypto = require('crypto');

function verify(secret, body, sigHeader) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');
  const received = sigHeader.replace(/^sha256=/, '');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received)
  );
}

app.post('/webhook', (req, res) => {
  const body = req.rawBody; // need raw body parser
  const sig = req.headers['x-license-webhook-signature'] || '';
  if (!verify(SECRET, body, sig)) {
    return res.status(401).send('invalid signature');
  }
  process(req.body);
  res.status(200).send('ok');
});

重试机制

如果您的端点返回非 2xx 状态码:

  • 重试 最多 5 次
  • 退避策略:指数 backoff(1 分钟、2 分钟、4 分钟、8 分钟、16 分钟)
  • 5 次失败后事件标记为永久失败,需要在后台手动 retry

后台 → Webhooks 页面会显示Failed Events 面板,可以:

  • 查看失败原因(last_error 字段)
  • 点击 Retry 手动重投
  • 修复端点问题后再 retry

最佳实践

✅ 应该❌ 不应该
------
验证签名信任任何 POST 请求
幂等处理(用事件 ID 去重)假设每条事件只投递一次
5xx 响应让服务器重试总是返回 200
限制处理时间(< 30s)长时间同步处理
收到事件后立刻返回 200,处理放后台在请求线程中处理复杂逻辑

安全提示

  • 不要把 webhook secret 写到前端代码
  • 定期轮换 secret(删除旧 webhook,重新注册)
  • 仅接受来自 foren.dev 的 HTTPS 请求(可加 IP 白名单)

下一步