ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PHP微信支付v3完整实例:签名、回调验签与退款落地指南

PHP微信支付v3完整实例:签名、回调验签与退款落地指南 简介这份资源是面向PHP开发者的微信支付V3完整接入实例适合需要为商城、线上业务集成微信支付的中初级后端人员参考。包内共16个文件以asp与php脚本为主辅以txt说明、pem证书、js脚本、gif图片及mdb数据文件覆盖统一下单、前端调起支付、异步回调通知、订单查询确认等核心流程并涉及API签名机制、私钥与公钥证书管理、沙箱环境测试、异常处理与退款接口等关键环节。代码示例与配置文件可直接对照调试帮助开发者理解V3版本在安全策略上的变化快速搭建可运行的支付链路。目前已有4630人学习下载适合作为接入微信支付V3时的实践参考与排错对照。1. PHP 微信支付 v3 完整实例从签名翻车到回调验签一套能跑通的落地路径很多 PHP 开发者第一次接微信支付 v3 时都会经历同一个夜晚本地用 phpstudy 或宝塔把环境搭好代码照着文档写完结果请求一发出就返回401 Unauthorized或者签名错误。更玄学的是同样的代码在别人的机器上能跑在你这里就是不行。问题往往不在业务逻辑而在 v3 的整套认证体系——它和 v2 的 MD5 签名完全是两套东西。微信支付 v3 用 SHA256-RSA 做请求签名用 AES-256-GCM 做回调解密用平台证书做应答验签任何一个环节的证书、序列号、私钥格式对不上都会直接翻车。这篇笔记面向正在用 PHP 接微信支付 v3 的开发者不管你是用原生 PHP、ThinkPHP 还是 Laravel核心链路是一样的。我会把签名、下单、回调、退款、对账这几块拆开讲给出能直接抄的代码和参数说明也会把那些文档里没写、只有踩过才知道的坑摆出来。读完你应该能独立跑通一套完整的支付流程并且知道每一步失败时该看哪里。2. 微信支付 v3 的认证体系为什么你的签名总是对不上2.1 请求签名到底签了什么微信支付 v3 的请求签名不是把参数拼起来做 MD5而是构造一个特定格式的签名串再用商户私钥做 SHA256-RSA 签名。签名串的格式是五行每行以换行符结尾HTTP请求方法\n URL路径\n 请求时间戳\n 请求随机串\n 请求报文主体\n这里有几个容易出错的点。第一URL 路径要带 query string比如/v3/pay/transactions/native?mchid123不能只写/v3/pay/transactions/native。第二请求时间戳是秒级不是毫秒。第三请求报文主体如果是 GET 请求就是空字符串但那一行换行符不能省。第四签名用的是商户 API 私钥不是平台证书私钥这两个东西很多人搞混。签名完成后放到Authorization头里格式是WECHATPAY2-SHA256-RSA2048 mchid商户号,nonce_str随机串,signature签名值,timestamp时间戳,serial_no证书序列号注意serial_no是商户 API 证书的序列号不是平台证书的序列号。这个序列号在微信支付商户平台下载证书时可以拿到也可以用 openssl 命令从证书文件里读出来。2.2 用 PHP 实现签名与请求头构造下面这段代码是签名和构造 Authorization 头的核心逻辑我一般会封装成一个WechatPayV3类的基础方法?php class WechatPayV3 { private $mchId; private $serialNo; private $privateKey; private $apiv3Key; public function __construct($mchId, $serialNo, $privateKeyPath, $apiv3Key) { $this-mchId $mchId; $this-serialNo $serialNo; $this-privateKey openssl_pkey_get_private(file_get_contents($privateKeyPath)); $this-apiv3Key $apiv3Key; } // 生成请求签名 public function buildAuthorization($method, $urlPath, $body ) { $timestamp time(); $nonceStr bin2hex(random_bytes(16)); // 构造签名串五行每行以 \n 结尾 $message $method . \n . $urlPath . \n . $timestamp . \n . $nonceStr . \n . $body . \n; // SHA256-RSA 签名 openssl_sign($message, $signature, $this-privateKey, OPENSSL_ALGO_SHA256); $signature base64_encode($signature); // 拼接 Authorization 头 $authorization sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%s,serial_no%s, $this-mchId, $nonceStr, $signature, $timestamp, $this-serialNo ); return $authorization; } }这段代码里$urlPath必须包含 query string$body是原始 JSON 字符串不能是数组再 json_encode 一次否则签名和实际发送的 body 不一致。openssl_sign的第四个参数必须是OPENSSL_ALGO_SHA256默认是 SHA1用错了签名一定失败。random_bytes(16)生成 16 字节随机串转成 hex 是 32 位字符符合微信要求。2.3 平台证书与应答验签请求签名只是第一步微信支付返回的应答也需要验签否则你无法确认响应真的来自微信。验签用的是微信支付平台证书的公钥不是商户私钥。平台证书需要通过接口下载下载回来的证书要用 APIv3 密钥解密。常见做法是先调用GET /v3/certificates获取平台证书列表用 APIv3 密钥对返回的encrypt_certificate做 AES-256-GCM 解密得到证书 PEM然后缓存起来。验签时从响应头Wechatpay-Serial找到对应的平台证书序列号用对应公钥验签。验签的签名串格式和请求签名类似但用的是响应内容应答时间戳\n 应答随机串\n 应答报文主体\n这里有个坑应答报文主体必须是原始字符串不能先 json_decode 再 json_encode因为 JSON 编码的顺序和空格可能变化导致验签失败。我一般会在 HTTP 客户端拿到原始响应后先验签再解析 JSON。提示平台证书有有效期建议缓存但不要永久缓存定期刷新。如果验签突然失败先检查平台证书是否过期。3. 用 PHP 跑通 Native 下单从构造请求到拿到二维码链接3.1 Native 下单的接口参数与请求体Native 支付是扫码支付最常用。接口是POST /v3/pay/transactions/native请求体是 JSON核心参数如下参数名类型必填说明appidstring是公众号或小程序 appidmchidstring是商户号descriptionstring是商品描述out_trade_nostring是商户订单号6-32 位notify_urlstring是回调地址必须 HTTPSamount.totalint是金额单位分amount.currencystring否默认 CNY请求体构造时注意amount是嵌套对象total是整数不要传字符串。out_trade_no同一商户号下不能重复重复会返回ORDERPAID或OUT_TRADE_NO_USED。3.2 完整下单代码与 curl 请求下面是一个完整的 Native 下单方法包含签名、发送请求、验签和解析public function nativePay($outTradeNo, $description, $totalFee, $notifyUrl) { $urlPath /v3/pay/transactions/native; $body json_encode([ appid $this-appId, mchid $this-mchId, description $description, out_trade_no $outTradeNo, notify_url $notifyUrl, amount [ total $totalFee, currency CNY ] ], JSON_UNESCAPED_UNICODE); $authorization $this-buildAuthorization(POST, $urlPath, $body); $ch curl_init(); curl_setopt_array($ch, [ CURLOPT_URL https://api.mch.weixin.qq.com . $urlPath, CURLOPT_POST true, CURLOPT_POSTFIELDS $body, CURLOPT_RETURNTRANSFER true, CURLOPT_HTTPHEADER [ Content-Type: application/json, Accept: application/json, Authorization: . $authorization, User-Agent: PHP-WechatPay/1.0 ], CURLOPT_TIMEOUT 10, CURLOPT_SSL_VERIFYPEER true ]); $response curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if ($httpCode ! 200) { throw new Exception(下单失败: . $response); } $result json_decode($response, true); return $result[code_url]; // 二维码链接 }json_encode时用JSON_UNESCAPED_UNICODE保证中文描述不被转义虽然微信不强制但可读性好。CURLOPT_SSL_VERIFYPEER设为 true 是安全做法如果本地环境证书有问题可以临时设为 false 调试但生产必须 true。code_url拿到后前端可以用它生成二维码用户扫码后微信会回调你的notify_url。3.3 下单失败的排查顺序下单失败时不要急着改代码按这个顺序查看 HTTP 状态码。401 是签名问题400 是参数问题403 是权限或证书问题。看响应体里的code和message。比如PARAM_ERROR会告诉你哪个字段有问题。检查Authorization头里的serial_no是否和商户平台证书序列号一致。检查私钥文件是否匹配用openssl rsa -in apiclient_key.pem -check验证。检查系统时间是否准确时间偏差超过 5 分钟会签名失败。注意微信支付 v3 的接口域名是api.mch.weixin.qq.com不要用 v2 的域名。回调地址必须是 HTTPS且不能带端口号。4. 回调验签与解密别让假通知骗了你的订单4.1 回调通知的验签流程微信支付回调是 POST 请求body 是加密的 JSON。处理流程是先验签再解密再处理业务。验签用的是平台证书签名串格式和应答验签一样应答时间戳\n 应答随机串\n 应答报文主体\n这三个值分别从请求头Wechatpay-Timestamp、Wechatpay-Nonce、Wechatpay-Signature获取报文主体是原始 body。验签通过后body 里的resource字段是加密的需要用 APIv3 密钥做 AES-256-GCM 解密。4.2 AES-256-GCM 解密代码public function decryptResource($associatedData, $nonce, $ciphertext) { $ciphertext base64_decode($ciphertext); $tag substr($ciphertext, -16); // 最后 16 字节是 tag $ciphertext substr($ciphertext, 0, -16); $plaintext openssl_decrypt( $ciphertext, aes-256-gcm, $this-apiv3Key, OPENSSL_RAW_DATA, $nonce, $tag, $associatedData ); if ($plaintext false) { throw new Exception(解密失败); } return json_decode($plaintext, true); }$associatedData是resource里的associated_data$nonce是resource.nonce$ciphertext是resource.ciphertext。aes-256-gcm模式需要 tag微信把 tag 拼在密文最后 16 字节所以要拆开。$this-apiv3Key是 APIv3 密钥32 位字符串不是 API 密钥。4.3 回调处理的完整示例与幂等性public function handleNotify() { $body file_get_contents(php://input); $timestamp $_SERVER[HTTP_WECHATPAY_TIMESTAMP]; $nonce $_SERVER[HTTP_WECHATPAY_NONCE]; $signature $_SERVER[HTTP_WECHATPAY_SIGNATURE]; // 验签 $message $timestamp . \n . $nonce . \n . $body . \n; $platformCert $this-getPlatformCert($_SERVER[HTTP_WECHATPAY_SERIAL]); $verify openssl_verify($message, base64_decode($signature), $platformCert, OPENSSL_ALGO_SHA256); if ($verify ! 1) { $this-reply(FAIL, 验签失败); return; } $data json_decode($body, true); $resource $this-decryptResource( $data[resource][associated_data], $data[resource][nonce], $data[resource][ciphertext] ); // 处理业务注意幂等 $outTradeNo $resource[out_trade_no]; $transactionId $resource[transaction_id]; // 检查订单是否已处理避免重复 if ($this-isOrderProcessed($outTradeNo)) { $this-reply(SUCCESS, OK); return; } // 更新订单状态 $this-updateOrder($outTradeNo, $transactionId); $this-reply(SUCCESS, OK); } private function reply($code, $message) { header(Content-Type: application/json); echo json_encode([code $code, message $message]); exit; }回调处理必须幂等因为微信会重复通知。用out_trade_no做唯一索引或者用 Redis 锁。回复微信的格式是{code:SUCCESS,message:OK}如果处理失败回复 FAIL微信会重试。提示回调地址不能有重定向必须直接返回 200。如果用了框架注意中间件不要拦截。5. 退款、查询与对账把资金链路补完整5.1 退款接口的签名与参数退款接口是POST /v3/refund/domestic/refunds参数包括out_trade_no、out_refund_no、amount.refund、amount.total、amount.currency。签名方式和下单一样注意out_refund_no不能重复。public function refund($outTradeNo, $outRefundNo, $refundFee, $totalFee) { $urlPath /v3/refund/domestic/refunds; $body json_encode([ out_trade_no $outTradeNo, out_refund_no $outRefundNo, amount [ refund $refundFee, total $totalFee, currency CNY ] ], JSON_UNESCAPED_UNICODE); $authorization $this-buildAuthorization(POST, $urlPath, $body); // 发送请求同下单 }退款是异步的提交后不一定立即成功需要查退款状态或等退款回调。退款回调的验签和解密流程和支付回调一样。5.2 查询订单与对账文件下载查询订单用GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchid{mchid}注意 URL 路径带 query string签名时要包含。对账文件下载用GET /v3/bill/tradebill?bill_date2024-01-01bill_typeALL返回下载链接再下载文件。对账文件是 CSV注意编码和分隔符。5.3 常见状态码与处理策略状态码含义处理策略200成功正常处理400参数错误检查请求体字段401签名错误检查私钥、序列号、签名串403权限不足检查商户号、appid 绑定404资源不存在检查订单号429频率限制降低请求频率500微信内部错误重试6. 避坑与排查那些文档没写但一定会遇到的问题6.1 私钥格式不对导致签名失败现象openssl_sign返回 false或者签名后请求返回 401。原因下载的私钥文件可能是 PKCS#8 格式而 PHP 的openssl_pkey_get_private需要 PKCS#1 或正确的 PEM。解决用openssl rsa -in apiclient_key.pem -out apiclient_key_pkcs1.pem转换或者直接用原始文件但确保没有多余空格和换行。6.2 回调验签失败但解密成功现象验签返回 0但解密能拿到数据。原因验签用的平台证书不对或者签名串拼接时 body 被修改过。解决确保用原始 body不要用json_decode后再json_encode。检查Wechatpay-Serial对应的平台证书是否已下载。6.3 金额单位搞错导致下单失败现象返回PARAM_ERROR提示金额错误。原因微信支付 v3 金额单位是分不是元。解决前端传元后端乘以 100 转成分。注意浮点数精度用intval($amount * 100)或bcmul。6.4 回调地址被框架路由拦截现象微信回调一直重试但你的日志没有记录。原因框架的 CSRF 中间件或路由规则拦截了 POST 请求。解决把回调地址加入白名单或者用独立入口文件处理回调。6.5 平台证书过期导致验签失败现象之前正常的验签突然失败。原因平台证书有有效期过期后需要重新下载。解决定期刷新平台证书建议每天一次缓存到 Redis 或文件。7. 进阶技巧用单例和缓存把支付类写得更稳支付类不需要每次请求都重新初始化尤其是私钥和平台证书。我一般会把WechatPayV3做成单例平台证书缓存到 Redis设置 12 小时过期。这样既减少文件 IO也避免频繁请求证书接口。另一个技巧是签名串的调试。当签名失败时把签名串打印出来和微信官方文档的示例对比。注意换行符和空格很多时候就是多了一个空格。// 单例示例 class WechatPayV3Factory { private static $instance; public static function getInstance() { if (self::$instance null) { self::$instance new WechatPayV3( config(wechat.mch_id), config(wechat.serial_no), config(wechat.private_key_path), config(wechat.apiv3_key) ); } return self::$instance; } }平台证书缓存可以用 Redispublic function getPlatformCert($serialNo) { $cacheKey wechat_platform_cert_ . $serialNo; $cert redis()-get($cacheKey); if ($cert) { return $cert; } // 请求证书接口解密后缓存 $cert $this-downloadPlatformCert($serialNo); redis()-setex($cacheKey, 43200, $cert); return $cert; }最后说一个血泪经验生产环境一定要把微信支付的请求和回调日志完整记录下来包括请求头、请求体、响应体。出问题时这些日志就是后悔药。我习惯用file_put_contents按天写文件简单直接。希望帮到你。本文还有配套的精品资源点击获取
返回列表