线上偶发验签失败:网关吃前缀 + 时间戳单位

1 小时 30 分钟前
 luobobo8965

线上收款偶发失败,十笔错一笔。支付方后台显示「通知失败,已重试 5 次」,最后钱退回去了;我这边日志只有一行 signature mismatch。查了三天,根因是两个问题叠在一起:网关把回调路径前缀吃掉了,验签代码又把时间戳解析后重新拼回签名原文。

现象

[WARN] callback verify failed: signature mismatch
       orderNo=PO2026102300xx timestamp=1730xxxxxxx sign=V5x...
[INFO] retry 1/5 ... retry 5/5 failed, give up

三个特征:失败不是全量,约 1/10 ;本地用同一套密钥、同一份报文重算签名能过;把生产抓包原文放进测试环境回放也能过。

先把能查的手边项排一遍:三处密钥比对一致;拼接规则按文档排序、空值处理、URL 编码与抓包逐字节比对一致; NTP 偏差 200ms 内; nonce 在 Redis 写入读回正常。这些全对,线上照样失败,说明问题不在你正在看的这一层,得跳到链路两头去看。

第一步:先看请求有没有打到路由

应用侧日志搜订单号,0 条;网关访问日志同一时间窗里,发往 /pay/callback 的请求全是 404 。到这里就该停手:404 不是签名问题,验签代码一次都没执行。原因是网关转发时把 /pay 前缀吃掉了,proxy_pass 末尾少一个斜杠,location 匹配的那段被整个替换,打到后端的路径少了前缀。把末尾斜杠和 location 对齐、保留前缀就好了。

第二步:直接打自己的回调地址

用 curl POST 自己的回调 URL ,判据只有一条:返回的必须是应用层响应(比如 {"code":"PARAM_ERROR"}),不是网关的 404/502 HTML 页面。修好前缀后,失败率从 1/10 掉到约 1/3 ,说明还有第二个问题在挡着。

第三步:把参与签名的原文打出来比对

不要反复重算签名,先把原文和字段长度落日志:

log.Info("verify raw", "raw", rawForSign,
    "ts", req.Timestamp,
    "ts_len", len(strconv.FormatInt(req.Timestamp, 10)))

日志出来 ts_len=13,是毫秒。而代码里是这么拼的:

// 错:先解析再格式化,原文就变了
ts := time.Unix(req.Timestamp/1000, 0).Format("2006-01-02 15:04:05")
rawForSign := fmt.Sprintf("...&timestamp=%s", ts)

对方发来 13 位毫秒,我方 /1000 之后格式化成秒级字符串再拼回原文,差 3 位,签名必然不匹配;时间窗校验反而是对的,所以日志里只看到签名无效,看不到过期。正确做法一句话:签名原文只用对方发来的原始字符串,任何字段都不解析、不重新格式化。

修复就三件事:时间戳单位改成显式配置,并对 10 位和 13 位做归一化;单位不匹配时打 WARN ,不要静默失败;验签失败日志按 timestamp 、sign 、nonce 分步骤输出,带脱敏样本。

为什么偶发

灰度期间一半实例新配置、一半旧配置。落到旧实例:前缀被吃,直接 404 ;落到另一台旧实例:前缀对、但时间戳被重新格式化,验签失败。两个问题叠加,观感就是十笔错一笔。偶发不是网络抖动,是部分机器配置不一致的指纹。

怎么避免

这类问题最难的地方不在修,而在你一开始就怀疑错了方向。接入文档里把时间戳单位写死( 13 位毫秒 / 10 位秒),验签日志常打 ts_len,签名原文只取原始字符串,灰度时挂一条验签成功率指标,低于 99% 自动回滚。

261 次点击
所在节点    程序员
0 条回复

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1251771

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX