一、Webhook在短信系统中的位置
企业调用短信API后,接口通常只返回“请求已接收”或“提交成功”,最终是否送达需要等待后续状态。Webhook把状态变化主动推送到企业指定的HTTPS地址,避免业务系统持续轮询。
除送达状态外,Webhook还可用于上行短信、退订、号码回复或账户事件。不同事件应使用明确的类型字段,不要依赖自由文本猜测事件含义。
二、回调端点的基本要求
- 必须使用HTTPS,并配置有效证书和稳定域名。
- 只接受需要的HTTP方法和内容类型。
- 限制请求体大小,校验JSON结构和必填字段。
- 先完成必要校验并入队,再快速返回成功状态。
- 不要在同步请求中执行耗时的报表、通知或第三方调用。
- 记录请求时间、来源、事件ID和处理结果。
回调端点的目标不是一次完成所有业务逻辑,而是安全、快速地接收事件。复杂处理应进入内部消息队列,由后台任务异步执行。
三、建议的事件数据结构
下面是通用示例,实际字段名称应以平台文档为准:
{
"event_id": "evt_01J...",
"event_type": "sms.delivery.updated",
"message_id": "msg_01J...",
"client_reference": "order_20260716_001",
"status": "delivered",
"occurred_at": "2026-07-16T06:30:15Z",
"destination": "+国家码手机号",
"error_code": null
}
event_id用于事件去重,message_id关联短信平台记录,client_reference关联企业内部业务,occurred_at用于判断事件顺序。手机号等敏感字段应按最小需要保存和展示。
四、签名验证与来源认证
Webhook不能只依赖来源IP判断真实性。常见做法是平台使用共享密钥对“时间戳+原始请求体”计算HMAC签名,企业系统收到请求后使用相同算法重新计算并进行恒定时间比较。
# 通用Python示例,头名称和签名格式需按实际平台调整
import hashlib
import hmac
signed_payload = timestamp.encode() + b"." + raw_body
expected = hmac.new(
WEBHOOK_SECRET.encode(),
signed_payload,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, received_signature):
raise PermissionError("invalid webhook signature")
还应检查时间戳是否在允许窗口内,防止攻击者重复发送历史有效请求。密钥应保存在服务端密钥管理系统,并支持轮换。
五、幂等、重复与乱序事件
Webhook平台通常采用“至少一次”投递,因此同一个事件可能重复到达。数据库应对事件ID建立唯一约束,已处理事件直接返回成功,不能重复执行退款、通知或业务操作。
事件也可能乱序到达,例如“delivered”先于“sent”。更新状态时应结合状态优先级和事件发生时间,避免较旧事件把最终状态覆盖成中间状态。
| 问题 | 推荐处理 |
|---|---|
| 重复事件 | 使用event ID唯一索引,重复请求返回成功 |
| 乱序事件 | 比较occurred_at及状态优先级 |
| 未知message ID | 暂存事件并告警,不直接丢弃 |
| 数据库暂时不可用 | 返回可重试状态,并保护服务不过载 |
六、HTTP响应与重试策略
完成验签和可靠入队后,应尽快返回2xx。格式错误、签名错误等永久问题不应通过无限重试解决;系统临时不可用则可以返回5xx,让平台稍后重试。
企业应确认平台的重试次数、退避时间和最长保留期。自己的处理服务也需要死信队列,用于保存多次失败的事件,避免异常数据永久丢失。
七、日志、数据和监控
- 保存event ID、message ID、业务流水号和事件类型。
- 保存接收时间、发生时间、处理时间和最终结果。
- 记录签名验证结果,但不要在日志中输出密钥。
- 对手机号等敏感字段进行掩码或限制访问。
- 建立回调延迟、失败率、重复率和积压量仪表盘。
- 定期演练密钥轮换、数据库故障和消息队列积压。