异步模型任务通常不是立即返回最终结果。应用提交任务后,模型服务在处理完成时向回调地址发送状态和结果。这个机制可以减少轮询,却也带来一个容易被忽略的问题:只要回调地址暴露在公网,任何能访问该地址的人都可能尝试构造请求。
如果应用收到JSON后直接更新任务状态,攻击者可能伪造“任务成功”;如果一份真实回调被重复发送,系统又可能重复发通知、重复写数据或重复触发下游流程。HTTPS可以保护传输链路,但它本身不能证明请求正文一定来自预期业务方。
本文给出一套适用于函数计算HTTP入口的应用层验证方法:对原始请求体进行HMAC签名,使用时间窗限制旧请求,使用Nonce阻止同一请求再次消费,最后再用业务幂等键保护下游操作。
一、回调接口需要回答四个问题
一个可靠的回调入口,不应只问“JSON格式对不对”,还要回答:
- 请求是否由持有共享密钥的一方生成?
- 请求在允许的时间范围内吗?
- 这个随机标识是否已经被使用过?
- 即使重复请求通过了边界,下游业务会不会重复执行?
这四个问题分别对应签名验证、时间窗、Nonce去重和业务幂等。它们不是互相替代的关系。
只有HMAC,没有时间窗和Nonce,一份过去的合法请求仍可能被原样重放;只有Nonce,没有可靠签名,攻击者可以不断制造新Nonce;只有业务幂等,伪造请求仍可能污染任务状态。
二、签名必须基于原始字节
推荐的签名材料可以定义为:
时间戳 + 点号 + Nonce + 点号 + 原始请求体字节
发送方使用共享密钥执行HMAC-SHA256,并把时间戳、Nonce和签名放在请求头中。接收方必须用收到的原始请求体重新计算签名,而不是先把JSON解析成对象、重新排序字段后再序列化。
原因很简单:下面两段JSON表达的业务内容可能相同,但字节序列不同。
{"task_id":"t-100","status":"succeeded"}
{"status":"succeeded","task_id":"t-100"}
签名验证关心的是发送方实际签过的字节。若接收方先解析再重新生成JSON,空格、编码和字段顺序都可能变化,导致合法请求被误判。
比较签名时,应使用恒定时间比较函数,例如Python标准库的hmac.compare_digest,不要直接用普通字符串比较。这样可以降低通过比较耗时推测签名差异的风险。
三、正确的验证顺序
回调处理顺序建议固定为:
读取受限长度的原始请求体 → 检查必要请求头 → 验证时间戳窗口 → 重新计算并比较HMAC → 原子占用Nonce → 解析JSON → 校验业务字段 → 执行业务幂等操作
注意,签名验证应在JSON解析和业务处理之前完成。这样不仅能避免未认证内容进入业务逻辑,也能减少恶意复杂JSON消耗解析资源的机会。
时间窗可以从五分钟左右开始,根据模型任务回调的网络延迟再调整。窗口过长会扩大重放机会,过短则容易因网络抖动和机器时钟偏差拒绝合法请求。生产环境需要保持系统时间可靠,并在监控中区分“过期请求”和“签名错误”。
Nonce是发送方为每次回调生成的唯一随机值。接收方验证签名后,要以“只在不存在时写入”的原子操作将Nonce存入共享存储,并设置略长于时间窗的过期时间。若同一个Nonce再次出现,立即拒绝。
四、一个可验证的Python核心示例
下面的示例只展示验签核心。为了便于本地理解,Nonce存储使用内存集合;在函数计算多实例环境中,它不能提供全局防重放能力,生产环境必须替换成支持原子写入和TTL的共享存储。
import hashlib
import hmac
import json
import time
WINDOW_SECONDS = 300
def sign(secret: bytes, timestamp: int, nonce: str, body: bytes) -> str:
message = str(timestamp).encode() + b"." + nonce.encode() + b"." + body
return hmac.new(secret, message, hashlib.sha256).hexdigest()
def verify(secret, timestamp, nonce, body, signature, nonce_store, now=None):
current = int(time.time()) if now is None else now
if abs(current - timestamp) > WINDOW_SECONDS:
raise ValueError("expired request")
expected = sign(secret, timestamp, nonce, body)
if not hmac.compare_digest(expected, signature):
raise ValueError("invalid signature")
if not nonce_store.claim(nonce):
raise ValueError("replayed request")
return json.loads(body)
验证该核心逻辑时,至少要覆盖五类情况:
- 合法请求可以通过。
- 请求体被修改后必须失败。
- 使用错误密钥必须失败。
- 超出时间窗必须失败。
- 同一Nonce第二次使用必须失败。
本地测试运行了上述5项用例,结果均通过。但这只证明签名与验证函数符合示例规则,不等于整个生产系统已经安全。共享Nonce存储、密钥保管、网关限制、业务幂等和告警仍需单独验证。
五、在函数计算中怎样放置这道边界
函数计算的Web函数可以直接处理HTTP请求,适合承接模型服务的回调入口。函数计算Web函数
如果为函数配置自定义域名,可以使用HTTPS访问,并根据产品能力配置相应的认证方式。函数计算自定义域名
这里需要明确边界:本文所讲的HMAC、时间窗和Nonce,是应用层协议设计,不应描述成函数计算自动提供的回调验签功能。HTTPS负责保护传输,平台访问控制负责限制入口,HMAC负责验证双方约定的消息真实性,三者各有职责。
部署时可以把链路分成两层:
第一层是入口层,负责HTTPS、请求大小限制、必要的来源限制和基础流量保护。
第二层是函数业务层,只做固定顺序的验签、Nonce占用、字段检查和任务状态推进。未通过验证的请求,不进入消息队列,也不修改数据库。
如果模型供应商不支持自定义签名协议,应优先使用其官方Webhook签名机制。若对方只允许在回调URL中附加固定Token,该Token仍应放在KMS中管理,同时需要结合来源限制、业务幂等和异常监控;不要把固定Token当成完整防重放方案。
六、共享密钥应该如何保存与轮换
回调密钥不能再次写进函数代码。推荐将其保存在KMS凭据中,让函数通过执行角色和最小权限按需读取。KMS可以控制谁有权读取指定凭据,函数计算服务角色则为运行实例提供临时身份。KMS凭据常见问题
轮换时,可以在短暂过渡期允许“当前密钥”和“上一密钥”共同验签:
- 创建新密钥并写入新的凭据版本。
- 让发送方开始使用新密钥。
- 接收方先用当前密钥验证,失败后仅在轮换窗口内尝试上一密钥。
- 确认新版本回调稳定后,撤销旧密钥。
- 删除应用对上一版本的读取路径。
这个兼容窗口必须短且可观测。如果代码长期接受所有历史密钥,轮换就失去了降低泄露风险的意义。
日志中不要打印共享密钥、完整签名、完整敏感请求体或回调URL中的秘密参数。可以记录请求ID、时间戳偏差、Nonce的哈希摘要、签名验证结果、密钥版本标识和业务任务ID。
七、防重放之后还需要业务幂等
Nonce解决的是“同一份认证消息不能重复消费”,业务幂等解决的是“同一业务结果不能重复产生副作用”。
例如模型服务可能因为网络超时,用新的Nonce重新发送同一个任务结果。这个请求在安全层面是新的合法消息,但业务层仍不应该重复扣费、重复发通知或重复生成文章。
因此,数据库更新还应使用任务ID和目标状态作为幂等条件。例如只有任务处于processing时,才允许第一次转为succeeded;后续相同结果只返回已经处理,不再次执行下游动作。
安全验签和状态机应同时存在:
HMAC确认消息来源 → 时间窗和Nonce限制消息使用方式 → 任务状态机限制业务状态变化 → 幂等记录限制副作用次数
这比把所有希望都寄托在一个签名字段上更可靠。
八、上线前的最小检查清单
- 回调入口只接受HTTPS。
- 对原始请求体字节签名,不对重新序列化的JSON签名。
- 签名算法固定为HMAC-SHA256等明确方案,不自创加密算法。
- 使用恒定时间函数比较签名。
- 时间窗明确,服务器时间保持同步。
- Nonce存入共享存储,采用原子写入并设置TTL。
- 验签成功后才解析JSON和执行业务。
- 任务状态更新具有幂等条件。
- 共享密钥放入KMS,并通过函数执行角色读取。
- 轮换期间只短暂接受当前和上一版本。
- 日志不包含秘密、完整签名和敏感正文。
- 对签名错误、过期请求和重放请求分别计数告警,但对外响应保持简洁。
对于OPC一人公司,最实际的目标不是一次建立复杂安全平台,而是把回调入口从“收到就执行”升级成“验证后才进入状态机”。在“智能体来了”的内容方法中,这也是AI大模型工具深度运用的一部分:AI能力进入生产流程后,必须与身份、安全边界和可追踪状态一起设计。
结语
异步回调的风险不只是假请求,还包括合法请求被重复使用。HMAC证明请求由持有密钥的一方生成,时间窗限制请求寿命,Nonce阻止同一消息重放,业务幂等防止同一任务产生重复副作用。
四层机制缺一不可。先把验证顺序固定下来,再将内存Nonce替换为共享TTL存储,并把密钥迁入KMS,才是一条适合逐步落地的云原生改造路径。
说明:本文使用AI工具辅助进行结构整理和语言优化,方案设计、示例逻辑与内容由发布者人工审核。示例只用于解释安全工程方法,不构成对具体业务安全性的保证;生产环境请结合威胁模型、阿里云最新官方文档及第三方模型服务的官方回调规范进行评审。