yansongda/pay 微信回调处理全指南:callback() 验签解密与实战用法
yansongda/pay 微信回调处理全指南:callback() 验签解密与实战用法
本篇技术指南聚焦 yansongda/pay 微信(Wechat)支付回调的接收处理,围绕 Pay::wechat()->callback() 方法展开,覆盖 php-fpm 与 swoole 两种运行环境下的参数传递方式、V3 回调验签与 AES-GCM 解密原理、多租户配置切换,以及小程序虚拟支付回调的特殊处理。读完本文,你将掌握一行代码接入微信回调、手动解析请求参数、按租户切换配置,以及结合 success() 正确应答微信的完整实战方案。
方法签名一览
微信接收回调在 SDK 中的入口方法定义如下(见 src/Provider/Wechat.php):
| 方法名 | 参数 | 返回值 |
|---|---|---|
| callback | 无/array/ServerRequestInterface | Collection |
$contents(第一个参数):null/array/Psr\Http\Message\ServerRequestInterface三选一,表示回调请求的来源;$params(第二个参数):可选,用于传递_config、_action等自定义变量;- 返回值:
Yansongda\Supports\Collection实例(解密后的回调数据),在 swoole/长驻进程场景下也可能是Rocket。
快速上手:一行代码接收回调
Pay::config($this->config);
// 是的,你没有看错,就是这么简单!
$result = Pay::wechat()->callback();
不传任何参数时,SDK 会自动从全局($_SERVER、php://input 等)识别微信发来的回调请求,完成验签与解密,最终通过 Collection 实例返回微信的处理参数。例如:
$result = Pay::wechat()->callback();
// $result 为 Collection 实例,resource 字段已被解密为明文
$resource = $result->get('resource'); // 或 $result->resource
对于微信支付 V3 回调,原始报文采用 JSON 信封结构(id、create_time、event_type、resource 等字段),其中 resource 内的 ciphertext 为 AEAD_AES_256_GCM 加密的密文。SDK 验签通过后会自行解密,你拿到的 $result->resource 即为明文的订单数据,可直接读取 out_trade_no、transaction_id、trade_state 等字段进行业务处理。
第一个参数:三种回调请求来源
null:自动识别全局请求
如果您没有传参,则 yansongda/pay 会自动识别微信的回调请求并进行验签解密处理,通过 Collection 实例返回微信的处理参数。
:::warning
建议仅在 php-fpm 下使用,swoole 方式请使用 ServerRequestInterface 参数传递方式。
:::
原因在于 php-fpm 每次请求都是一个全新的进程,$_SERVER、php://input 天然就代表"当前这一次"回调;而 swoole 常驻内存、一个 Worker 会依次处理多个请求,全局超全局变量会被并发复用,此时必须显式传入请求对象才能保证取到正确的那一次回调数据。
ServerRequestInterface:swoole 环境推荐
推荐在 swoole 环境下传递此参数,传递此参数后,yansongda/pay 会自动进行后续处理:
use Psr\Http\Message\ServerRequestInterface;
// 在 swoole 框架(如 Hyperf、Swoft 等)的控制器中
public function notify(ServerRequestInterface $request)
{
Pay::config($config);
$result = Pay::wechat()->callback($request);
// ... 业务处理
return Pay::wechat()->success();
}
SDK 内部依赖 PSR-7 的 ServerRequestInterface 抽象,因此无论你使用哪种 swoole 框架,只要能把框架的请求对象注入进来即可无缝对接。
array:自行解析请求参数
也可以自行解析请求参数,传递一个 array 会自动进行后续处理:
$result = Pay::wechat()->callback([
'body' => file_get_contents('php://input'),
'headers' => [
'Wechatpay-Serial' => 'xxx',
'Wechatpay-Timestamp' => 'xxx',
'Wechatpay-Nonce' => 'xxx',
'Wechatpay-Signature' => 'xxx',
'Content-Type' => 'application/json',
],
]);
从源码看,src/Provider/Wechat.php 的 getCallbackParams() 对 array 参数做了两种归一化:
- 当数组同时包含
body与headers两个键时,会构造一个携带这些头信息的ServerRequest(适用于你自行从原始 HTTP 层拿到报文和头部的情况); - 当数组不包含这两个键时,则把整个数组
json_encode后作为请求体(适用于你已把回调 JSON 解析成数组的情况)。
两种写法最终都会还原为 PSR-7 的 ServerRequestInterface,随后走与其它方式完全一致的处理管线。
第二个参数:自定义变量与多租户切换
第二个参数主要是传递相关自定义变量的,类似于 web() 中的 _config / _method 等参数。
例如,如果你想在回调的时候使用非默认配置,则可以:
$result = Pay::wechat()->callback(null, ['_config' => 'yansongda']);
即切换为 yansongda 这个租户的配置信息。多租户(tenant)机制是 yansongda/pay 初始化配置时的核心能力:配置按 default、yansongda 等租户名分组,回调时通过 _config 指定使用哪一组商户配置进行验签与解密。如果同时维护多个微信商户号(如多个主体、服务商与子商户),这一参数即可保证"谁的配置验谁的签"。
除了 _config,第二个参数还支持 _action 等动作变量,用于区分回调场景,下文虚拟支付部分会详细展开。
源码透视:callback() 背后发生了什么
callback() 并非魔法,它的处理管线完全可读。整体流程如下(见 src/Provider/Wechat.php):
- 归一化请求:
getCallbackParams()把null/array/ServerRequestInterface统一转为 PSR-7 请求对象; - 派发事件:触发
CallbackReceived事件(见 src/Event/CallbackReceived.php),事件中携带 provider、请求对象与 params,便于你通过事件系统做日志、审计等旁路处理; - 选择插件:根据
$params['_action']匹配回调插件——virtual走虚拟支付回调插件(VirtualCallbackPlugin),其余默认走 V3 回调插件(V3\CallbackPlugin); - 执行管线:通过
pay([$plugin], ['_request' => $request, '_params' => $params])交给 Artful 管线执行,插件内部完成验签、解密,并把结果写入Rocket的Destination。
V3 回调:微信支付 V3 验签与 AES-GCM 解密
普通微信支付(JSAPI、Native、H5、App、小程序、合单、支付分等)的回调统一由 src/Plugin/Wechat/V3/CallbackPlugin.php 处理,核心步骤:
第一步:验签。 调用 WechatTrait::verifyWechatSign()(见 src/Traits/WechatTrait.php),从请求头读取:
Wechatpay-Serial:微信平台证书序列号;Wechatpay-Timestamp:回调时间戳;Wechatpay-Nonce:随机串;Wechatpay-Signature:签名。
验签内容为 timestamp . "\n" . nonce . "\n" . body . "\n" 三段拼接,用微信平台公钥做 sha256WithRSAEncryption 验签。同时校验时间戳:abs(time() - timestamp) > 300(5 分钟)即判定过期并抛出"签名异常: 微信回调时间戳已过期"(见 src/Traits/WechatTrait.php),用于防重放。
第二步:解密。 验签通过后,对 resource 中的 ciphertext 调用 decryptWechatResource()(见 src/Traits/WechatTrait.php)解密。加密算法必须是 AEAD_AES_256_GCM,使用配置中的 mch_secret_key(API v3 密钥,长度必须为 32 字节,见 src/Provider/Wechat.php)配合 nonce、associated_data 解密,最终 resource.ciphertext 被替换为明文字符串。
第三步:证书兜底。 验签所需的微信平台证书来自配置项 wechat_public_cert_path。若本地没有对应序列号的证书,SDK 会自动调用 reloadWechatPublicCerts()(见 src/Traits/WechatTrait.php)走 WechatPublicCertsPlugin 重新拉取平台证书并缓存到 CertManager,无需你手动干预。
虚拟支付回调:走 Virtual 插件
微信小程序虚拟支付(游戏代币、会员订阅等虚拟商品)的回调与 V3 回调协议不同——它沿用公众号消息推送的加密规范,必须显式指定 _action => 'virtual':
$result = Pay::wechat()->callback(null, ['_action' => 'virtual']);
// 返回 Collection 实例,包含解密后的回调数据
:::warning
虚拟支付回调必须传递 _action => 'virtual' 参数,否则会使用 V3 回调插件导致验签失败。
:::
对应的实现为 src/Plugin/Wechat/Virtual/CallbackPlugin.php,处理逻辑差异明显:
- 验签方式不同:从 URL Query 读取
msg_signature、timestamp、nonce,与请求体中的Encrypt密文、配置virtual_pay.callback_token一起按字典序排序后计算sha1比对(见 src/Plugin/Wechat/Virtual/CallbackPlugin.php); - 解密算法不同:使用
virtual_pay.encoding_aes_key(43 字符)推导 AES-256-CBC 密钥,解密后校验消息长度前缀与appId是否与mini_app_id一致(见 src/Plugin/Wechat/Virtual/CallbackPlugin.php); - 报文格式兼容:
parseBody()同时兼容 JSON({"Encrypt":"..."})与 XML(<xml><Encrypt><![CDATA[...]]></Encrypt></xml>)两种推送格式(见 src/Plugin/Wechat/Virtual/CallbackPlugin.php)。
虚拟支付所需的 callback_token、encoding_aes_key、offer_id、app_key 等配置统一放在 wechat.default.virtual_pay 段下,详见 初始化文档。若你的小程序仅出售虚拟商品(微信会强制关闭微信支付、无法拿到 mch_id 等商户凭据),只要同时配置 virtual_pay.app_key 与 virtual_pay.app_secret,上方四项微信支付凭据可全部留空——该规则由 src/Config/WechatConfig.php 的 validateRequired() 实现。
回调数据与事件分发
V3 回调解密后的 Collection 中,resource 为明文订单数据,同时保留外层信封字段(id、event_type、resource_type、summary 等)。业务侧一般按 event_type 分发处理,例如:
$result = Pay::wechat()->callback();
$resource = $result->get('resource'); // 明文
switch ($result->get('event_type')) {
case 'TRANSACTION.SUCCESS':
// 支付成功,更新订单状态
break;
case 'REFUND.SUCCESS':
// 退款成功
break;
// ...
}
支付分(PayScore)回调同样走 V3 协议(JSON 信封 + resource 加密),无需额外 _action,直接 Pay::wechat()->callback() 即可,请按明文中的 event_type 自行分发处理(见 支付分文档)。
另外,每次回调都会触发 CallbackReceived 事件(见 src/Event/CallbackReceived.php),你可以监听该事件实现全局的回调日志、风控等旁路逻辑,而不必在每个回调入口重复编写。
处理完成后如何应答微信
微信要求收到回调后必须返回固定格式的响应,否则会视为失败并重试。callback() 只负责接收与解析,应答请使用 success() 方法(详见 微信确认回调):
Pay::config($config);
// $result = Pay::wechat()->callback();
return Pay::wechat()->success();
- 默认返回
{"code":"SUCCESS","message":"成功"}(HTTP 200,application/json); - 支付分场景请传
Pay::wechat()->success(['_action' => 'payscore']),返回 HTTP 204 空包体; - 虚拟支付场景请传
Pay::wechat()->success(['_action' => 'virtual']),默认返回 XML<xml><ErrCode>0</ErrCode><ErrMsg>success</ErrMsg></xml>;需要 JSON 时传Pay::wechat()->success(['_action' => 'virtual', '_format' => 'json']),返回{"ErrCode":0,"ErrMsg":"success"}。
相关分支实现在 src/Provider/Wechat.php。
测试与验证
仓库为回调处理提供了完整的单元测试,可作为验证实现正确性的参照:
- V3 回调插件测试:tests/Plugin/Wechat/V3/CallbackPluginTest.php —— 构造携带
Wechatpay-*签名头的ServerRequest,断言验签解密后resource.ciphertext非空、信封保持原样; - 虚拟支付回调插件测试:tests/Plugin/Wechat/Virtual/CallbackPluginTest.php —— 覆盖 XML/JSON 两种推送、
xpay_goods_deliver_notify、xpay_coin_pay_notify、xpay_refund_notify等事件类型,以及签名缺失、签名错误、参数错误等异常路径。
测试中的请求构造方式(ServerRequest + 头部/Query 参数)与你自行使用 array 或 ServerRequestInterface 传参时的场景一一对应,遇到验签失败时,可对照测试用例排查请求头与报文是否完整。
常见问题排查
- "签名异常: 验证微信签名失败":多为本地缺少对应序列号的微信平台证书,或
mch_secret_key配置错误。确认wechat_public_cert_path已配置(php-fpm 下强烈建议配置,见 初始化文档),SDK 会在缺失时自动拉取证书; - "签名异常: 微信回调时间戳已过期":本地服务器时间与微信服务器时间偏差超过 5 分钟,请校准系统时钟;
- "加解密异常: 解密失败,请检查微信 mch_secret_key 是否正确":
mch_secret_key必须是 32 字节的 API v3 密钥(形如 md5 值),且需与商户平台设置完全一致; - 虚拟支付回调一直验签失败:确认已传
['_action' => 'virtual'],并核对virtual_pay.callback_token与encoding_aes_key是否与微信小程序后台配置一致。
至此,从一行代码接入、三种请求来源、多租户切换,到 V3 与虚拟支付两套验签解密管线的原理,再到应答微信的 success() 用法,微信回调处理的完整闭环已经打通。