资讯动态

PHP微信支付V3实战:从签名验签到回调解密完整案例

发布时间:2026/10/17 2:52:43 来源:尧图企业网站定制
简介这是一份面向PHP开发者的微信支付V3接口接入案例适合具备PHP面向对象基础、需要为项目快速集成微信支付能力的开发者。资源围绕V3版本核心支付功能展开同时兼容V2部分能力覆盖订单创建、支付验证、退款申请、支付结果异步通知、支付状态查询等关键环节并对OAuth2.0授权获取OpenID、RSA2048证书加解密、安全规范与错误日志处理等要点做了封装降低接入复杂度。压缩包共15个文件以14个PHP源码为主辅以1个Markdown说明文档整体约42KB涵盖配置、核心支付类、JSAPI与Native扫码支付、订单与退款操作、回调通知及示例代码等模块目录结构清晰便于按需查阅。目前已有2012人学习下载读者可借此理解完整支付流程与各接口交互逻辑掌握证书管理、回调验证与退款查询的落地写法并参考示例快速调试自己的支付模块。1. 从一次支付回调超时说起这个 PHP 微信支付 V3 案例到底能干什么去年帮一个做知识付费的朋友排查线上问题用户付完款课程没开通查日志发现回调接口返回了 500微信那边重试了三次全挂。翻代码一看用的是 V2 版本的 XML 回调解析签名校验逻辑还写错了。后来换成 V3 版本用平台证书验签、AES-GCM 解密回调数据问题才彻底解决。这个 PHP 微信支付 V3 案例就是干这个的——它把 V3 版本的下单、回调、退款、查单这些核心链路都跑通了用的是 PHP 语言适合正在做微信支付接入或者从 V2 迁移到 V3 的开发者。不管你是接 Native 扫码支付还是 JSAPI 公众号支付这个案例里的代码结构和签名逻辑都能直接抄作业。2. 技术选型与目录结构为什么用 PHP 原生 cURL 而不是框架2.1 技术栈拆解这个案例没有依赖 Laravel、ThinkPHP 这类重型框架用的是 PHP 原生语法加 cURL 扩展。为什么这么选因为微信支付 V3 的核心难点不在框架而在签名生成、证书管理和回调验签。用原生写反而能把每个环节看清楚你往任何框架里移植都方便。具体依赖就几个PHP 7.4 以上需要开启openssl、curl、json扩展商户 API 证书apiclient_cert.pem、apiclient_key.pem微信支付平台证书用于回调验签V3 必须一个能接收 HTTPS 回调的公网域名常见做法是把商户私钥和平台证书放在项目外的安全目录通过环境变量或者配置文件读取不要直接提交到代码仓库。我一般会在项目根目录建一个cert/文件夹加进.gitignore部署时手动上传。2.2 目录结构说明案例的目录结构很扁平没有过度设计wechat-pay-v3/ ├── config/ │ └── config.php # 商户号、appid、证书路径等配置 ├── cert/ │ ├── apiclient_cert.pem # 商户证书 │ ├── apiclient_key.pem # 商户私钥 │ └── platform_cert.pem # 平台证书用于验签 ├── src/ │ ├── WxPayV3.php # 核心类签名、请求、验签 │ └── WxPayException.php # 异常处理 ├── demo/ │ ├── native_pay.php # Native 扫码支付示例 │ ├── jsapi_pay.php # JSAPI 支付示例 │ ├── notify.php # 回调处理示例 │ └── refund.php # 退款示例 └── README.mdsrc/WxPayV3.php是整个项目的核心所有请求都经过它。demo/目录下的文件是给你直接复制到项目里改的每个文件对应一个支付场景。这种结构的好处是你不需要理解整个框架只要看懂一个核心类加几个示例文件就能上手。2.3 核心类初始化逻辑先看WxPayV3.php的构造函数和基础配置?php class WxPayV3 { private $mchId; // 商户号 private $appId; // 公众号或小程序 appid private $serialNo; // 商户证书序列号 private $privateKey; // 商户私钥内容 private $platformCert; // 平台证书内容 private $apiV3Key; // APIv3 密钥 public function __construct($config) { $this-mchId $config[mch_id]; $this-appId $config[app_id]; $this-apiV3Key $config[api_v3_key]; // 读取商户私钥 $this-privateKey file_get_contents($config[private_key_path]); // 读取平台证书 $this-platformCert file_get_contents($config[platform_cert_path]); // 获取证书序列号 $this-serialNo $this-getCertSerialNo($config[cert_path]); } // 从商户证书中提取序列号 private function getCertSerialNo($certPath) { $cert file_get_contents($certPath); $parsed openssl_x509_parse($cert); return $parsed[serialNumberHex]; } }这里有几个关键点。serialNo不是随便填的必须从商户证书里解析出来微信服务端会用这个序列号找到对应的公钥来验证你的签名。apiV3Key是你在商户平台设置的 32 位密钥用于解密回调数据里的resource字段。平台证书在 V3 里是必须的因为微信返回的数据和回调通知都需要用它来验签不像 V2 那样用 MD5 密钥就能糊弄过去。3. 核心模块拆解签名、请求、回调验签的完整链路3.1 签名生成V3 和 V2 最大的区别V3 的签名机制比 V2 复杂得多但安全性也高得多。V2 是用 MD5 拼参数V3 是用 SHA256-RSA 对特定格式的字符串签名。看代码// 生成请求签名 public function buildSignature($method, $url, $body, $timestamp, $nonce) { // 1. 构造待签名字符串 $message $method . \n . $url . \n . $timestamp . \n . $nonce . \n . $body . \n; // 2. 用商户私钥进行 SHA256-RSA 签名 openssl_sign($message, $signature, $this-privateKey, OPENSSL_ALGO_SHA256); // 3. Base64 编码 return base64_encode($signature); } // 生成 Authorization 头 public function buildAuthorization($method, $url, $body) { $timestamp time(); $nonce bin2hex(random_bytes(16)); $signature $this-buildSignature($method, $url, $body, $timestamp, $nonce); return sprintf( WECHATPAY2-SHA256-RSA2048 mchid%s,nonce_str%s,signature%s,timestamp%d,serial_no%s, $this-mchId, $nonce, $signature, $timestamp, $this-serialNo ); }待签名字符串的格式是固定的请求方法、URL、时间戳、随机串、请求体每行一个最后加一个换行符。这里最容易翻车的地方是 URL 必须带 query string比如/v3/pay/transactions/native后面如果跟了参数签名时也要带上。另外nonce用random_bytes生成不要用rand()或者mt_rand()微信对随机性有要求。3.2 发起请求cURL 封装与超时处理签名生成后用 cURL 把请求发出去public function request($method, $url, $body ) { $authorization $this-buildAuthorization($method, $url, $body); $ch curl_init(); curl_setopt_array($ch, [ CURLOPT_URL https://api.mch.weixin.qq.com . $url, CURLOPT_RETURNTRANSFER true, CURLOPT_TIMEOUT 10, // 超时 10 秒 CURLOPT_CONNECTTIMEOUT 5, // 连接超时 5 秒 CURLOPT_HTTPHEADER [ Authorization: . $authorization, Content-Type: application/json, Accept: application/json, User-Agent: wechat-pay-v3-php/1.0 ], CURLOPT_CUSTOMREQUEST $method, ]); if ($method POST || $method PUT) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); } $response curl_exec($ch); $httpCode curl_getinfo($ch, CURLINFO_HTTP_CODE); $error curl_error($ch); curl_close($ch); if ($error) { throw new WxPayException(cURL 错误: . $error); } return [code $httpCode, body json_decode($response, true)]; }超时设置很关键。微信支付接口偶尔会抽风如果不设超时PHP 进程会一直卡住在高并发场景下直接把服务器拖死。我一般设连接超时 5 秒、总超时 10 秒超过就抛异常让上层重试。另外注意CURLOPT_CUSTOMREQUEST要跟签名时的$method一致有人签名用 POST 请求用 GET微信直接返回 401。3.3 回调验签与解密V3 最绕的一环回调处理是 V3 里最容易出问题的地方。微信回调过来的是加密数据需要先用平台证书验签再用 APIv3 密钥解密// 验证回调签名 public function verifyNotify($headers, $body) { // 1. 从请求头获取签名信息 $timestamp $headers[Wechatpay-Timestamp]; $nonce $headers[Wechatpay-Nonce]; $signature $headers[Wechatpay-Signature]; $serial $headers[Wechatpay-Serial]; // 2. 构造待验签字符串 $message $timestamp . \n . $nonce . \n . $body . \n; // 3. 用平台证书公钥验签 $verify openssl_verify( $message, base64_decode($signature), $this-platformCert, OPENSSL_ALGO_SHA256 ); if ($verify ! 1) { throw new WxPayException(回调验签失败); } // 4. 解密 resource 数据 $data json_decode($body, true); $resource $data[resource]; $ciphertext base64_decode($resource[ciphertext]); $associatedData $resource[associated_data]; $nonceStr $resource[nonce]; $plaintext openssl_decrypt( $ciphertext, aes-256-gcm, $this-apiV3Key, OPENSSL_RAW_DATA, $nonceStr, $tag, $associatedData ); return json_decode($plaintext, true); }验签失败最常见的原因是平台证书不对。V3 的平台证书会定期更换如果你写死了证书文件过一段时间就会验签失败。常见做法是定期调用/v3/certificates接口下载最新平台证书或者用微信提供的证书管理器自动更新。解密时aes-256-gcm模式需要传$tag参数这个在openssl_decrypt里是引用传递别漏了。4. 跑起来从配置到第一笔支付成功的完整步骤4.1 环境准备与配置填写先把项目克隆到本地然后改config/config.php?php return [ mch_id 1900000001, // 商户号 app_id wx1234567890abcdef, // 公众号 appid api_v3_key your32charapiv3key, // APIv3 密钥 cert_path __DIR__ . /../cert/apiclient_cert.pem, private_key_path __DIR__ . /../cert/apiclient_key.pem, platform_cert_path __DIR__ . /../cert/platform_cert.pem, notify_url https://yourdomain.com/demo/notify.php ];api_v3_key是 32 位字符串在商户平台「账户中心-API安全」里设置。notify_url必须是 HTTPS而且不能带参数微信回调时会往这个地址 POST 数据。证书文件从商户平台下载后放到cert/目录注意文件权限设为 600别让其他用户读到。4.2 Native 扫码支付示例Native 支付是最容易测试的场景不需要公众号授权。看demo/native_pay.php?php require_once __DIR__ . /../src/WxPayV3.php; $config require __DIR__ . /../config/config.php; $wxPay new WxPayV3($config); $outTradeNo TEST . time(); // 商户订单号 $body [ appid $config[app_id], mchid $config[mch_id], description 测试商品, out_trade_no $outTradeNo, notify_url $config[notify_url], amount [ total 1, // 金额单位分 currency CNY ] ]; $result $wxPay-request(POST, /v3/pay/transactions/native, json_encode($body)); if ($result[code] 200) { // 返回 code_url生成二维码 echo 二维码链接: . $result[body][code_url]; } else { echo 下单失败: . $result[body][message]; }out_trade_no是商户系统内部的订单号必须唯一。amount.total单位是分1 就是 1 分钱测试时用这个金额最划算。返回的code_url是一个weixin://开头的链接用二维码生成库转成图片给用户扫。扫码后微信会回调notify_url你在回调里处理业务逻辑。4.3 回调处理与订单状态更新demo/notify.php是回调入口部署到公网后微信会往这里发通知?php require_once __DIR__ . /../src/WxPayV3.php; $config require __DIR__ . /../config/config.php; $wxPay new WxPayV3($config); // 获取请求头和请求体 $headers getallheaders(); $body file_get_contents(php://input); try { // 验签并解密 $data $wxPay-verifyNotify($headers, $body); // 处理业务逻辑 if ($data[trade_state] SUCCESS) { $outTradeNo $data[out_trade_no]; $transactionId $data[transaction_id]; // 更新订单状态示例用文件记录实际用数据库 file_put_contents( __DIR__ . /../orders.log, date(Y-m-d H:i:s) . 订单 {$outTradeNo} 支付成功微信单号 {$transactionId}\n, FILE_APPEND ); } // 必须返回 200否则微信会重试 http_response_code(200); echo json_encode([code SUCCESS, message 成功]); } catch (Exception $e) { // 验签失败返回 500微信会重试 http_response_code(500); echo json_encode([code FAIL, message $e-getMessage()]); }回调处理有两个铁律第一必须返回 200 告诉微信收到了否则微信会按 15s、15s、30s、3m、10m 的频率重试第二业务逻辑要幂等同一笔订单可能收到多次回调别重复加钱。我一般会在数据库里加一个transaction_id唯一索引插入失败就说明已经处理过了。5. 避坑与排查V3 支付接入中最容易翻车的 5 个点5.1 签名报错 401 Unauthorized现象请求返回 401错误信息是Signature verification failed。原因待签名字符串拼错了。最常见的是 URL 没带 query string或者body为空时多拼了一个换行符。另外serial_no必须是小写十六进制有人从证书里解析出来是大写微信不认。解决把待签名字符串打印出来逐字符对比特别注意\n的位置和数量。serial_no用strtolower()转一下。5.2 回调验签失败现象回调接口返回 500日志显示openssl_verify返回 0。原因平台证书过期或者用错了。V3 平台证书有效期只有一年而且微信会不定期更换。如果你从商户平台下载的是旧的平台证书验签必然失败。解决调用/v3/certificates接口获取最新平台证书或者用微信提供的Wechatpay-Serial头去匹配对应的证书。别写死一个证书文件。5.3 解密报错error:1C80006B:Provider routines::wrong final block length现象openssl_decrypt返回 false错误信息是wrong final block length。原因api_v3_key不对或者nonce和associated_data传错了。V3 回调的resource里有nonce和associated_data解密时必须原样传入。解决检查api_v3_key是不是 32 位nonce是不是 12 位。openssl_decrypt的$tag参数是引用传递确保传入了正确的变量。5.4 回调重复触发导致重复发货现象用户付了一笔钱但订单被处理了两次发了两次货。原因微信回调是至少一次语义如果第一次回调你返回了非 200或者处理超时微信会重试。如果你的业务逻辑没有幂等控制就会重复处理。解决在数据库里对transaction_id加唯一索引或者用 Redis 做分布式锁。处理前先查一下这笔微信单号是否已经处理过。5.5 cURL 请求超时导致订单状态不一致现象用户扫码付了钱但你的系统显示未支付。原因下单请求超时了但微信那边其实已经创建了订单。用户扫码支付后回调可能因为网络问题没收到。解决下单超时后不要直接返回失败而是调用查单接口/v3/pay/transactions/out-trade-no/{out_trade_no}确认订单状态。回调没收到时也要有定时任务主动查单补偿。6. 二次开发把这套代码改造成生产级支付模块6.1 从示例代码到生产模块的改造思路示例代码能跑通但直接上生产还差几个关键改造。第一是配置管理别把商户号和密钥写在 PHP 文件里用环境变量或者配置中心。第二是日志每次请求和回调都要记录完整的请求头、请求体、响应体出问题时能快速定位。第三是异常处理把WxPayException细分网络异常、签名异常、业务异常分开捕获上层好做不同的重试策略。我一般会加一个WxPayLogger类把请求前后的数据写到按天切割的日志文件里class WxPayLogger { public static function log($type, $data) { $file __DIR__ . /../logs/wxpay_ . date(Ymd) . .log; $line date(Y-m-d H:i:s) . [{$type}] . json_encode($data, JSON_UNESCAPED_UNICODE) . \n; file_put_contents($file, $line, FILE_APPEND | LOCK_EX); } }然后在request()方法里发请求前记一次收到响应后记一次。回调入口也记一次。这样出问题时把日志拉出来一看就知道是签名错了还是微信那边返回了错误码。6.2 平台证书自动更新平台证书过期是生产环境最常见的故障。我一般会写一个定时任务每天凌晨调用/v3/certificates接口拉取最新证书对比序列号有变化就更新本地文件。代码大概长这样public function updatePlatformCert() { $result $this-request(GET, /v3/certificates); if ($result[code] ! 200) { throw new WxPayException(获取平台证书失败); } foreach ($result[body][data] as $item) { $cert $this-decryptResource($item); // 解密后得到证书内容 $serialNo $item[serial_no]; // 保存证书文件名带序列号 file_put_contents( __DIR__ . /../cert/platform_ . $serialNo . .pem, $cert[certificate] ); } }验签时根据回调头里的Wechatpay-Serial找到对应的证书文件而不是固定用一个platform_cert.pem。这样即使微信换了证书你的系统也能自动适配。6.3 对账与补偿机制支付系统不能只依赖回调必须有主动对账。我一般会做两层保障第一层是定时任务每 5 分钟查一次最近 10 分钟未支付的订单调用查单接口确认状态第二层是每天凌晨下载微信的对账单跟本地订单逐笔核对发现差异就告警。查单接口的调用很简单$result $wxPay-request(GET, /v3/pay/transactions/out-trade-no/ . $outTradeNo . ?mchid . $config[mch_id]); if ($result[code] 200 $result[body][trade_state] SUCCESS) { // 补偿处理 }注意查单接口的 URL 带了 query string签名时要把?mchidxxx也拼进去否则会返回 401。这个坑我踩过当时排查了半天才发现是签名 URL 没带参数。6.4 一个容易忽略的细节金额单位微信支付所有金额单位都是分不是元。下单时传 1 就是 1 分钱查单返回的amount.total也是分。我见过有人在数据库里存的是元回调回来直接存进去结果对账时发现金额差了 100 倍。从那以后我每次对接支付都强制在代码里加一层金额转换函数所有跟微信交互的地方统一用分展示给用户时才转成元。这个习惯帮我省了不少对账的麻烦。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑