Webhooks(事件推送)
Webhook 是一种"服务器主动通知合作伙伴"的机制:当 License 发生关键事件(激活、续期、吊销、过期等)时,服务器会向您注册的 URL 发送 HTTPS POST 请求。
适用场景
- 实时集成:您的服务需要立即响应 license 变化(例如下发新功能、停止服务)
- 审计同步:把事件流同步到您自己的审计系统
- 数据仓库:把事件喂给数据仓库做 BI 分析
怎么注册
仅管理员可注册:
- 后台登录 → 侧边栏 Webhooks → + Register
- 输入 URL(必须是 HTTPS,公网可访问)
- 选择 scope:
all— 接收所有事件(推荐)- 特定事件名(见下表)
注册时显示一次性共享密钥,这是验证签名的关键。
事件类型
| 事件名 | 触发时机 |
|---|---|
| --- | --- |
license.activated | 设备激活成功 |
license.renewed | License 续期 |
license.upgraded | Plan 升级 |
license.revoked | License 被吊销 |
license.expiring | License 即将到期(剩余 < 7 天) |
license.in_grace | License 进入宽限期 |
license.expired | License 完全失效 |
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 白名单)
下一步
- API 参考 · webhooks — API 端点规范
- API 参考 · webhooks/register — 注册端点
- Admin 文档 · Webhooks