登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

Label Studio webhook 任务完成回调的签名处理

来源:17golang原创

时间:2026-10-28 10:12:51 459浏览 收藏

Label Studio webhook 的签名处理要先区分原生能力与自建协议:普通 webhook 可配置认证请求头,但不能把静态头值当作逐次计算的正文签名。若接收系统要求 HMAC,可在确认来源身份的自建中继上计算签名,再由业务接收端校验。本文中的签名头均为自定义协议,不是 Label Studio 内置字段。

官方文档地址:https://labelstud.io/guide/webhooks.html

为什么回调到了,接收端仍在报缺少签名

以一个标注结果接入服务为例:接收代码要求每次请求携带动态摘要,而 Label Studio 仅配置了回调地址。此时先核对双方协议,不能直接假定存在 X-Hub-Signature 一类头。官方普通 webhook 配置允许添加自定义 header;公开发送实现也展示了配置 headers 与 JSON 正文的发送方式,没有本文中时间戳加正文的 HMAC 生成逻辑。这一判断针对普通 webhook,不代替定制部署或其他版本的接口约定。

静态认证头适合让入口识别持有共享凭据的发送方,应通过 HTTPS 传输并限制访问;它不随正文变化,也不独自阻止已捕获请求重放。不要在配置页预填一个固定的“签名”字符串,然后期待正文变化时它自动更新。

先把“任务完成”对应到真正的业务事件

常见接入点是 ANNOTATION_CREATED。官方事件参考将它定义为任务新增标注时发送的事件,载荷含 annotation.id、annotation.task、annotation.was_cancelled 等信息。业务上需要多少份标注、是否审阅通过、是否允许跳过,应由项目规则判断;不能把收到一次创建事件直接解释成任务全部完成。

因此入口需保留 action、项目标识与标注标识,再由后台判断是否满足业务完成条件。was_cancelled 为 true 的跳过标注不应未经判断进入“有效完成”分支。若仅发送 action 而关闭完整 payload,接收端也就没有足够字段执行这些判断。

需要动态签名时,明确中继的信任边界

可采用两个独立边界:Label Studio 到中继使用 HTTPS 与专用静态认证头;中继认证成功后,以保留的原始正文生成 HMAC,再发送给业务接收端。中继必须拒绝未认证来源,覆盖而不是透传外部传入的签名头,并禁止绕过它直达业务端。中继签名证明的是中继认可并发送了这些字节;若源凭据泄露,签名无法恢复源身份的可信性。

Label Studio来源认证和自建中继签名的静态关系图
信任边界结构图:来源认证和中继签名是两种机制,连线表达依赖关系,并非运行记录。

下面定义一个原创的专用路由协议:只接受 POST /hooks/label-studio,不接收查询参数;签名消息包含协议版本、固定路由、十进制秒级时间戳、随机 nonce 和原始正文。中继使用独立随机密钥,双方从受控配置加载相同的密钥字节,不复用 Label Studio API token。

自定义请求头含义
X-Relay-Time发送时间,秒级十进制字符串
X-Relay-Nonce每次投递新建的32位小写十六进制随机值
X-Relay-Signaturesha256= 前缀加64位小写十六进制摘要

这些名字只属于本例。中继可用 secrets.token_hex(16) 创建 nonce;接收端从真实请求读取头和原始 bytes。JSON 解析后重新序列化可能改变空白或编码,必须先针对实际传输的 bytes 验签,再解析业务内容。

把签名计算与防重放存储分开实现

以下函数只演示消息格式、时间窗与摘要比较,没有启动 HTTP 服务。调用方必须在读取正文时限制大小、拒绝重复协议头,并确认真实方法、路径和无查询参数;不能对其他路由复用这段固定路由消息。

import hashlib
import hmac
import re

def message(raw_body: bytes, timestamp: str, nonce: str) -> bytes:
    # 固定协议版本和接收路由,末尾正文保持原始字节。
    prefix = f"v1\nPOST\n/hooks/label-studio\n{timestamp}\n{nonce}\n"
    return prefix.encode("ascii") + raw_body

def make_signature(key: bytes, raw_body: bytes,
                   timestamp: str, nonce: str) -> str:
    # key来自受控配置;发送端转发的正文必须就是raw_body。
    digest = hmac.new(
        key, message(raw_body, timestamp, nonce), hashlib.sha256
    ).hexdigest()
    return "sha256=" + digest

def valid_signature(key: bytes, raw_body: bytes, timestamp: str,
                    nonce: str, supplied: str, now: int) -> bool:
    # 先限制格式,避免非ASCII头值、巨大整数或错误摘要格式。
    if not re.fullmatch(r"[0-9]{1,12}", timestamp):
        return False
    if not re.fullmatch(r"[0-9a-f]{32}", nonce):
        return False
    if not re.fullmatch(r"sha256=[0-9a-f]{64}", supplied):
        return False
    if abs(now - int(timestamp)) > 300:
        return False
    expected = make_signature(key, raw_body, timestamp, nonce)
    # 使用恒时比较函数;通过此处还不代表完成防重放。
    return hmac.compare_digest(expected, supplied)

300 秒是本例自定的允许时钟偏差,不是产品规定。接收端使用自己读取的当前时间,维护时钟同步。通过摘要校验后,还要在共享持久化存储中原子登记 nonce,禁止多进程分别用普通内存集合做去重。记录至少保留到该时间戳不可能再次被时间窗接受:允许未来300秒时,固定从接收时保留300秒不一定够。

推荐将已验签事件的持久化收件记录与 nonce 唯一性放在同一事务中;提交成功才返回成功响应,异步工作者按业务幂等键处理。若先消耗 nonce,随后落库失败,又把后续重投当成成功重复,可能丢事件。重复投递的响应应依据已持久化状态决定,不能只看 nonce 存在就宣称业务完成。

验签输入与持久化收件职责的静态关系图
接收职责结构图:摘要输入、时间窗口、nonce记录与持久化事件各自约束不同问题。

怎样判断修复方向正确

下面是供接入时采用的预期用例,不是本文已经执行的测试结果。它们针对中继与业务服务自定义协议,不需要把产品改造成原生支持这些头。

输入变化预期判断
正文改一个字节,沿用原摘要摘要不匹配,拒绝
时间戳超过约定窗口拒绝,不进入业务处理
同nonce与同事件再次到达查询持久化收件状态,不重复产生副作用
验签通过但action不在允许集合不执行标注完成处理
标注创建但未满足项目完成规则保存事件并重新判断任务状态,不提前完成任务

接入时容易混淆的三个问题

只在接收端增加 HMAC 代码够吗?不够。发送端必须按同一协议生成动态签名;普通静态 header 配置不会替你计算它。无需动态签名的内网接入,也可按实际需求采用 HTTPS 与来源认证,无须虚构额外机制。

签名成功等于任务已完成吗?不等于。签名回答字节完整性与持钥身份问题;action、项目归属、有效标注和完成规则仍需业务判断。

记录哪些排查信息?记录事件类型、项目和标注标识、接收状态及错误类别;不要把认证头、密钥或含个人信息的完整标注正文写入普通日志。保留可补偿的收件状态,比只记录HTTP状态更有用。

资料:官方事件字段说明;普通 webhook 发送实现;Python HMAC 与摘要比较。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>