支付失败,八成是签名没对上
昨晚22:15,一个电商商户在群里喊“支付页面直接报‘支付失败’,钱扣了订单却没成”。我远程拉日志一看,接口返回 code:-1、msg 明确写的是“验签失败”。不到两分钟就确定,这次易支付支付失败不是什么大故障,就是签名校验卡住了。
平台回调通知给到商户服务端后,要求对关键参数做一次RSA-SHA256签名,再回传验签字段。验签不通过,易支付就直接中断流程,前端就展示通用失败提示。这类问题我处理过不下几十次,签名原始字符串的组装或者密钥本身的格式最容易出幺蛾子。
一步步排查:从日志到原始字符串
我先调取Nginx层的请求记录,找到这个商户的订单号。请求在易支付网关侧成功受理,支付通道返回成功,但异步通知发给商户接收地址后,收到的响应体是 {“code”:-1}。这瞬间排除了通道抖动的原因——不像之前我在一次并发压测里看到的数据库连接池耗尽那种随机失败,那次复盘写成了支付通道稳定性测试方法:一次并发超时复盘。
既然错在通知处理阶段,我让商户技术把收到通知时打印的签名原文发过来。他们用的PHP,签名串是“amount=9.90&out_trade_no=202608042315XXXX&status=success&trade_no=ZF20260804……”,拼接看起来没错。然后我让他在自己服务器上用同一份私钥对这段原文重新计算签名,和收到的sign字段对比。结果不一致。
我马上意识到可能是密钥载入有问题。让商户执行 echo -n $原始字符串 | openssl dgst -sha256 -sign private_key.pem | base64 ,生成的签名和易支付传过来的也不一样。奇怪的是,同样的办法在我本地用测试密钥能对上。那就不是算法逻辑的毛病。
接下来检查密钥文件本身。商户是从运营后台密钥管理页面复制的RSA私钥,粘贴到服务器配置文件的 merchant_private_key 字段。我让他跑 wc -c private_key.pem ,输出比原本密钥长度多了2个字节。用xxd查看文件开头,赫然出现了 0x0D 0x0A ,典型的Windows换行符。
根因:肉眼看不见的换行符
复制密钥时,是从QQ聊天窗口直接选中文本粘贴的,Windows剪贴板在文本末尾自动带上了回车。这一对不可见字符混进私钥字符串,OpenSSL解析时没有剔除,导致实际参与签名的私钥内容被污染。整个签名计算因此彻底变样,而易支付那边的验签当然不通过。
分情况解决:三种常见支付失败情形
不是每次支付失败都卡在签名。我习惯把这类排查归成三个方向,用户只需按实际情况操作。
签名类失败(验签失败、sign error)
首先用命令重现签名:echo -n “待签名字符串” | openssl dgst -sha256 -sign 真实私钥.pem | base64,与请求中sign对比。如果对不上,检查私钥文件是否带BOM头(file命令可查看)、是否包含换行或空格。终极办法是用代码加载后trim()一下,或者通过PEM格式标准接口读取,而不是手工拼接字符串。
回调地址不可达(通知失败、HTTP 504)
在商户服务器上用curl从支付网关IP直接访问回调URL,看是否握手成功。我曾碰到过因证书过期导致TLS握手失败,最终支付页面卡住,复盘过程类似这篇网站HTTPS证书过期致支付中断的复盘。支付行业通行做法是回调地址必须支持TLS1.2以上,且CA证书在有效期内。如果有WAF或CDN,记得把易支付的异步通知IP加白。
接口超时(请求无响应、网络报错)
支付API默认连接超时3000ms、读取超时5000ms较合理。偶尔因DNS解析慢或链路丢包导致失败,可以在代码层增加1次重试,间隔800ms。保证服务器到易支付网关的出向443端口通畅。用curl -w “time_total: %{time_total}\n” -o /dev/null -s 易支付API地址可测实际耗时。
事后复盘:下次我会先核对密钥长度
整个过程从报障到定位根因,花了将近30分钟。如果一开始就想到让商户执行 wc -c 检查密钥长度,10分钟内就能收工。另外,投产前强制在测试环境跑一遍完整支付验证,包括异步通知验签,可以避免这种低级失误漏到生产环境。每个商户接入时,我都会让他们用命令行工具先验一遍完整签名流程。
预防支付失败的长效配置规范
这类因隐藏字符导致的配置错误,在接入阶段完全可以杜绝。
- 私钥、密钥统一用PEM文件形式分发,通过SFTP上传,不在即时通讯工具里直接传输。
- 上线前在沙箱环境至少完成3笔真实金额支付,其中一笔故意模拟验签失败,观察业务侧的降级提示。
- 使用配置中心推送密钥时,对值做严格的格式校验(正则匹配“-----BEGIN RSA PRIVATE KEY-----”开头,不含空白符)。
支付系统里,签名机制是信任链的根基。一个换行符就能让整条链路失效,而排查它的时间远比重启服务要长得多。把密钥当作二进制数据来管理,是最稳妥的方式。