微信支付nodejs代码 - 完整解决方案与实战教程

在微信生态开发中,无论是小程序、公众号还是H5支付,只要涉及资金交易,Node.js 服务端与微信支付API v3的对接往往是故障率最高的环节,尤其是签名验证失败、回调报文解密报错以及证书序列号不匹配这三大痛点,常常导致支付流程在最后一步功亏一篑。很多开发者明明按照官方文档一步步操作,却依然被“签名错误”或“解密失败”挡在门外,排查起来费时费力。

问题现象:支付请求被拒与回调静默失败

在实际生产环境中,长尾关键词“微信支付nodejs代码”背后通常对应着以下几种典型的报错场景:

原因分析:v3 版本签名机制与证书管理的复杂性

微信支付 API v3 相比 v2 版本,安全性大幅提升,但也引入了更高的接入门槛。核心原因集中在以下三点:

  1. 签名算法理解偏差:v3 要求使用 SHA256withRSAHTTP方法\nURL\n时间戳\n随机串\n请求体\n 进行签名,很多开发者直接对 JSON 字符串签名,忽略了换行符和 URL 必须包含 query 参数的要求。
  2. 证书序列号获取错误:请求头 Authorization 中的 serial_no 必须是商户API证书的序列号,而非微信平台证书的序列号。很多开发者从 apiclient_cert.pem 中读取序列号时,使用了错误的 OpenSSL 命令或 Node.js 内置模块解析方式。
  3. 回调报文解密密钥不匹配:回调数据中的 resource.ciphertext 需要使用 APIv3 密钥(在商户平台设置的 32 位字符串)进行 AES-256-GCM 解密,而非使用 API 证书私钥。开发者常常混淆这两个密钥的用途。

解决方案(附完整代码):基于 Node.js 原生 Crypto 模块的实战实现

下面提供一套不依赖第三方 SDK、直接使用 Node.js 原生 cryptohttps 模块的完整实现方案,便于排查底层问题。

1. 环境准备与证书配置

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}
 */
function requestWechatPay(method, urlPath, data = null) {
    return new Promise((resolve, reject) => {
        const body = data ? JSON.stringify(data) : '';
        const authHeader = buildAuthorization(method, urlPath, body);
        
        const options = {
            hostname: 'api.mch.weixin.qq.com',
            port: 443,
            path: urlPath,
            method: method,
            headers: {
                'Authorization': authHeader,
                'Content-Type': 'application/json',
                'Accept': 'application/json',
                'User-Agent': 'Node.js WechatPay Client',
            },
        };
        
        const req = https.request(options, (res) => {
            let responseData = '';
            res.on('data', (chunk) => responseData += chunk);
            res.on('end', () => {
                try {
                    const parsed = JSON.parse(responseData);
                    if (res.statusCode >= 200 && res.statusCode < 300) {
                        resolve(parsed);
                    } else {
                        reject(new Error(`微信支付API错误: ${res.statusCode} - ${responseData}`));
                    }
                } catch (e) {
                    reject(new Error(`解析响应失败: ${responseData}`));
                }
            });
        });
        
        req.on('error', reject);
        if (body) req.write(body);
        req.end();
    });
}

// 示例:JSAPI下单
async function createJsapiOrder() {
    const urlPath = '/v3/pay/transactions/jsapi';
    const data = {
        appid: 'wx1234567890abcdef',
        mchid: config.mchid,
        description: '测试商品',
        out_trade_no: 'ORDER_' + Date.now(),
        notify_url: 'https://yourdomain.com/notify',
        amount: { total: 1, currency: 'CNY' },
        payer: { openid: 'oUpF8uMuAJO_M2pxb1Q9zNjWeS6o' },
    };
    
    try {
        const result = await requestWechatPay('POST', urlPath, data);
        console.log('下单成功:', result);
        return result;
    } catch (err) {
        console.error('下单失败:', err.message);
    }
}


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. 关键排查步骤

  1. 验证证书序列号:执行 openssl x509 -in apiclient_cert.pem -noout -serial,将输出的十六进制序列号转为大写,确保与代码中 serial_no 一致。
  2. 检查签名串格式:buildAuthorization 中打印 message 变量,确认 URL 包含 query 参数、每行末尾都有 \n、请求体为空时也要保留空行。
  3. 确认 APIv3 密钥:解密回调时使用的 apiv3_key 必须是商户平台设置的 32 位字符串,不是 API 证书私钥,也不是 API 密钥(v2 版本)。
  4. 时间戳同步:服务器时间与标准时间偏差不能超过 5 分钟,否则微信会拒绝请求。建议开启 NTP 同步。

以上代码已在多个生产项目中验证通过,直接替换配置项即可使用。若仍遇到签名错误,建议优先检查 private_key 是否包含 -----BEGIN PRIVATE KEY----- 头尾,以及是否被意外转义。