- 后端
- 即时通讯
【免费下载链接】easywechat
📦 一个 PHP 微信 SDK
本篇指南聚焦 EasyWeChat 6.x 的微信支付(Pay)模块,覆盖从商户资质初始化(含平台证书与微信支付公钥两种模式)、基于 APIv3/APIv2 的通用请求封装,到回调通知验签、支付与退款事件处理,以及 JSAPI/Native/小程序/APP 四种调起支付配置的完整生成链路。阅读完本文后,你将能基于仓库中的src/Pay/源码与docs/src/6.x/pay/文档,独立完成一个可上线的微信支付接入方案。
一、实例化:Application 与完整配置项
微信支付模块的入口是EasyWeChat\Pay\Application,它是一个工厂类,所有支付能力(HTTP 客户端、工具类、配置、商户账户、验签器、回调服务端)都从这一个实例分发。以下是最完整的初始化配置:
<?php use EasyWeChat\Pay\Application; $config = [ 'mch_id' => 1360649000, // 商户证书 'private_key' => __DIR__ . '/certs/apiclient_key.pem', 'certificate' => __DIR__ . '/certs/apiclient_cert.pem', // v3 API 秘钥 'secret_key' => '43A03299A3C3FED3D8CE7B820Fxxxxx', // v2 API 秘钥 'v2_secret_key' => '26db3e15cfedb44abfbb5fe94fxxxxx', // 平台证书:微信支付 APIv3 平台证书,需要使用工具下载 'platform_certs' => [ // 如果是「平台证书」模式 // 使用 Key/Value 结构, key 为 平台证书的序列号,value 为微信支付平台证书的绝对路径 // "{SerialNo}" => '/path/to/wechatpay/cert.pem' // 如果是「微信支付公钥」模式 // 使用 Key/Value 结构, key 为微信支付公钥 ID(PUB_KEY_ID 开头),value 为微信支付公钥文件绝对路径 // "{$pubKeyId}" => '/path/to/wechatpay/pubkey.pem', ], /** * 接口请求相关配置,超时时间等 */ 'http' => [ 'throw' => true, // 状态码非 200、300 时是否抛出异常,默认为开启 'timeout' => 5.0, // 如果你在国外想要覆盖默认的 url 的时候才使用,根据不同的模块配置不同的 base_uri // 'base_uri' => 'https://api.mch.weixin.qq.com/', ], ]; $app = new Application($config);1.1 必填项与可选参数说明
从 Pay\Config.php 的源码可见,支付模块明确声明了四个必填键:
protected array $requiredKeys = [ 'mch_id', 'secret_key', 'private_key', 'certificate', ];| 配置键 | 必填 | 说明 |
|---|---|---|
mch_id | 是 | 商户号,微信支付商户平台申请获得 |
private_key | 是 | 商户 API 私钥(apiclient_key.pem)的绝对路径,用于 APIv3 请求签名 |
certificate | 是 | 商户 API 证书(apiclient_cert.pem)的绝对路径,其序列号用于签名头声明 |
secret_key | 是 | APIv3 密钥,用于回调通知 AES-GCM 解密 |
v2_secret_key | 否 | APIv2 密钥,仅在调用 v2 接口(如企业付款/付款到零钱)时必需 |
platform_certs | 否 | 平台证书或微信支付公钥映射表,用于验签与敏感字段加密,见下文 |
http | 否 | 底层 HTTP 客户端选项,见 1.3 节 |
1.2 「平台证书」与「微信支付公钥」两种模式
2024 年 Q3 起,微信支付官方开启了「微信支付公钥」平替「平台证书」方案。这意味着初始化时只需配置微信支付公钥 ID与微信支付公钥即可完全兼容,使用 CLI/API 下载「平台证书」不再是必要步骤。两项信息均可在微信支付商户平台 -> 账户中心 -> API 安全 中查看/下载。
从 Merchant.php 的normalizePlatformCerts()可以看到两种模式在代码层面是统一处理的:
- 以列表形式传入(
array_is_list为真,即不带键的数组)时,会自动通过PublicKey::getSerialNo()提取证书序列号作为键——适用于「平台证书」模式; - 以Key/Value 映射传入时,键即你指定的标识符(平台证书序列号,或
PUB_KEY_ID_开头的微信支付公钥 ID)——适用于「微信支付公钥」模式。
两者最终都会被归一化为array<string, PublicKey>,供验签与加密使用。
1.3 http 配置的底层去向
http配置项在 Application.php 的getClient()中被取出并传入Client构造器,最终经 Client.php 合并进 Symfony HttpClient 的默认选项。其中:
throw:默认true,非 200/300 状态码直接抛异常;设置为false时改为返回响应对象交由业务判断;timeout:单次请求超时秒数;base_uri:默认https://api.mch.weixin.qq.com/,仅需覆盖默认域名时(如海外节点)才配置。
二、核心 API:从 $app 访问各模块
Application就是一个工厂类,所有模块都从$app中访问,且几乎都提供了协议(接口)和 setter 可自定义替换。
2.1 API Client(getClient)
$app->getClient();它封装了多种模式的 API 调用方法(get/post/postJson/uploadMedia等),并自动处理了两套签名体系:
- 以
/v3/(含/hk/v3/、/global/v3/)开头视为APIv3 请求,自动追加WECHATPAY2-SHA256-RSA2048授权头; - 其余路径视为APIv2 请求,自动为 POST 的 XML 体或指定 GET 请求的 query 参数附加 MD5/HMAC-SHA256 旧版签名,并将
Content-Type切换为text/xml。
请求/响应的边界细节可以查看 Client.php 中的isV3Request()与request(),v2 响应还会根据return_code/result_code判定业务失败。更多说明请参阅 API 调用。
2.2 工具(getUtils)
$app->getUtils();用于生成各种调起支付所需配置(JSBridge、JSSDK、小程序、APP),以及敏感字段 RSA 加密。源码中Utils构造时注入Merchant,见 Utils.php。详细用法见下文第五节及 工具文档。
2.3 配置(getConfig)
$config = $app->getConfig();可读取与修改运行期配置:$config->get($key, $default)读取,$config->set($key, $value)在调用前动态修改配置项。
2.4 支付账户(getMerchant)
$account = $app->getMerchant(); $account->getMerchantId(); $account->getPrivateKey(); $account->getCertificate(); $account->getSecretKey(); $account->getV2SecretKey(); $account->getPlatformCert($serial); $account->getPlatformCerts();从源码可见 Merchant.php 将商户号、私钥(PrivateKey)、证书(PublicKey)、v3/v2 密钥统一建模为Merchant值对象,并被Client、Validator、Server、Utils共享。getPlatformCert($serial)支持按证书序列号或公钥 ID 精确取回对应平台证书。
三、签名验证:Validator 的完整机制
按官方建议,在拿到微信接口响应和接收到微信支付的回调通知时,都应验证签名,确保数据确实来自微信支付。通过$app->getValidator()获取验证器。
3.1 验证器的工作细节
从 Validator.php 源码可以看到完整验签流程:
- 依次检查
Wechatpay-Signature、Wechatpay-Timestamp、Wechatpay-Serial、Wechatpay-Nonce四个响应头是否齐全; - 拼接待验签报文:
"{timestamp}\n{nonce}\n{body}\n"; - 校验时间戳偏移,
MAX_ALLOWED_CLOCK_OFFSET = 300秒,超过即抛出InvalidSignatureException(防重放); - 按
Wechatpay-Serial从商户账户中取出对应平台证书/公钥,缺失时抛出InvalidConfigException; - 使用
openssl_verify+ SHA256 校验签名,失败抛出InvalidSignatureException。
3.2 推送消息(Webhook)的签名验证
$server = $app->getServer(); $server->handlePaid(function (Message $message, \Closure $next) use ($app) { // $message->out_trade_no 获取商户订单号 // $message->payer['openid'] 获取支付者 openid try{ $app->getValidator()->validate($app->getRequest()); // 验证通过,业务处理 } catch(Exception $e){ // 验证失败 } return $next($message); }); // 默认返回 ['code' => 'SUCCESS', 'message' => '成功'] return $server->serve();3.3 API 返回值的签名验证
// API 请求示例 $response = $app->getClient()->postJson("v3/pay/transactions/jsapi", [...]); try{ $app->getValidator()->validate($response->toPsrResponse()); // 验证通过 } catch(Exception $e){ // 验证失败 }validate()的入参是 PSR-7MessageInterface,因此请求对象($app->getRequest(),来自InteractWithServerRequest)与响应对象($response->toPsrResponse())都能直接传入,验签逻辑完全统一。
四、回调服务端:支付与退款事件处理
$app->getServer()返回 Server.php,负责解析并解密微信支付推送的通知。
4.1 消息解密机制
getRequestMessage()会根据Content-Type自动区分两套解密流程:
- JSON(APIv3):解析
resource中的ciphertext/nonce/associated_data,使用secret_key做 AES-GCM 解密(AesGcm::decrypt); - XML(APIv2):兼容历史回调,若含
req_info则用v2_secret_key的 MD5 作 AES-ECB 解密;若含event_ciphertext等字段,则同样走 AES-GCM。
解密结果封装为Message,其中$message->transaction_id、$message->out_trade_no、$message->mchid、$message->payer['openid']等字段可直接读取。
4.2 事件过滤器
源码中内置了精准的事件过滤:
handlePaid():仅当eventType === 'TRANSACTION.SUCCESS'且trade_state === 'SUCCESS'时触发回调;handleRefunded():仅当事件为REFUND.SUCCESS、REFUND.ABNORMAL、REFUND.CLOSED时触发。
未命中事件类型的消息会自动交给$next($message)继续流转,最终由serve()返回{'code':'SUCCESS','message':'成功'};处理过程抛出异常则返回 HTTP 500 与{'code':'ERROR',...}。
4.3 Laravel 中的接入示例
// 假设你设置的通知地址notify_url为: https://easywechat.com/payment_notify // 注意:通知地址notify_url必须为https协议,且路由需排除 CSRF 验证 Route::post('payment_notify', function () { // $app 为你实例化的支付对象,此处省略实例化步骤 $server = $app->getServer(); // 处理支付结果事件 $server->handlePaid(function ($message) { // $message 为微信推送的通知结果 // 微信支付订单号 $message['transaction_id'] // 商户订单号 $message['out_trade_no'] // 商户号 $message['mchid'] // 进行业务处理,如存数据库等... }); // 处理退款结果事件 $server->handleRefunded(function ($message) { // 同上,$message 详看微信官方文档 // 进行业务处理,如存数据库等... }); return $server->serve(); });五、调起支付:Utils 生成四种支付配置
$app->getUtils()提供四种调起方式所需的签名参数,底层统一由 Utils.php 完成 RSA(APIv3)或 MD5(APIv2)签名:
$appId = '商户申请的公众号/小程序对应的 appid'; $signType = 'RSA'; // 默认RSA,v2要传MD5 $config = $utils->buildBridgeConfig($prepayId, $appId, $signType); // 返回数组5.1 WeixinJSBridge 调起支付
$config = $utils->buildBridgeConfig($prepayId, $appId, $signType);WeixinJSBridge.invoke( 'getBrandWCPayRequest', { timeStamp: "<?= $config['timeStamp'] ?>", //注意 timeStamp 的格式 nonceStr: "<?= $config['nonceStr'] ?>", package: "<?= $config['package'] ?>", signType: "<?= $config['signType'] ?>", paySign: "<?= $config['paySign'] ?>" // 支付签名 }, function (res) { if (res.err_msg == 'get_brand_wcpay_request:ok') { // 使用以上方式判断前端返回,微信团队郑重提示: // res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。 } } )返回结构从源码buildBridgeConfig()可见:appId、timeStamp、nonceStr、package(prepay_id=xxx)、signType、paySign。签名报文为"{appId}\n{timeStamp}\n{nonceStr}\n{package}\n",signType !== 'RSA'时走 v2 的createV2Signature()。
5.2 JSSDK(wx.chooseWXPay)调起支付
$config = $utils->buildSdkConfig($prepayId, $appId, $signType);wx.chooseWXPay({ timestamp: "<?= $config['timestamp'] ?>", nonceStr: "<?= $config['nonceStr'] ?>", package: "<?= $config['package'] ?>", signType: "<?= $config['signType'] ?>", paySign: "<?= $config['paySign'] ?>", success: function (res) { // 支付成功后的回调函数 } })注意:buildSdkConfig()本质是buildBridgeConfig()的变体,只是把timeStamp键重命名为timestamp(源码见 Utils.php 的buildSdkConfig())。
5.3 小程序(wx.requestPayment)调起支付
$config = $utils->buildMiniAppConfig($prepayId, $appId, $signType);wx.requestPayment({ timeStamp: "<?= $config['timeStamp'] ?>", nonceStr: "<?= $config['nonceStr'] ?>", package: "<?= $config['package'] ?>", signType: "<?= $config['signType'] ?>", paySign: "<?= $config['paySign'] ?>", success: function (res) { // 支付成功后的回调函数 } })5.4 APP 调起支付
$config = $utils->buildAppConfig($prepayId, $appId);APP 场景返回结构不同:appid、partnerid(取自商户号)、prepayid、noncestr、timestamp、package(固定Sign=WXPay)以及基于"{appid}\n{timestamp}\n{noncestr}\n{prepayid}\n"计算出的sign字段。
六、实战示例:从下单到查询
以下示例均基于$app->getClient()直接调用微信支付 APIv3/APIv2 接口(完整示例集合见 示例文档)。
6.1 JSAPI 下单
$response = $app->getClient()->postJson("v3/pay/transactions/jsapi", [ "mchid" => "1518700000", // <---- 请修改为您的商户号 "out_trade_no" => "native12177525012012070352333'.rand(1,1000).'", "appid" => "wx6222e9f48a0xxxxx", // <---- 请修改为服务号的 appid "description" => "Image形象店-深圳腾大-QQ公仔", "notify_url" => "https://weixin.qq.com/", "amount" => [ "total" => 1, "currency" => "CNY" ], "payer" => [ "openid" => "o4GgauInH_RCEdvrrNGrnxxxxxx" // <---- 请修改为服务号下单用户的 openid ] ]); \dd($response->toArray(false));下单成功后返回的prepay_id即可传给第五节中buildBridgeConfig()/buildMiniAppConfig()生成调起参数。
6.2 Native 下单
$response = $app->getClient()->postJson('v3/pay/transactions/native', [ 'mchid' => (string)$app->getMerchant()->getMerchantId(), 'out_trade_no' => 'native20210720xxx', 'appid' => 'wxe2fb06xxxxxxxxxx6', 'description' => 'Image形象店-深圳腾大-QQ公仔', 'notify_url' => 'https://weixin.qq.com/', 'amount' => [ 'total' => 1, 'currency' => 'CNY', ] ]); print_r($response->toArray(false));6.3 查询订单(商户订单号 / 微信订单号)
// 按商户订单号查询 $outTradeNo = 'native20210720xxx'; $response = $app->getClient()->get("v3/pay/transactions/out-trade-no/{$outTradeNo}", [ 'query'=>[ 'mchid' => $app->getMerchant()->getMerchantId() ] ]); print_r($response->toArray()); // 按微信订单号查询 $transactionId = '217752501201407033233368018'; $response = $app->getClient()->get("v3/pay/transactions/id/{$transactionId}", [ 'query'=>[ 'mchid' => $app->getMerchant()->getMerchantId() ] ]); print_r($response->toArray());6.4 企业付款到零钱(APIv2)
v2 接口调用由 Client.php 自动附加旧版签名(LegacySignature),v2_secret_key为必填:
$response = $api->post('/mmpaymkttransfers/promotion/transfers', [ 'xml' => [ 'mch_appid' => $app->getConfig()['app_id'], //注意在配置文件中加上app_id 'mchid' => $app->getConfig()['mch_id'], //商户号 'partner_trade_no' => '202203081646729819743', // 商户订单号,需保持唯一性(只能是字母或者数字,不能包含有符号) 'openid' => 'ogn1H45HCRxVRiEMLbLLuABbxxxx', //用户openid 'check_name' => 'FORCE_CHECK', // NO_CHECK:不校验真实姓名, FORCE_CHECK:强校验真实姓名 're_user_name'=> '用户真实姓名', // 如果 check_name 设置为 FORCE_CHECK 则必填用户真实姓名 'amount' => '100', //金额 'desc' => '理赔', // 企业付款操作说明信息。必填 ], 'local_cert' => $app->getConfig()['certificate'], //v2证书绝对路径 'local_pk' => $app->getConfig()['private_key'], //v2证书密钥绝对路径 ]); print_r($response->toArray());6.5 JSAPI 下单(服务商/特约商户模式)
$response = $app->getClient()->postJson("v3/pay/partner/transactions/jsapi", [ "sp_appid" => $appId, // 服务商应用ID "sp_mchid" => '********', // 服务商户号 'sub_mchid' => '*********', // 子商户号/二级商户号 "sub_appid" => '********', // 子商户/二级商户应用ID(选填) "description" => $this->payDesc($from), // 商品描述 "out_trade_no" => $order['pay_sn'], // 商户订单号 "notify_url" => $this->config['notify_url'], // 通知地址 "amount" => [ "total" => intval($order['order_amount'] * 100), // 总金额(单位:分) ], "payer" => [ "sp_openid" => $this->auth['openid'], // 用户在服务商AppID下的唯一标识 "sub_openid" => $this->auth['openid'] // 用户在子商户AppID下的唯一标识。若传sub_openid,则sub_appid必填 ], // 支付者,(sp_openid 和 sub_openid 二选一) 'attach' => $from ]); print_r($response->toArray());6.6 敏感信息加密(6.17.0+)
特约商户进件、支付行业参数等接口需要加密敏感字段(如联系人姓名)。使用Utils::encryptWithRsaPublicKey()配合Wechatpay-Serial头完成:
使用默认公钥 ID(取platform_certs中第一个):
$utils = $app->getUtils(); $response = $app->getClient()->withSerialHeader()->postJson("v3/applyment4sub/applyment/", [ "business_code" => "12345678", 'contact_info' => [ 'contact_name' => $utils->encryptWithRsaPublicKey('张三'), //... ], //... ]); print_r($response->toArray());或显式指定平台证书序列号 / 微信支付公钥 ID(必须在配置项platform_certs内):
$utils = $app->getUtils(); $response = $app->getClient()->withSerialHeader("PUB_KEY_ID_123456")->postJson("v3/applyment4sub/applyment/", [ "business_code" => "12345678", 'contact_info' => [ 'contact_name' => $utils->encryptWithRsaPublicKey("张三","PUB_KEY_ID_123456"), //... ], //... ]); print_r($response->toArray());从源码看,encryptWithRsaPublicKey()使用OPENSSL_PKCS1_OAEP_PADDING填充做 RSA 公钥加密并返回 base64;withSerialHeader()在未传参时自动取platform_certs的第一个键作为Wechatpay-Serial头(见 Client.php)。
七、获取证书序列号
商户证书的序列号是 APIv3 签名头serial_no的来源(由 Signature.php 从certificate自动提取)。如需在部署、运维或第三方工具中人工获取,可用 openssl 命令行:
openssl x509 -in /path/to/merchant/apiclient_cert.pem -noout -serial | awk -F= '{print $2}'八、签名与报文格式补充:源码级确认
- APIv3 请求签名:
Signature::createHeader()拼接"{METHOD}\n{URL+query}\n{timestamp}\n{nonce}\n{body}\n",用商户私钥做 SHA256WithRSA 签名,输出WECHATPAY2-SHA256-RSA2048授权头,内含mchid、nonce_str、timestamp、serial_no、signature五个字段,签名过程完全由Client自动完成; - APIv2 请求签名:
LegacySignature::sign()自动注入nonce_str并保留sub_mch_id/sub_appid,按字典序排序后用v2_secret_key计算 MD5 或 HMAC-SHA256(大写)附加到参数中,无需开发者手工参与; - 签名验证器:
Validator校验四个响应头、300 秒时钟偏移、按序列号取平台证书并做 RSA-SHA256 验签,覆盖回调通知与 API 响应两种场景。
这两套签名与一套验签体系共同保证了 EasyWeChat 微信支付模块的请求可信与回执可验。结合 index 文档、示例文档 与 工具文档,即可从零完成微信支付的初始化、下单、调起、回调与验签的完整闭环。
- 后端
- 即时通讯
【免费下载链接】easywechat
📦 一个 PHP 微信 SDK
相关推荐
WeiXinMPSDK 微信支付(V2)支付回调实战:从 notify_url 到 ResponseHandler 签名验证的完整实现
WeiXinMPSDK 微信支付(V2)支付回调实战:从 notify_url 到 ResponseHandler 签名验证的完整实现 本文围绕 WeiXinM
后端即时通讯金融科技Kubo(IPFS)防火墙配置指南:开放 Swarm 端口 4001 并验证节点可达性
Kubo(IPFS)防火墙配置指南:开放 Swarm 端口 4001 并验证节点可达性 本篇指南以 Kubo(Go 语言实现的 IPFS 节点)为对象,系统讲解
后端即时通讯EasyWeChat 微信支付异步通知处理指南:支付结果、退款与扫码支付回调的 SDK 用法与底层原理
EasyWeChat 微信支付异步通知处理指南:支付结果、退款与扫码支付回调的 SDK 用法与底层原理 微信支付的所有核心事件——用户完成支付、退款成功、扫码支
后端即时通讯
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考