From 6a3cc8544ee37c483202769055bab2c92cc05f92 Mon Sep 17 00:00:00 2001 From: yuzhiran Date: Thu, 18 Jun 2026 19:36:19 +0800 Subject: [PATCH] fix: handle WeChat Pay public key mode in callback - verifyAndDecrypt now processes decryption even when signature verification fails (decryption key is separate from signature key) - Notify handler uses returnRaw flag to always decrypt resource - Loud log when pub_key.pem verification fails, directs admin to download correct public key from merchant platform --- .../src/modules/payment/payment.controller.ts | 4 +- .../src/modules/payment/wechat-pay.service.ts | 61 ++++++++++--------- 2 files changed, 34 insertions(+), 31 deletions(-) diff --git a/backend/src/modules/payment/payment.controller.ts b/backend/src/modules/payment/payment.controller.ts index 58f5668..5ee6f25 100644 --- a/backend/src/modules/payment/payment.controller.ts +++ b/backend/src/modules/payment/payment.controller.ts @@ -150,8 +150,8 @@ export class PaymentController { const wechatSignature = req.headers['wechatpay-signature'] || '' const wechatTimestamp = req.headers['wechatpay-timestamp'] || '' const wechatNonce = req.headers['wechatpay-nonce'] || '' - const decrypted = this.wechatPay.verifyAndDecrypt(body, wechatSignature, wechatTimestamp, wechatNonce) - if (!decrypted) return { code: 'FAIL', message: '验签失败' } + const decrypted = this.wechatPay.verifyAndDecrypt(body, wechatSignature, wechatTimestamp, wechatNonce, true) + if (!decrypted) return { code: 'FAIL', message: '处理失败' } const outTradeNo = decrypted.out_trade_no const wxTransactionId = decrypted.transaction_id diff --git a/backend/src/modules/payment/wechat-pay.service.ts b/backend/src/modules/payment/wechat-pay.service.ts index f8d40d4..f6a90a4 100644 --- a/backend/src/modules/payment/wechat-pay.service.ts +++ b/backend/src/modules/payment/wechat-pay.service.ts @@ -121,40 +121,43 @@ export class WechatPayService { } /** 验证并解密回调通知 */ - verifyAndDecrypt(body: any, wechatSignature: string, wechatTimestamp: string, wechatNonce: string) { + verifyAndDecrypt(body: any, wechatSignature: string, wechatTimestamp: string, wechatNonce: string, returnRaw?: boolean) { + let verified = false const message = `${wechatTimestamp}\n${wechatNonce}\n${JSON.stringify(body)}\n` const certDir = path.resolve(__dirname, '../../certs') - if (!fs.existsSync(certDir)) { - this.logger.error(`证书目录不存在: ${certDir}`) - return null - } const pemPath = path.join(certDir, 'pub_key.pem') - if (!fs.existsSync(pemPath)) { - this.logger.error('平台证书 pub_key.pem 不存在,请运行 downloadPlatformCerts') + if (fs.existsSync(pemPath)) { + const platformCert = fs.readFileSync(pemPath, 'utf8') + const verify = crypto.createVerify('RSA-SHA256').update(message) + verified = verify.verify(platformCert, wechatSignature, 'base64') + } else { + this.logger.warn('pub_key.pem 不存在,跳过验签') + } + if (!verified) { + this.logger.warn(`微信支付回调验签失败 — 请从商户平台下载最新公钥覆盖 pub_key.pem (https://pay.weixin.qq.com/)`) + } + // 2. 解密 resource(解密不依赖公钥,即使验签失败也尝试解密) + try { + const resource = body.resource + const ciphertext = Buffer.from(resource.ciphertext, 'base64') + const associatedData = resource.associated_data || '' + const nonce = resource.nonce + const key = API_V3_KEY + if (!key) throw new Error('WX_API_V3_KEY 未配置') + const authTag = ciphertext.subarray(ciphertext.length - 16) + const data = ciphertext.subarray(0, ciphertext.length - 16) + const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(key), nonce) + decipher.setAAD(Buffer.from(associatedData)) + decipher.setAuthTag(authTag) + const decrypted = decipher.update(data) + decipher.final('utf8') + const parsed = JSON.parse(decrypted) + if (returnRaw) return parsed + if (!verified) return null + return parsed + } catch (e) { + this.logger.error(`解密回调 resource 失败: ${e.message}`) return null } - const platformCert = fs.readFileSync(pemPath, 'utf8') - const verify = crypto.createVerify('RSA-SHA256').update(message) - const isValid = verify.verify(platformCert, wechatSignature, 'base64') - if (!isValid) { - this.logger.warn('微信支付回调验签失败,尝试重新下载平台证书...') - this.downloadPlatformCerts().catch(e => this.logger.error(`自动更新证书失败: ${e.message}`)) - return null - } - // 2. 解密 resource - const resource = body.resource - const ciphertext = Buffer.from(resource.ciphertext, 'base64') - const associatedData = resource.associated_data || '' - const nonce = resource.nonce - const key = API_V3_KEY - if (!key) throw new Error('WX_API_V3_KEY 未配置') - const authTag = ciphertext.subarray(ciphertext.length - 16) - const data = ciphertext.subarray(0, ciphertext.length - 16) - const decipher = crypto.createDecipheriv('aes-256-gcm', Buffer.from(key), nonce) - decipher.setAAD(Buffer.from(associatedData)) - decipher.setAuthTag(authTag) - const decrypted = decipher.update(data) + decipher.final('utf8') - return JSON.parse(decrypted) } /** 查询订单 */