For the complete documentation index, see llms.txt. This page is also available as Markdown.

回调签名与验签

使用用户级 HMAC-SHA256 签名验证 CatFee Webhook 来源与完整性,包含用户中心设置、快速接入示例、防重放与幂等处理。

开启回调签名后,CatFee 会为你的 HTTP/HTTPS 回调附加 HMAC-SHA256 签名。你的服务器使用专属密钥验签,确认消息来自持有该密钥的一方,并且请求体和事件请求头没有被修改。

每个用户只有一个密钥和一个开关,统一作用于该用户所有 HTTP/HTTPS 回调。默认关闭;首次开启生成密钥,关闭后保留,重新开启仍使用原密钥。Telegram 和邮件通知不受影响。

签名功能不改变通知 JSON 结构,delegation_type 等字段的兼容行为保持不变。事件协议仍通过 X-EVENT-VERSION: 20260930 标识;签名中的 v1 是独立的签名格式版本。事件字段说明见回调(Webhook)。

快速接入

  1. 按下方示例准备接收接口:先获取原始请求体,验签通过后再解析 JSON。

  2. 在用户中心开启回调签名,复制当前用户的签名密钥(下文记为 secret),保存到接收端的环境变量或密钥存储中。

  3. 使用下方固定测试向量校验实现,再发送测试通知,确认实际回调能通过验签。

  4. 接入持久化幂等和业务处理;可靠接收成功后返回 HTTP 200,响应正文为 success。

首次开启立即影响后续发送,包含已经排队的重试通知。请安排好开启、配置密钥和部署验签代码的切换窗口。切换过程中验签失败应返回非 2xx,由平台按现有策略重试;不要临时放行无签名或签名错误的请求。

签名的开启、查询与关闭

签名的开启、查询和关闭均在用户中心操作。

开启签名并获取密钥

  1. 登录用户中心,进入回调签名设置。

  2. 开启签名开关;首次开启时,系统会为当前用户生成一个签名密钥。

  3. 复制签名密钥,保存到接收端的环境变量或密钥存储中,例如 CATFEE_WEBHOOK_SECRET。

下文和验签代码中的 secret 均指从用户中心复制的签名密钥,其格式为标准 Base64 编码的 32 字节随机密钥。它与购买能量 API 的 API Secret 不同,请勿混用,也不要放进前端代码、日志或公开仓库。

查询当前设置

在用户中心的回调签名设置中查看当前开关状态和签名密钥。从未开启时,尚未生成密钥;首次开启后,关闭开关仍会保留密钥。

关闭签名

在用户中心关闭签名开关。关闭后,后续 HTTP/HTTPS 回调不再附加签名;重新开启仍使用原密钥。

开关统一作用于当前用户的所有 HTTP/HTTPS 回调,并在每次实际发送时生效,同样适用于测试通知和已有通知的重试。关闭签名时,应同步调整自己的接收策略。

请求头与签名规则

开启签名后,在原有事件请求头基础上增加两个头:

请求头
说明

X-EVENT-ID

事件 ID,参与签名和幂等处理;重试时不变

X-EVENT-TYPE

事件类型,例如 EVENT_DELEGATION

X-EVENT-VERSION

业务事件协议版本,当前为 20260930

X-EVENT-TIMESTAMP

新增,本次发送的 Unix 秒级时间戳,十进制字符串

X-EVENT-SIGNATURE

新增,v1= 加 64 位小写十六进制 HMAC-SHA256 结果

请求头名称不区分大小写,参与签名的值不能改写。不需要传递密钥 ID,也不要把密钥放进回调请求。

计算步骤

  1. 将从用户中心复制的 secret 按标准 Base64解码为 32 字节密钥,不要直接把 Base64 字符串当作 HMAC 密钥。

  2. 按以下顺序拼接前缀,再追加收到的原始请求体字节:

  1. 使用恒定时间比较函数,将计算结果与收到的签名比较。示例分别使用 timingSafeEqual、compare_digest 和 MessageDigest.isEqual。

分隔符 \n 是一个 LF 字节(0x0a),不是反斜杠加字母 n,也不是 CRLF。前缀字段为非空可打印 ASCII。最后一个前缀字段后有一个 LF,body 后不额外添加换行。

接收端示例

选择所用语言,按下方说明保存代码并配置 CATFEE_WEBHOOK_SECRET。示例已包含签名格式检查、300 秒时间窗口和恒定时间比较;你的业务幂等及可靠落库仍需自行实现。代码按文件分为两个标签,可使用代码块的复制按钮分别保存。

示例使用 CommonJS,将两个代码标签中的源码分别保存为 verify.js 和 receiver.js,放在同一目录。回调路由必须注册在全局 express.json() 之前,使用原始请求体验签后再解析 JSON。

将两个代码标签中的源码分别保存为 verify.py 和 receiver.py,放在同一目录。通过 await request.body() 获取原始字节,验签通过后再解析 JSON;不要将 await request.json() 的结果重新序列化用于验签。

将两个代码标签中的源码分别保存为 VerifyWebhook.java 和 CallbackController.java,放入同一个包,并补上对应的 package 声明。要求 Java 17 或更高版本。@RequestBody 使用 byte[] 接收原始字节,验签通过后再解析为业务对象。

以上控制器用于快速联调验签,TODO 处必须接入实际业务或可靠队列后再上线。若回调入口服务于多个 CatFee 用户,应通过自己配置的路由或其他可信上下文选择用户密钥,不能信任未验签 body 声称的用户身份。

防重放、幂等与重试

签名有效并不表示这条通知从未收到过。接收端还需要:

  • 时间窗口:建议仅接受与当前时间相差不超过 300 秒的请求,同时拒绝过旧和过远的未来时间。示例已经实现此检查,请保持服务器时钟同步。

  • 持久化幂等:按“用户+X-EVENT-TYPE+X-EVENT-ID”做原子去重。只在业务事务成功或消息已可靠入队后标记完成,处理失败时允许重试;不要用仅保存在进程内的 Set 代替生产幂等。

  • 重复通知:对于已成功处理的事件,返回 HTTP 200 和 success,不要再次执行业务。

  • 重试验签:平台每次发送重新生成时间戳和签名,事件 ID 不变。验签时间使用 X-EVENT-TIMESTAMP,不要使用 body 内的业务发生时间;同一秒内的相同请求可能有相同签名。

签名失败返回非 2xx;验签通过且可靠接收后返回 HTTP 200,正文为 success。签名回调不自动跟随 3xx 重定向,请直接配置最终接收地址。

使用固定测试向量验证

将以下内容保存为 test-vector.json,与验签源码放在同一目录。仅用于本地测试,不可用于正式回调:

原始 body 是以下单行 UTF-8 文本,行末没有换行:

按所选语言运行自测:

示例自测将当前时间固定为 1790726400,校验正确签名、正文或请求头篡改、错误密钥以及时间窗口。实际服务器验签必须使用真实当前时间,不能沿用测试中的固定时间。

完成自测后,在用户中心对已配置的订阅发送测试通知,验证真实回调能通过验签。测试通知使用同一签名开关和密钥,目前支持余额、订阅和代理事件;测试通知不会写入重试队列。

常见问题

问题
排查方式

验签一直失败

确认使用的是回调 secret,并先做标准 Base64 解码;不要使用购买能量 API 的 API Secret

字段一样但签名不同

检查是否重新序列化 JSON,或增加了空格、Unicode 转义、末尾换行;应对原始字节验签

事件 ID 对不上

签名取 X-EVENT-ID 请求头,不取可能带 D-、B- 前缀的 JSON event_id

时间戳校验失败

使用 Unix 秒,不是毫秒或 ISO 日期;检查服务器时钟,不要用业务发生时间验签

开启后收到旧事件

开关作用于实际发送,已有待重试通知也会加签;按本次发送时间验签即可

关闭后再开启是否换 key

不会,用户继续使用同一个密钥

HTTP 是否支持

支持,验签规则与 HTTPS 相同;需要传输加密时使用 HTTPS

为什么回调地址跳转后收不到通知

签名回调不跟随重定向,应直接填写最终接收 URL

返回回调(Webhook)查看事件结构和业务字段。

Last updated

Was this helpful?