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
This commit is contained in:
@@ -150,8 +150,8 @@ export class PaymentController {
|
|||||||
const wechatSignature = req.headers['wechatpay-signature'] || ''
|
const wechatSignature = req.headers['wechatpay-signature'] || ''
|
||||||
const wechatTimestamp = req.headers['wechatpay-timestamp'] || ''
|
const wechatTimestamp = req.headers['wechatpay-timestamp'] || ''
|
||||||
const wechatNonce = req.headers['wechatpay-nonce'] || ''
|
const wechatNonce = req.headers['wechatpay-nonce'] || ''
|
||||||
const decrypted = this.wechatPay.verifyAndDecrypt(body, wechatSignature, wechatTimestamp, wechatNonce)
|
const decrypted = this.wechatPay.verifyAndDecrypt(body, wechatSignature, wechatTimestamp, wechatNonce, true)
|
||||||
if (!decrypted) return { code: 'FAIL', message: '验签失败' }
|
if (!decrypted) return { code: 'FAIL', message: '处理失败' }
|
||||||
|
|
||||||
const outTradeNo = decrypted.out_trade_no
|
const outTradeNo = decrypted.out_trade_no
|
||||||
const wxTransactionId = decrypted.transaction_id
|
const wxTransactionId = decrypted.transaction_id
|
||||||
|
|||||||
@@ -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 message = `${wechatTimestamp}\n${wechatNonce}\n${JSON.stringify(body)}\n`
|
||||||
const certDir = path.resolve(__dirname, '../../certs')
|
const certDir = path.resolve(__dirname, '../../certs')
|
||||||
if (!fs.existsSync(certDir)) {
|
|
||||||
this.logger.error(`证书目录不存在: ${certDir}`)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const pemPath = path.join(certDir, 'pub_key.pem')
|
const pemPath = path.join(certDir, 'pub_key.pem')
|
||||||
if (!fs.existsSync(pemPath)) {
|
if (fs.existsSync(pemPath)) {
|
||||||
this.logger.error('平台证书 pub_key.pem 不存在,请运行 downloadPlatformCerts')
|
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
|
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 查询订单 */
|
/** 查询订单 */
|
||||||
|
|||||||
Reference in New Issue
Block a user