Laravel支付回调需排除CSRF验证、区分数据格式用getContent()或all()、严格按平台规则验签、返回指定响应体,再通过订单号与流水号双校验保障幂等性。

支付回调不是「接收到请求就完事」,Laravel 中真正难的是:验签失败、重复通知、状态不一致、并发写入冲突——这些才是线上支付翻车的主因。
为什么 Route::post('/pay/notify') 经常 405 或 419?
Laravel 默认 CSRF 保护会拦截所有 POST 请求,而微信/支付宝回调根本不会带 _token。别急着关全局中间件,更安全的做法是把回调路由排除在 VerifyCsrfToken 外。
- 在
app/Http/Middleware/VerifyCsrfToken.php的$except数组里加:'/pay/notify*'(注意通配符) - 确保路由定义在
routes/api.php而非web.php,避免被 session 中间件干扰 - 支付宝回调可能用
GET(同步通知),但异步通知一定是POST;微信只用POST,且 body 是原始 XML 或 JSON,不能依赖request()->all()
request()->getContent() 和 request()->all() 到底该用哪个?
微信回调发的是 raw XML,支付宝 V3 是 JSON,V2 是 form 表单——数据格式决定你第一行代码怎么写。
- 微信公众号/小程序支付回调:
simplexml_load_string(request()->getContent()),再转数组,别用input() - 支付宝 V3 异步通知:
json_decode(request()->getContent(), true),request()->all()会为空 - 支付宝 V2(老版本):
request()->all()可用,但必须手动校验sign和sign_type字段 - 无论哪种,都建议先记录原始
request()->getContent()到日志,方便出问题时比对平台原始推送
验签失败的三个高频原因
不是密钥填错了才验签失败,更多是细节错位。
- 微信:
sign字段要从数组中剔除后再排序拼接,且键名全部小写、无空格、末尾不加&—— 少一个字符就失败 - 支付宝:V3 用
OpenSSL验证 JWT 签名,V2 用openssl_verify,但公钥格式必须是 PEM(以-----BEGIN PUBLIC KEY-----开头),PKCS8 不行 - 时间戳/随机串参与签名?微信要求
nonce_str,支付宝 V2 不需要,V3 完全不用——混用文档会导致永远验不过 - 别在验签前用
trim()或urldecode()处理原始参数,平台推送的内容就是原样,改了就对不上
处理成功后返回什么,微信/支付宝才不重发?
不是 return JSON 就算“成功”,它们只认特定响应体和 HTTP 状态码。
- 微信:必须返回
<xml><return_code></return_code><return_msg></return_msg></xml>,HTTP 状态码 200,Content-Type 为text/xml,多一个空格或换行都可能触发重试 - 支付宝 V3:返回 HTTP 200 + 空响应体即可,但必须是
text/plain或application/json,不能是 HTML - 支付宝 V2:返回字符串
success(全小写,无空格、无换行),别的都不认 - 千万避免在返回前执行耗时操作(比如发邮件、调外部 API),先回包再队列处理业务逻辑
最麻烦的从来不是「怎么接到回调」,而是「怎么确定这笔钱真的进了账、且只处理一次」——幂等性得靠订单号 + 支付平台流水号双校验,数据库加唯一索引只是底线,不是保险丝。










