微信支付Native扫码模式实战:从商户私钥签名到Webhook回调闭环

对于面向 PC 端 Web 网站的独立开发者和 SaaS 工具产品,微信支付 Native 扫码支付(模式二) 是转化率最高、用户操作门槛最低的收款形态。
用户在收银台页面点击“立即升级”,前端展示一个微信支付二维码,用户掏出手机微信扫一扫完成付款,前端页面在 1 秒内自动感知并完成权限开通。
看似丝滑的交互背后,涉及到了微信支付 V3 接口规范中最核心的 RSA-SHA256 签名计算、APIv3 证书验签、AES-256-GCM 回调密文解密以及前端长轮询/WebSocket 同步。
本文以 Node.js 全栈为例,手把手跑通微信 Native 支付的全链路闭环。
微信支付 V3 全链路交互时序
客户端 (浏览器) Node.js 后端 微信支付官方网关
│ │ │
│── 1. POST /api/pay/create ─►│ │
│ (选择套餐: 季度会员) │── 2. 生成本地未支付订单 │
│ │── 3. 使用商户私钥计算签名 │
│ │── 4. POST /v3/pay/transactions/native ──►│
│ │◄── 5. 返回 code_url ───────│
│◄── 6. 返回 二维码字符串 ───│
│
│ (前端使用 qrcode.react 将 code_url 渲染为二维码并轮询订单状态)
│
[ 用户手机微信扫码支付成功 ]
│ │
│◄── 7. POST /api/pay/wechat/notify (异步推送密文回调) ───│
│ │
│ │── 8. 使用微信平台证书验证签名
│ │── 9. 使用 APIv3 密钥解密密文
│ │── 10. 开启 DB 事务将订单置为 PAID
│ │── 11. 返回 200 OK
│ │
│── 12. 轮询检测到已支付 ───►│
│◄── 13. 返回支付成功,跳转 ─│
第一步:后端实现 V3 请求签名与统一下单
微信支付 V3 接口要求在请求头 Authorization 中携带通过商户私钥生成的 RSA 签名串:
import crypto from 'crypto';
interface WechatNativeOrderParams {
mchid: string;
appid: string;
serialNo: string;
privateKey: string;
outTradeNo: string;
description: string;
totalFeeInCents: number;
notifyUrl: string;
}
export async function createWechatNativeOrder(params: WechatNativeOrderParams): Promise<string> {
const url = 'https://api.mch.weixin.qq.com/v3/pay/transactions/native';
const method = 'POST';
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonceStr = crypto.randomBytes(16).toString('hex');
const requestBody = JSON.stringify({
appid: params.appid,
mchid: params.mchid,
description: params.description,
out_trade_no: params.outTradeNo,
notify_url: params.notifyUrl,
amount: {
total: params.totalFeeInCents,
currency: 'CNY'
}
});
// 1. 构造 V3 签名原串
const signaturePayload = `${method}\\n/v3/pay/transactions/native\\n${timestamp}\\n${nonceStr}\\n${requestBody}\\n`;
// 2. 使用商户私钥进行 RSA-SHA256 签名
const sign = crypto.createSign('RSA-SHA256');
sign.update(signaturePayload);
const signature = sign.sign(params.privateKey, 'base64');
// 3. 构造 Authorization 请求头
const authHeader = `WECHATPAY2-SHA256-RSA2048 mchid="${params.mchid}",nonce_str="${nonceStr}",signature="${signature}",timestamp="${timestamp}",serial_no="${params.serialNo}"`;
const res = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': authHeader
},
body: requestBody
});
if (!res.ok) {
const errText = await res.text();
throw new Error(`微信下单失败: ${errText}`);
}
const data = await res.json();
return data.code_url; // 微信返回的二维码原串,如 weixin://wxpay/bizpayurl?pr=xxxx
}
第二步:处理支付成功回调与密文解密(Webhook)
当用户完成付款后,微信支付网关会向配置的 notify_url 推送加密通知。为了防止数据篡改,必须使用 APIv3 密钥解密 AES-256-GCM 密文:
// 解密微信 V3 回调报文
export function decryptWechatV3Resource(
apiV3Key: string,
associatedData: string,
nonce: string,
ciphertext: string
): any {
const key = Buffer.from(apiV3Key, 'utf-8');
const nonceBuf = Buffer.from(nonce, 'utf-8');
const authTagLength = 16;
const cipherBuffer = Buffer.from(ciphertext, 'base64');
const authTag = cipherBuffer.subarray(cipherBuffer.length – authTagLength);
const encryptedData = cipherBuffer.subarray(0, cipherBuffer.length – authTagLength);
const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonceBuf);
decipher.setAuthTag(authTag);
decipher.setAAD(Buffer.from(associatedData, 'utf-8'));
const decrypted = Buffer.concat([
decipher.update(encryptedData),
decipher.final()
]);
return JSON.parse(decrypted.toString('utf-8'));
}
在 Fastify 路由中完成回调闭环处理:
app.post('/api/pay/wechat/notify', async (req, reply) => {
const body = req.body as any;
const { resource } = body;
try {
// 解密微信推送的明文订单数据
const decryptedOrder = decryptWechatV3Resource(
process.env.WECHAT_API_V3_KEY!,
resource.associated_data,
resource.nonce,
resource.ciphertext
);
const { out_trade_no, transaction_id, trade_state, amount } = decryptedOrder;
if (trade_state === 'SUCCESS') {
// 业务幂等更新:更新数据库并开通权限
await processSuccessfulPayment(out_trade_no, transaction_id, amount.total);
}
// 必须返回 200 及指定 code 告知微信无需重复推送
return reply.status(200).send({ code: 'SUCCESS', message: 'OK' });
} catch (err) {
console.error('解密微信回调失败:', err);
return reply.status(500).send({ code: 'FAIL', message: '解密失败' });
}
});
第三步:前端二维码渲染与长轮询
前端利用 qrcode.react 快速将后端返回的 code_url 生成图形二维码:
import React, { useEffect, useState } from 'react';
import QRCode from 'qrcode.react';
export const WechatPayModal: React.FC<{ orderId: string; codeUrl: string; onPaid: () => void }> = ({
orderId,
codeUrl,
onPaid
}) => {
useEffect(() => {
// 开启 2 秒一次的轻量轮询
const interval = setInterval(async () => {
const res = await fetch(`/api/orders/${orderId}/status`);
const data = await res.json();
if (data.status === 'PAID') {
clearInterval(interval);
onPaid();
}
}, 2000);
return () => clearInterval(interval);
}, [orderId, onPaid]);
return (
<div className="flex flex-col items-center p-6 bg-white rounded-xl shadow-lg border border-slate-100">
<h3 className="text-lg font-bold text-slate-800 mb-4">请使用微信扫码支付</h3>
<div className="p-3 bg-white border border-slate-200 rounded-lg shadow-inner">
<QRCode value={codeUrl} size={180} />
</div>
<p className="mt-4 text-xs text-slate-500">支付完成后页面将自动跳转</p>
</div>
);
};





