云选科普

模型异步回调验签:防伪造、防重放与幂等

公网回调不能收到 JSON 就执行。本文给出 HMAC 原始字节签名、时间窗、Nonce 去重、密钥轮换和业务幂等的完整验证顺序。

异步模型任务常通过 Webhook 返回结果。应用提交任务后,模型服务在处理完成时调用公网地址,回传状态或产物。这个机制减少了轮询,却也把一个可修改任务状态的入口暴露在网络上。

HTTPS 能保护传输过程,但不能单独证明请求正文来自预期发送方。一个可靠的回调入口至少要回答四个问题:消息是谁生成的、是否仍在有效期内、是否已经使用过,以及重复业务结果会不会再次产生副作用。

四层边界分别解决什么问题

建议把回调安全拆成四层:

  • HMAC 签名:证明发送方持有共享密钥,并检测正文是否被修改;
  • 时间窗:限制合法消息可以被使用的时间;
  • Nonce:阻止同一份已签名消息在窗口内再次消费;
  • 业务幂等:阻止同一任务结果因合法重试而重复扣费、发通知或写数据。

这四层不能互相替代。只有 HMAC 时,旧的合法请求仍可能被重放;只有 Nonce 时,攻击者可以不断生成新的随机值;只有幂等时,伪造请求仍可能污染状态。

如果模型供应商提供官方 Webhook 签名协议,应优先严格实现其规范,包括签名材料、请求头名称、时间容差和密钥轮换方式,不要自行发明不兼容的协议。

签名必须覆盖原始请求体

一个简单的签名材料可以定义为:

timestamp + "." + nonce + "." + raw_body

发送方使用共享密钥计算 HMAC-SHA256,并在请求头中携带时间戳、Nonce 和签名。接收方必须对收到的原始字节重新计算签名,不能先把 JSON 解析为对象、排序字段后再序列化。

下面两段 JSON 的业务含义可能相同,但字节序列不同:

{"task_id":"t-100","status":"succeeded"}
{"status":"succeeded","task_id":"t-100"}

如果发送方签的是第一段,接收方重新序列化为第二段再验签,合法请求也会失败。网关、框架或中间件如果修改请求体,同样会破坏签名。因此,入口层应保留原始字节,并限制允许读取的最大正文长度。

Python 的核心计算可以写成:

import hashlib
import hmac

def sign(secret: bytes, timestamp: int, nonce: str, body: bytes) -> str:
    message = (
        str(timestamp).encode("ascii")
        + b"."
        + nonce.encode("utf-8")
        + b"."
        + body
    )
    return hmac.new(secret, message, hashlib.sha256).hexdigest()

比较签名时使用 hmac.compare_digest。Python 官方文档明确建议在验证外部摘要时使用该函数,而不是普通相等比较,以降低基于比较耗时推测差异的风险。

验证顺序要固定

推荐处理顺序如下:

限制正文大小
  → 读取原始请求体
  → 检查必要请求头
  → 校验时间戳
  → 重新计算并比较 HMAC
  → 原子占用 Nonce
  → 解析 JSON
  → 校验字段与任务归属
  → 执行业务幂等状态变更

先验签、后解析,可以减少未认证内容进入业务逻辑,也能避免复杂 JSON 过早消耗解析资源。

时间窗没有适用于所有系统的固定值。窗口越短,重放空间越小,但对网络延迟和时钟偏差越敏感。应根据供应商回调延迟、跨地域网络和重试策略设置,并保持服务器时间同步。监控中要区分“时间戳过期”“签名错误”和“Nonce 已使用”,便于发现配置错误或攻击行为。

Nonce 必须存入所有函数实例共享的存储,并使用“仅当不存在时写入”的原子操作,同时设置略长于时间窗的 TTL。单实例内存集合只能用于本地测试,在函数扩容或进程重启后无法提供全局防重放能力。

安全验证之后仍要业务幂等

模型服务可能因为没有及时收到成功响应,使用新的 Nonce 重发同一个任务结果。这个请求在安全层面是新的合法消息,但业务层仍不应重复执行。

可使用任务 ID、回调事件 ID 或业务幂等键约束状态变化。例如:

UPDATE model_task
SET status = 'succeeded',
    result_url = :result_url,
    completed_at = CURRENT_TIMESTAMP
WHERE task_id = :task_id
  AND status = 'processing';

只有第一次满足状态条件的更新会成功。后续重复事件返回“已处理”,不再触发计费、通知或下游任务。若业务包含多个副作用,可以使用事务内事件表或 Outbox 模式,保证状态更新与事件记录一致。

幂等键的保存期限应覆盖供应商可能的最大重试周期。对会产生费用或不可逆动作的回调,还应记录事件来源、签名版本、任务状态迁移和最终处理结果。

密钥保存与轮换

共享密钥不能写入函数代码或回调 URL。可以保存在 KMS 等秘密管理系统中,让运行函数通过工作负载身份和最小权限读取。

轮换时允许当前密钥和上一密钥短暂共存:

  1. 创建新密钥并写入新版本;
  2. 让发送方开始使用新密钥;
  3. 接收方先验证当前版本,只在轮换窗口内尝试上一版本;
  4. 监控新版本的成功率和签名错误;
  5. 稳定后撤销旧密钥;
  6. 删除读取上一版本的代码路径或配置。

不要遍历所有历史密钥尝试验签,这会扩大泄露后的可用窗口,也会掩盖发送方配置错误。日志中同样不要记录完整密钥、完整签名或敏感正文;可记录密钥版本、Nonce 哈希摘要、时间偏差、任务 ID 和验证结果。

函数入口还需要哪些保护

应用层验签只是其中一道边界。入口层还应配置:

  • 仅接受 HTTPS;
  • 请求体大小和处理超时限制;
  • 合理的并发、速率和异常流量保护;
  • 必要时的来源网络限制;
  • 对失败响应保持简洁,避免泄露验证细节;
  • 对多次签名失败、过期请求和重放请求分别告警。

当供应商不支持自定义 HMAC,但提供固定 Token 时,应把 Token 放入秘密管理系统,并结合来源限制、幂等和监控。固定 Token 能提供基础认证,却不能单独完成消息完整性校验和防重放。

上线前测试清单

至少覆盖以下测试:

  • 合法请求可以通过;
  • 请求体修改一个字节后验签失败;
  • 使用错误密钥时失败;
  • 超出时间窗时失败;
  • 同一 Nonce 第二次使用时失败;
  • 多实例并发提交同一 Nonce 时只有一次成功;
  • 新 Nonce 重发同一任务结果时不会重复产生副作用;
  • 轮换窗口内新旧密钥按预期工作;
  • 日志、错误响应和监控事件不包含秘密;
  • KMS 或共享存储异常时系统安全失败,不绕过验证。

把 HMAC、时间窗、Nonce 和业务幂等组合起来,回调入口才能从“收到就执行”升级为“验证后才推进状态机”。

资料核对依据:

继续浏览

还想继续看,可以再看这些文章

适合想继续在同一主题下横向阅读、对比不同切入角度的用户。