webman中证书路径需设为600权限并确保php进程用户可读;含连字符的微信回调请求头须通过中间件保留原样;平台证书需按序列号主动拉取并缓存;解密resource.ciphertext必须使用32位apiv3密钥而非证书。

Webman 中证书路径必须可读且权限严格
Webman 是常驻进程,PHP 进程启动后不会重新检查文件权限。如果 apiclient_cert.pem 或 apiclient_key.pem 放在 public/ 目录下,不仅私钥可能被直接下载,还容易因 Web 服务器配置疏漏导致泄露;更常见的是,证书放在 config/cert/wechat/ 后忘记设为 600 权限,导致 PHP 进程读取失败但无明确报错。
实操建议:
- 证书统一放
config/cert/wechat/,用chmod 600 config/cert/wechat/apiclient_*.pem设置权限 - 确认 PHP 进程用户(如
www-data或nginx)对整个路径有执行权限(chmod 755 config/cert/wechat) - Webman reload 前务必验证:在命令行用
sudo -u www-data cat config/cert/wechat/apiclient_key.pem测试是否可读 - 不要用
file_get_contents()直接加载路径变量,先realpath()并检查is_readable()
回调验签前必须手动透传带连字符的请求头
Webman 默认会把 Wechatpay-Serial、Wechatpay-Timestamp 这类含连字符的 Header 转成下划线(如 wechatpay_serial),导致验签时拿不到原始值,签名字符串拼接错误,验签必然失败。
实操建议:
- 在
config/middlewares.php中注册自定义中间件,从$request->getServerParams()中提取原始 Header - 关键字段必须原样保留:
Wechatpay-Serial、Wechatpay-Nonce、Wechatpay-Timestamp、Wechatpay-Signature - 签名字符串拼接格式必须是
"{$timestamp}\n{$nonce}\n{$body}\n"—— 注意末尾那个换行符,缺了就验不过 - 别用
$request->getBody()->getContents()二次读取,它可能为空;应只读一次并缓存$body字符串
平台证书需主动拉取并按序列号索引缓存
微信平台证书会自动轮换,Wechatpay-Serial 头里的序列号可能对应本地没有的证书。不处理这个,某天凌晨证书更新后,所有回调验签全部失败,但日志里只显示“找不到证书”,没提示去拉新证书。
实操建议:
- 首次收到未知
Wechatpay-Serial时,调用/v3/certificates接口获取全量证书列表(需用商户私钥签名) - 解析响应体,遍历
data数组,用$cert['serial_no']匹配,提取$cert['encrypt_certificate']['certificate']并 base64_decode 后保存为 PEM 格式文件 - 证书文件名建议用序列号哈希命名,如
wechat_platform_{$serial_hash}.pem,避免路径注入 - 缓存到内存或 Redis,键为
wechat:platform-cert:{$serial_no},过期时间设为 24 小时(微信证书有效期通常 2 周,但轮换窗口窄)
解密 resource.ciphertext 必须用 APIv3 密钥而非证书
很多人混淆了“验签”和“解密”:验签用的是微信平台证书(公钥),而解密 resource.ciphertext 用的是你在商户平台设置的 32 位 APIv3 key —— 它是 AES-256-GCM 的对称密钥,不是文件,也不是证书里的任何字段。
实操建议:
-
APIv3 key必须作为字符串配置,不能写成路径,也不能误填为商户私钥内容 - 解密前要先 Base64 解码
resource.ciphertext,再用openssl_decrypt(..., 'aes-256-gcm', $apiV3Key, ...),注意 IV 和 AEAD tag 都从resource.associated_data和resource.nonce中提取 - 别跳过
resource.algorithm === "AEAD_AES_256_GCM"校验,微信未来可能支持其他算法 - 解密失败时,优先检查
$apiV3Key是否恰好 32 字节(strlen($key) === 32),中文空格、复制遗漏、前后换行都会导致长度错误
最易被忽略的点:平台证书轮换不是“偶尔发生”,而是微信强制策略;你本地缓存的证书哪怕只差一个序列号,回调链路就断在验签环节,业务侧完全感知不到——它既不抛异常,也不进你的控制器逻辑,只是默默返回 401。务必把证书拉取+缓存逻辑做成幂等接口,上线前用真实回调头压测一遍。











