支付宝异步通知验签失败主因是框架自动解析/转码请求体;须用file_get_contents('php://input')读原始数据并urldecode,再交由SDK验签,且订单更新需幂等控制。

支付宝异步通知验签失败的常见原因
验签失败不是支付宝没发,而是你没接住——90% 的问题出在请求体被框架自动解析或转码。ThinkPHP 默认会对 $_POST 做 URL 解码和类型转换,但支付宝签名是基于原始未解码的 query string 计算的。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 必须用
file_get_contents('php://input')读取原始 POST 数据,不能依赖input()或$this->request->post() - 原始数据是形如
out_trade_no=xxx&trade_status=TRADE_SUCCESS&sign=xxx的字符串,注意它不含 JSON 结构,也不是 multipart - 验签前要先用
urldecode()对整个字符串做一次解码(支付宝文档里写“不编码”,但实际发过来的字段值可能含 %2B 这类编码) - 验签时排除
sign和sign_type字段,其余字段按字典序排序后拼接
ThinkPHP 中调用支付宝 SDK 验签的正确姿势
别自己拼字符串验签,容易漏字段顺序或编码差异。直接用官方 PHP SDK 的 AlipayTradeService::verifyNotify(),但要注意它默认读 $_POST,得手动喂原始数据。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 初始化
AlipayTradeService后,调用$alipay->setRequestParameters($params),其中$params是从file_get_contents('php://input')解析出的关联数组(用parse_str()) - 确保
$alipay->alipay_config['rsa_public_key']是支付宝公钥(不是你的私钥),且格式为 PEM,开头是-----BEGIN PUBLIC KEY----- - SDK 内部会自动过滤
sign/sign_type、排序、拼串、验签;返回true才代表验签通过 - 如果返回
false,不要立刻拒单,先记录原始php://input内容和$_SERVER['HTTP_USER_AGENT'],方便排查是否被代理重写
订单状态更新必须加幂等控制
支付宝异步通知可能重复推送(网络超时重试、你返回非 200 状态都会触发),如果每次来都执行发货逻辑,就真发两次货了。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 用数据库唯一约束兜底:在订单表加
trade_no字段并建唯一索引,插入支付成功记录时用INSERT IGNORE或ON DUPLICATE KEY UPDATE - 业务层加状态机判断:只允许从
unpaid→paid,如果已是paid就跳过后续操作 - 不要依赖支付宝的
trade_status做最终判断,它可能滞后(比如先发TRADE_SUCCESS,后发TRADE_CLOSED),应以你自己落库的状态为准 - 记录每次通知的
notify_id,同一 ID 只处理一次(缓存或 DB 记录均可,有效期建议设为 24 小时)
回调接口必须返回明确响应且不抛异常
支付宝收到非 200 响应或超时,会持续重推最多 25 次,间隔从 1m 到 2h 不等。而 ThinkPHP 默认异常会输出 HTML 错误页,支付宝识别为失败。
实操建议:
立即学习“PHP免费学习笔记(深入)”;
- 整个回调逻辑包在
try/catch里,任何异常都捕获,记录日志,然后返回纯文本success - 不要用
return json(['msg'=>'ok']),支付宝只认字符串success,多一个空格都不行 - 避免在回调里调外部 HTTP 接口(比如发短信、调物流),这些操作异步化,否则容易超时
- 确认服务器时间与支付宝服务器时间误差小于 15 分钟,否则验签会因时间戳过期失败(
timestamp字段参与验签)
验签和幂等不是两个独立环节,而是咬合在一起的动作:验签不过就绝不能碰数据库,验签过了也得先查状态再决定是否更新。最容易被忽略的是原始请求体的读取时机——它只能在入口第一行获取,后面再读就是空的。


















