证书须放非Web可访问目录(如runtime/cert/),用配置管理路径并校验可读性;AES-GCM解密需完整提取nonce/ciphertext/associated_data/tag四部分并分别base64解码;必须先用平台证书验签(SHA256withRSA)成功后才解密;回调需禁用CSRF、用php://input获取原始JSON、响应空body+200。

证书文件怎么放、路径怎么写才不报 file_get_contents(): failed to open stream
ThinkPHP 本身不处理证书加载逻辑,微信 V3 要求你手动读取 apiclient_cert.pem 和 apiclient_key.pem,但直接写相对路径极易出错。
常见错误是把证书丢进 public/ 或 runtime/ 下,结果被 Web 服务器屏蔽或权限不足;或者用 __DIR__ . '/../cert/apiclient_cert.pem' 这类硬编码,在部署到不同环境(如 Docker、宝塔)时路径失效。
- 证书必须放在非 Web 可访问目录,比如
config/cert/或runtime/cert/(确保runtime目录可写) - 用 ThinkPHP 的
Env或配置文件统一管理路径:config/wechat.php中定义'cert_path' => app()->getRuntimePath() . 'cert/' - 加载时显式检查文件存在性和可读性:
is_readable($certPath . 'apiclient_cert.pem'),别等微信 SDK 报错才排查 - 私钥文件
apiclient_key.pem必须带密码(微信后台导出时设置的),SDK 会传给openssl_pkey_get_private(),漏填密码会导致签名失败
wechatpay-php SDK 的 AES-GCM 解密回调为啥总抛 Invalid AEAD tag
微信 V3 回调通知体是 AES-256-GCM 加密的,不是简单 base64 解码就能用。很多开发者卡在这步,以为解密函数返回空就是验签失败,其实是 GCM 模式校验失败。
根本原因是:GCM 需要完整拿到 nonce(12 字节)、ciphertext、associated_data 和 tag(16 字节)四部分,缺一不可;而微信把它们全塞在了同一个 JSON 字段里,靠字段名区分,不是按顺序拼接。
- 务必从回调原始 JSON 中提取
resource.encrypted_type(必须是AES-GCM)、resource.nonce、resource.ciphertext、resource.associated_data、resource.tag - 不要用
base64_decode($ciphertext)后直接扔进openssl_decrypt()—— 微信的ciphertext是 base64 编码后的密文,但tag是单独 base64 编码的,必须分别 decode - PHP 的
openssl_decrypt()第五个参数是$tag,必须传入解码后的 binary tag(base64_decode($tag)),且长度严格为 16 字节 - 微信回调 header 中的
Wechatpay-Serial用于匹配平台证书,不是用来解密的,别混淆用途
验签和解密顺序错了,Invalid signature 就白忙活
微信要求「先验签、再解密」,不是反过来。很多人图省事,在没确认通知来源可信前就去解密,结果解出来一堆乱码还去 debug AES 参数,浪费半天时间。
验签依赖的是微信平台证书(不是你的商户证书),而该证书本身又需要定期更新(有效期 2 年,但微信可能提前轮换),所以不能写死。
- 必须先用
Wechatpay-Serialheader 查找本地缓存的对应平台证书(建议存runtime/cert/platform_*.pem) - 找不到就触发自动下载流程:用商户私钥对
/v3/certificates发 GET 请求,微信返回当前所有有效平台证书列表 - 验签用的是 SHA256 with RSA,PHP 要用
openssl_verify(),数据源是method + \n + uri + \n + timestamp + \n + nonce + \n + body + \n拼接后的字符串(注意末尾那个换行) - 只有
openssl_verify()返回 1,才允许执行后续 AES-GCM 解密,否则直接返回 401
ThinkPHP 路由和中间件怎么接住微信回调不丢数据
微信回调是 POST 请求,body 是原始 JSON(不是 application/x-www-form-urlencoded),默认会被 ThinkPHP 的 Input 或 Request 自动解析成数组,导致 AES 密文损坏。
更隐蔽的问题是:某些中间件(比如日志记录、CSRF 验证)会提前读取 input(),让后续无法获取原始流。
- 回调接口路由必须关闭 CSRF 验证:
Route::post('wechat/notify', 'WechatController@notify')->middleware('csrf:except') - 控制器里第一件事是获取原始 body:
$raw = file_get_contents('php://input'),别用$this->request->post()或$this->request->param() - 如果用了多层中间件,确保没有中间件调用
$request->input()或$request->getContent() - 微信要求响应必须是空 body + 200 状态码,ThinkPHP 默认可能输出 JSON 包裹的 success,要显式写
return response('', 200)->header('Content-Type', 'text/plain')
证书轮换、GCM tag 校验、原始流读取 —— 这三处任何一个环节松动,整个回调链就断。微信不会告诉你具体哪一步错了,只会静默重试直到超时。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











