记录一次使用node处理微信小程序的v3支付与支付回调(加解密)
预付款签名生成
比较不好理解的就是签名怎么生成,理解过后其实也挺简单的。下面代码可以直接使用,某些关键参数使用自己的就可以了。 文档链接
import { randomBytes, createPrivateKey, sign } from 'crypto'
import { readFileSync } from 'fs'
const tradeNo = '' // 根据自己业务逻辑生成订单号
const openId = '' // 小程序支付用户的openId
const goodsAmount = 1 // 支付金额:单位分
const goodsName = 'test支付' // 显示在支付时商品名称
const mchid = '' // 商户号
const wxAppid = '' // 小程序appid
const wxPayHost = 'https://api.mch.weixin.qq.com'
const wxPayApi = '/v3/pay/transactions/jsapi'
const apiUrl = wxPayHost + wxPayApi
// 生成签名
const generateSignature = (method, url, timestamp, nonce, data) => {
const key = getWechatPrivateKeyInstance()
const privateKey = createPrivateKey(key);
const message = method + '\n' + url + '\n' + timestamp + '\n' + nonce + '\n' + data + '\n';
const signature = sign('sha256', Buffer.from(message), { key: privateKey }).toString('base64');
return signature;
}
// 读取商户API证书密钥
const getWechatPrivateKeyInstance = (): Buffer => {
const merchantPrivateKeyFilePath = 'pem/apiclient_key.pem'; // 证书文件官方后台生成下载
return readFileSync(resolve(merchantPrivateKeyFilePath));
}
// 微信调起支付所需参数,可以参考文档
const createOrderParams = (money: number, orderName: string, openId: string, tradeNo: string) => {
return {
mchid: mchid,
out_trade_no: tradeNo,
appid: this.wxAppid,
description: orderName,
notify_url: this.notifyUrl,
amount: {
total: money,
currency: 'CNY'
},
payer: {
openid: openId
}
}
}
// 固定格式
const getPrepayIdString = (prepay_id: string) => {
return "prepay_id=" + prepay_id
}
const getPaySign = (prepay_id: string) => {
const getPrepayIdString = getPrepayIdString(prepay_id)
const key = getWechatPrivateKeyInstance()
const privateKey = createPrivateKey(key);
const nonce = randomBytes(16).toString('hex');
const timestamp = Math.floor(Date.now() / 1000).toString();
const message = wxAppid + '\n' + timestamp + '\n' + nonce + '\n' + getPrepayIdString + '\n';
const signature = sign('sha256', Buffer.from(message), { key: privateKey }).toString('base64');
return {
timestamp,
nonce,
package: getPrepayIdString,
signType: 'RSA', // 签名类型,默认为RSA,仅支持RSA
paySign: signature
}
}
const requestData = createOrderParams(goodsAmount, goodsName, openId, tradeNo)
const timestamp = Math.floor(Date.now() / 1000).toString();
// 获取当前时间戳
// 生成随机字符串
const nonce = randomBytes(16).toString('hex');
// 生成签名
const signature = await generateSignature('POST', wxPayApi, timestamp, nonce, JSON.stringify(requestData));
// 构造Authorization头部
const authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${mchid}",nonce_str="${nonce}",timestamp="${timestamp}",serial_no="${this.serial}",signature="${signature}"`;
// 发起微信预付款请求
axios.post(apiUrl, requestData, {
headers: {
'Content-Type': 'application/json',
'Authorization': this.authorization
},
}).then(async res => {
// 拿到调起支付所需prepay_id
const prepay_id = res.data.prepay_id
// 拿到了prepay_id 需要去组串调起支付所需参数
// https://pay.weixin.qq.com/docs/merchant/apis/mini-program-payment/mini-transfer-payment.html
getPaySign(prepay_id)
})
以上流程就能够达到调起小程序微信支付的步骤,当微信支付完成过后,微信小程序官方会调用自己项目提供的notify接口以获取微信支付回调
微信支付回调解密处理
import { createPrivateKey } from 'crypto'
// 微信回调解密
const decodePayNotify = (resource: wxPayCallbackBodyType['resource']): wxPayCallbackDecodeType => {
const v3Key = '' // 微信后台获取 v3 key
try {
const { ciphertext, associated_data, nonce } = resource
const AUTH_KEY_LENGTH = 16
// 密钥
const key_bytes = Buffer.from(v3Key, 'utf-8')
// 随机串
const nonce_bytes = Buffer.from(nonce, 'utf-8')
// 填充内容
const associated_data_bytes = Buffer.from(associated_data, 'utf-8')
// 密文Buffer
const ciphertext_bytes = Buffer.from(ciphertext, 'base64')
// 计算减去16位长度
const cipherdata_length = ciphertext_bytes.length - AUTH_KEY_LENGTH
// upodata
const cipherdata_bytes = ciphertext_bytes.slice(0, cipherdata_length)
// tag
const auth_tag_bytes = ciphertext_bytes.slice(cipherdata_length, ciphertext_bytes.length);
const decipher = createDecipheriv(
'aes-256-gcm', key_bytes, nonce_bytes
);
decipher.setAuthTag(auth_tag_bytes);
decipher.setAAD(Buffer.from(associated_data_bytes));
const output = Buffer.concat([
decipher.update(cipherdata_bytes),
decipher.final(),
]);
// 解密后 转成 JSON 格式输出
return JSON.parse(output.toString('utf8'));
} catch (error) {
console.log('解密失败', error)
throw error
}
}
// 提供notify接口,需要在微信后台配置回调地址,比如接口地址时:/pay/notify。就需要提供路由为https://xxx/pay/notify的接口给微信调用
const notify = (req) => {
console.log('微信回调')
const data: wxPayCallbackBodyType = req?.body || {}
if (data.event_type === 'TRANSACTION.SUCCESS') {
const decodeInfo = decodePayNotify(data.resource)
console.log('支付回调解析数据:', decodeInfo)
}
}
// 微信回调内容
interface wxPayCallbackDecodeType {
mchid: string
appid: string
out_trade_no: string
transaction_id: string
trade_state: string
trade_state_desc: string
bank_type: string
attach: string
success_time: string
payer: { openid: string },
amount: { total: number, payer_total: number, currency: string, payer_currency: string }
}
// 微信回调参数
interface wxPayCallbackBodyType {
id: string
create_time: number
resource_type: string
event_type: string
summary: string
resource: {
original_type: string
algorithm: string
ciphertext: string
associated_data: string
nonce: string
}
}
至此,微信小程序支付比较关键的预付款加密和回调解密都正常走通啦。当然还有一些其他逻辑,比如超时的预付款订单需要主动触发微信关闭订单接口,在此就不演示了。