支付回调验签连续失败三天,最后发现是网关的时间戳单位问题

一句话:三方支付回调经过自研网关做签名校验,灰度后验签失败率飙升;查了三天,根因不是密钥、不是拼接规则,而是网关的时间戳窗口默认按「秒」解释,回调方给的却是毫秒时间戳。

背景

  • 架构:网关(Spring Cloud Gateway)+ 支付服务 + 回调方
  • 回调链路:三方 → 网关(签名校验、时间戳窗口、nonce 防重放)→ 支付服务
  • 改造内容:把验签从支付服务前移到网关,做统一入口

现象

  • 灰度 5% 后,回调 401 从 0 飙升到 30% 以上,高峰期接近全失败
  • 错误统一是「签名无效」,日志只有一行 sign invalid
  • 本地用同一套密钥和参数构造请求,能验过

第一轮排查:逐条排除

  1. 密钥:三方后台、网关配置、支付服务三处比对,一致,排除
  2. 拼接规则:按文档排序、空值处理、URL 编码,和抓包原文逐字节比对,一致
  3. 服务器时钟:NTP 同步正常,偏差 200ms 内,排除
  4. nonce:Redis 写入读回正常,排除
  5. 签名算法:SHA256WithRSA,换成已知正确样本交叉验证,能过

每条都对,但生产就是失败。

卡点:本地复现不出来

本地构造的请求能过;把生产抓包的原文复制到本地脚本里跑,也能过。这说明问题不在「签名内容」,而在「校验发生的环境」。

于是做了两件事:

  • 在验签失败分支里把失败步骤拆开打点:timestamp / sign / nonce 三段分别判定
  • 灰度 1% 并保存失败样本(脱敏后只留字段结构)

根因

失败样本里 100% 卡在时间戳窗口:

  • 网关的时间戳单位配置默认 seconds
  • 回调方 SDK 发的是 13 位毫秒时间戳
  • 窗口校验把毫秒当秒,差值瞬间变成 45000+,全部判定「过期」

三个「为什么」也就通了:

  • 为什么本地能过:本地脚本用的是 10 位秒级时间戳,和生产不一致
  • 为什么偶发:高峰期走的是另一条回调通道,时间戳单位不同
  • 为什么日志没用:拆点之前只打「验签失败」,看不出卡在哪一段

修复

  1. 时间戳单位改成显式配置,并对 10 位 / 13 位做自动归一化
  2. 单位不匹配时打 WARN,不再静默失败
  3. 验签失败日志分步骤输出,带脱敏样本
  4. nonce 写入失败改为可观测降级,不再无声放行或拒绝
  5. 灰度开关 + 验签成功率指标,低于 99% 自动回滚

复盘:三条能迁移到别处

  1. 协议里的每个数字都要显式声明单位。时间戳、超时、金额精度、距离,文档没写,代码就有两种解释,两种解释就有一个是错的。
  2. 「签名无效」是垃圾日志。验签是复合流程,失败必须分步骤,否则排查成本是十倍。
  3. 本地能过不代表对。本地构造请求用的是「你以为的格式」,不是真实生产格式。灰度前先抓真实样本。

最后

这类问题有个共同特征:不是某个点写错了,而是两个模块的「默认值」不一致。

所以现在我做支付、网关、回调这类改造,第一步不是写代码,是先拉一张「字段与单位约定表」,各方确认后再动手。

遇到类似的问题可以找我聊,我的 GitHub 在 github.com/luobobo896。

相关推荐
BBmmo1 小时前
Java InputStream.read() 为什么返回 int?
java
Nuanyt1 小时前
LeetCode Hot100 刷题笔记 · 普通数组专题
java·笔记·算法·leetcode
cfm_29143 小时前
Leaf-segment号段模式
java
Wang's Blog3 小时前
Java 项目实战: 交友项目 - 发布圈子动态:数据库表和接口文档介绍
java·数据库·交友
paopaokaka_luck3 小时前
智慧电玩店管理系统(空闲机台随机分配、游玩时长概率模拟、积分自动结算、积分兑换闭环、 ECharts经营分析、 腾讯地图选址)
java·人工智能·mysql·echarts·推荐算法
LMT流氓兔3 小时前
第五单元——第9课:防止同一次操作重复执行
java·服务器·数据库·算法·ai·langchain
OxYGC3 小时前
Spring 面经系列 · 第 6 篇:Spring MVC 请求全流程、父子容器与零配置原理
java·spring·mvc
code2cat3 小时前
Java高级篇之JDBC:连接数据库,完成查询、更新与事务
java·开发语言·数据库
bksczm3 小时前
Redis数据结构之string
java·redis·mybatis