回调签名与验签
使用用户级 HMAC-SHA256 签名验证 CatFee Webhook 来源与完整性,包含用户中心设置、快速接入示例、防重放与幂等处理。
开启回调签名后,CatFee 会为你的 HTTP/HTTPS 回调附加 HMAC-SHA256 签名。你的服务器使用专属密钥验签,确认消息来自持有该密钥的一方,并且请求体和事件请求头没有被修改。
每个用户只有一个密钥和一个开关,统一作用于该用户所有 HTTP/HTTPS 回调。默认关闭;首次开启生成密钥,关闭后保留,重新开启仍使用原密钥。Telegram 和邮件通知不受影响。
快速接入
按下方示例准备接收接口:先获取原始请求体,验签通过后再解析 JSON。
在用户中心开启回调签名,复制当前用户的签名密钥(下文记为
secret),保存到接收端的环境变量或密钥存储中。使用下方固定测试向量校验实现,再发送测试通知,确认实际回调能通过验签。
接入持久化幂等和业务处理;可靠接收成功后返回
HTTP 200,响应正文为success。
首次开启立即影响后续发送,包含已经排队的重试通知。请安排好开启、配置密钥和部署验签代码的切换窗口。切换过程中验签失败应返回非 2xx,由平台按现有策略重试;不要临时放行无签名或签名错误的请求。
签名的开启、查询与关闭
签名的开启、查询和关闭均在用户中心操作。
开启签名并获取密钥
登录用户中心,进入回调签名设置。
开启签名开关;首次开启时,系统会为当前用户生成一个签名密钥。
复制签名密钥,保存到接收端的环境变量或密钥存储中,例如
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,也不要把密钥放进回调请求。
计算步骤
将从用户中心复制的
secret按标准 Base64解码为 32 字节密钥,不要直接把 Base64 字符串当作 HMAC 密钥。按以下顺序拼接前缀,再追加收到的原始请求体字节:
使用恒定时间比较函数,将计算结果与收到的签名比较。示例分别使用
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?