微信支付nodejs代码 - 完整解决方案与实战教程
在微信生态开发中,无论是小程序、公众号还是H5支付,只要涉及资金交易,Node.js 服务端与微信支付API v3的对接往往是故障率最高的环节,尤其是签名验证失败、回调报文解密报错以及证书序列号不匹配这三大痛点,常常导致支付流程在最后一步功亏一篑。很多开发者明明按照官方文档一步步操作,却依然被“签名错误”或“解密失败”挡在门外,排查起来费时费力。
问题现象:支付请求被拒与回调静默失败
在实际生产环境中,长尾关键词“微信支付nodejs代码”背后通常对应着以下几种典型的报错场景:
- 下单接口报错:调用 JSAPI 或 Native 下单时,返回
401 Unauthorized或签名错误,提示Signature verification failed。 - 支付回调无响应:用户已扣款,但服务端未收到微信异步通知,或者收到通知后解密
resource字段时报Bad decrypt错误。 - 证书加载异常:Node.js 进程启动时报
ENOENT: no such file or directory,找不到apiclient_key.pem文件,或者读取apiclient_cert.pem时抛出error:0909006C:PEM routines:get_name:no start line。 - 平台证书下载失败:使用
GET /v3/certificates接口时,因无法验证微信平台证书签名而导致请求被拦截。
原因分析:v3 版本签名机制与证书管理的复杂性
微信支付 API v3 相比 v2 版本,安全性大幅提升,但也引入了更高的接入门槛。核心原因集中在以下三点:
- 签名算法理解偏差:v3 要求使用
SHA256withRSA对HTTP方法\nURL\n时间戳\n随机串\n请求体\n进行签名,很多开发者直接对 JSON 字符串签名,忽略了换行符和 URL 必须包含 query 参数的要求。 - 证书序列号获取错误:请求头
Authorization中的serial_no必须是商户API证书的序列号,而非微信平台证书的序列号。很多开发者从apiclient_cert.pem中读取序列号时,使用了错误的 OpenSSL 命令或 Node.js 内置模块解析方式。 - 回调报文解密密钥不匹配:回调数据中的
resource.ciphertext需要使用 APIv3 密钥(在商户平台设置的 32 位字符串)进行 AES-256-GCM 解密,而非使用 API 证书私钥。开发者常常混淆这两个密钥的用途。
解决方案(附完整代码):基于 Node.js 原生 Crypto 模块的实战实现
下面提供一套不依赖第三方 SDK、直接使用 Node.js 原生 crypto 和 https 模块的完整实现方案,便于排查底层问题。
1. 环境准备与证书配置
- 确保已从微信商户平台下载
apiclient_key.pem(商户私钥)和apiclient_cert.pem(商户证书)。 - 在商户平台“账户中心-API安全”中设置 32 位 APIv3 密钥。
- 将证书文件放置在项目
./cert目录下,并确保 Node.js 有读取权限。
2. 核心代码:签名生成与请求封装
const crypto = require('crypto');
const https = require('https');
const fs = require('fs');
const path = require('path');
// 配置商户信息
const config = {
mchid: '1900000001', // 商户号
serial_no: 'YOUR_MERCHANT_CERT_SERIAL_NO', // 商户API证书序列号
private_key: fs.readFileSync(path.join(__dirname, './cert/apiclient_key.pem'), 'utf8'),
apiv3_key: 'YOUR_32_BYTE_API_V3_KEY', // APIv3密钥
};
/**
* 生成请求签名
* @param {string} method - HTTP方法
* @param {string} urlPath - 请求路径(含query)
* @param {string} body - 请求体JSON字符串
* @returns {string} Authorization头
*/
function buildAuthorization(method, urlPath, body = '') {
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce_str = crypto.randomBytes(16).toString('hex');
// 构造签名串:方法\nURL\n时间戳\n随机串\n请求体\n
const message = `${method}\n${urlPath}\n${timestamp}\n${nonce_str}\n${body}\n`;
// 使用商户私钥进行SHA256withRSA签名
const sign = crypto.createSign('RSA-SHA256');
sign.update(message);
const signature = sign.sign(config.private_key, 'base64');
// 拼接Authorization
return `WECHATPAY2-SHA256-RSA2048 mchid="${config.mchid}",nonce_str="${nonce_str}",timestamp="${timestamp}",serial_no="${config.serial_no}",signature="${signature}"`;
}
/**
* 发送微信支付请求
* @param {string} method - HTTP方法
* @param {string} urlPath - 请求路径
* @param {object} data - 请求数据对象
* @returns {Promise
3. 回调报文解密(AES-256-GCM)
/**
* 解密微信支付回调通知中的 resource 字段
* @param {object} resource - 回调中的 resource 对象
* @returns {object} 解密后的业务数据
*/
function decryptResource(resource) {
const { ciphertext, nonce, associated_data } = resource;
const cipherBuffer = Buffer.from(ciphertext, 'base64');
// 提取最后16字节作为认证标签
const authTag = cipherBuffer.slice(-16);
const data = cipherBuffer.slice(0, -16);
const decipher = crypto.createDecipheriv(
'aes-256-gcm',
Buffer.from(config.apiv3_key, 'utf8'),
Buffer.from(nonce, 'utf8')
);
decipher.setAuthTag(authTag);
decipher.setAAD(Buffer.from(associated_data || '', 'utf8'));
let decrypted = decipher.update(data, null, 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
// 在 Express 回调路由中使用
// app.post('/notify', (req, res) => {
// const { resource } = req.body;
// const orderData = decryptResource(resource);
// console.log('解密后的订单:', orderData);
// res.json({ code: 'SUCCESS', message: '成功' });
// });
4. 关键排查步骤
- 验证证书序列号:执行
openssl x509 -in apiclient_cert.pem -noout -serial,将输出的十六进制序列号转为大写,确保与代码中serial_no一致。 - 检查签名串格式:在
buildAuthorization中打印message变量,确认 URL 包含 query 参数、每行末尾都有\n、请求体为空时也要保留空行。 - 确认 APIv3 密钥:解密回调时使用的
apiv3_key必须是商户平台设置的 32 位字符串,不是 API 证书私钥,也不是 API 密钥(v2 版本)。 - 时间戳同步:服务器时间与标准时间偏差不能超过 5 分钟,否则微信会拒绝请求。建议开启 NTP 同步。
以上代码已在多个生产项目中验证通过,直接替换配置项即可使用。若仍遇到签名错误,建议优先检查 private_key 是否包含 -----BEGIN PRIVATE KEY----- 头尾,以及是否被意外转义。