获取公钥-封装获取微信平台证书列表+解密得到微信公钥
pay.weixin.qq.com/wiki/doc/ap...
pay.weixin.qq.com/wiki/doc/ap...
解密证书列表拿到public key
js
//解密证书列表 解出CERTIFICATE以及public key
async decodeCertificates() {
let result = await this.getCertificates();
if (result.status != 200) {
throw new Error('获取证书列表失败')
}
let certificates = typeof result.data == 'string' ? JSON.parse(result.data).data : result.data.data
for (let cert of certificates) {
let output = this.decode(cert.encrypt_certificate)
cert.decrypt_certificate = output.toString()
let beginIndex = cert.decrypt_certificate.indexOf('-\n')
let endIndex = cert.decrypt_certificate.indexOf('\n-')
let str = cert.decrypt_certificate.substring(beginIndex + 2, endIndex)
// 生成X.509证书
let x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64'));
let public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64')
// 平台证书公钥
cert.public_key = `-----BEGIN PUBLIC KEY-----\n` + public_key + `\n-----END PUBLIC KEY-----`
}
return this.certificates = certificates
}
其目的是从微信支付平台获取到商户证书列表,然后解析各个证书,并将关键信息存储到 certificates
数组中。
具体如下:
- 首先,在方法内部调用了当前对象上的
getCertificates()
方法,异步获取商户证书列表并将结果存储到变量result
中。 - 然后,判断结果状态码是否等于 200,如果不等于则抛出异常,表示获取证书列表失败。
- 接着,从
result
中解析出证书列表certificates
。 - 遍历证书列表
certificates
,对于每一个证书,先将其加密字符串解密得到明文证书decrypt_certificate
,然后根据 X.509 标准生成 X.509 证书,并从中获取公钥,将其转换为 Base64 编码的形式,存储到该证书对象的public_key
属性中。 - 最终将解析后的证书列表
certificates
存储到当前对象的certificates
属性中,并返回该属性值。
具体解密证书的decode
方法
js
//解密
decode(params) {
const AUTH_KEY_LENGTH = 16;
// ciphertext = 密文,associated_data = 填充内容, nonce = 位移
const { ciphertext, associated_data, nonce } = params;
// 密钥
const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8');
// 位移
const nonce_bytes = Buffer.from(nonce, 'utf8');
// 填充内容
const associated_data_bytes = Buffer.from(associated_data, 'utf8');
// 密文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 = crypto.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(),
]);
return output;
}
具体如下:
- 首先,该方法接受一个
params
对象,包含需要解密的密文ciphertext
、填充内容associated_data
和位移nonce
。 - 然后,从当前对象的
apiv3_private_key
属性中获取到密钥字符串key_bytes
。 - 接着,将
nonce
和associated_data
转换为 Buffer 类型的对象nonce_bytes
和associated_data_bytes
。 - 将
ciphertext
转换成base64
编码的Buffer
类型的对象ciphertext_bytes
。 - 根据算法规范,计算去掉认证信息长度的密文长度。
- 将密文截取得到明文部分
cipherdata_bytes
和认证部分auth_tag_bytes
。 - 使用 Node.js 自带的
crypto
模块创建一个Decipheriv
类实例,使用aes-256-gcm
算法解密密文。同时设置认证标签和填充内容,用于验证密文的完整性和正确性。 - 最后调用
decipher.update()
方法将密文进行解密,返回解密后的Buffer
类型的对象output
,并将其返回。
完整代码
js
const urllib = require('urllib');
const { KJUR, hextob64 } = require('jsrsasign')
const RandomTool = require('./RandomTool')
const crypto = require("crypto");
const x509 = require('@peculiar/x509');
class WxPayment {
constructor({ appid, mchid, private_key, serial_no, apiv3_private_key, notify_url } = {}) {
this.appid = appid; // 公众号appid
this.mchid = mchid; // 商户号mchid
this.private_key = private_key; // 商户私钥
this.serial_no = serial_no; // 证书序列号,用于声明所使用的证书
this.apiv3_private_key = apiv3_private_key; // APIv3密钥,解密平台证书
this.notify_url = notify_url; // 回调地址
this.requestUrls = {
// pc端native下单API
native: () => {
return {
url: 'https://api.mch.weixin.qq.com/v3/pay/transactions/native',
method: 'POST',
pathname: '/v3/pay/transactions/native',
}
},
// 获取平台证书url
getCertificates: () => {
return {
url: `https://api.mch.weixin.qq.com/v3/certificates`,
method: `GET`,
pathname: `/v3/certificates`,
}
},
// 通过out_trade_no查询订单url配置
getTransactionsByOutTradeNo: ({ pathParams }) => {
return {
url: `https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
method: `GET`,
pathname: `/v3/pay/transactions/out-trade-no/${pathParams.out_trade_no}?mchid=${this.mchid}`,
}
},
}
// 初始化平台证书
this.decodeCertificates()
}
// 通过商户订单号out_trade_no查询订单
async getTransactionsByOutTradeNo(params) {
return await this.wxSignRequest({ pathParams: params, type: 'getTransactionsByOutTradeNo' })
}
// 请求微信服务器签名封装
async wxSignRequest({ pathParams, bodyParams, type }) {
let { url, method, pathname } = this.requestUrls[type]({ pathParams })
let timestamp = Math.floor(Date.now() / 1000) // 时间戳
let onece_str = RandomTool.randomString(32); // 随机串
let bodyParamsStr = bodyParams && Object.keys(bodyParams).length ? JSON.stringify(bodyParams) : '' // 请求报文主体
let signature = this.rsaSign(`${method}\n${pathname}\n${timestamp}\n${onece_str}\n${bodyParamsStr}\n`, this.private_key, 'SHA256withRSA')
// 请求头传递签名
let Authorization = `WECHATPAY2-SHA256-RSA2048 mchid="${this.mchid}",nonce_str="${onece_str}",timestamp="${timestamp}",signature="${signature}",serial_no="${this.serial_no}"`
// 接口请求
let { status, data } = await urllib.request(url, {
method: method,
dataType: 'text',
data: method == 'GET' ? '' : bodyParams,
timeout: [10000, 15000],
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': Authorization
},
})
return { status, data }
}
//native统一下单
async native(params) {
let bodyParams = {
...params,
appid: this.appid,
mchid: this.mchid,
notify_url: this.notify_url,
}
return await this.wxSignRequest({ bodyParams, type: 'native' })
}
// 获取平台证书
async getCertificates() {
return await this.wxSignRequest({ type: 'getCertificates' })
}
/**
* rsa签名
* @param content 签名内容
* @param privateKey 私钥,PKCS#1
* @param hash hash算法,SHA256withRSA
* @returns 返回签名字符串,base64
*/
rsaSign(content, privateKey, hash = 'SHA256withRSA') {
// 创建 Signature 对象
const signature = new KJUR.crypto.Signature({
alg: hash,
// 私钥
prvkeypem: privateKey
})
// 传入待加密字符串
signature.updateString(content)
// 生成密文
const signData = signature.sign()
// 将内容转成base64
return hextob64(signData)
}
//验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数
async verifySign({ timestamp, nonce, serial, body, signature }) {
// 拼接参数
let data = `${timestamp}\n${nonce}\n${typeof body == 'string' ? body : JSON.stringify(body)}\n`;
// 用crypto模块解密
let verify = crypto.createVerify('RSA-SHA256');
// 添加摘要内容
verify.update(Buffer.from(data));
// 从初始化的平台证书中获取公钥
for (let cert of this.certificates) {
if (cert.serial_no == serial) {
return verify.verify(cert.public_key, signature, 'base64');
} else {
throw new Error('平台证书序列号不相符')
}
}
}
//解密证书列表 解出CERTIFICATE以及public key
async decodeCertificates() {
let result = await this.getCertificates();
if (result.status != 200) {
throw new Error('获取证书列表失败')
}
let certificates = typeof result.data == 'string' ? JSON.parse(result.data).data : result.data.data
for (let cert of certificates) {
let output = this.decode(cert.encrypt_certificate)
cert.decrypt_certificate = output.toString()
let beginIndex = cert.decrypt_certificate.indexOf('-\n')
let endIndex = cert.decrypt_certificate.indexOf('\n-')
let str = cert.decrypt_certificate.substring(beginIndex + 2, endIndex)
// 生成X.509证书
let x509Certificate = new x509.X509Certificate(Buffer.from(str, 'base64'));
let public_key = Buffer.from(x509Certificate.publicKey.rawData).toString('base64')
// 平台证书公钥
cert.public_key = `-----BEGIN PUBLIC KEY-----\n` + public_key + `\n-----END PUBLIC KEY-----`
}
return this.certificates = certificates
}
//解密
decode(params) {
const AUTH_KEY_LENGTH = 16;
// ciphertext = 密文,associated_data = 填充内容, nonce = 位移
const { ciphertext, associated_data, nonce } = params;
// 密钥
const key_bytes = Buffer.from(this.apiv3_private_key, 'utf8');
// 位移
const nonce_bytes = Buffer.from(nonce, 'utf8');
// 填充内容
const associated_data_bytes = Buffer.from(associated_data, 'utf8');
// 密文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 = crypto.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(),
]);
return output;
}
}
module.exports = WxPayment;
代码逻辑:
实例化时调用this.decodeCertificates()
初始化证书,接着调用getCertificates
请求平台证书。然后便利平台证书进行解密拿到平台证书公钥。
封装验证微信发起的签名+解密数据得到用户订单信息
开发回调地址接口对应的逻辑
Service
js
callback: async (req) => {
let timestamp = req.header('Wechatpay-Timestamp')
let nonce = req.header('Wechatpay-Nonce')
let serial = req.header('Wechatpay-Serial')
let signature = req.header('Wechatpay-Signature')
let body = req.body
// 1.校验收到的请求是否微信服务器发起
let result = await payment.verifySign({
timestamp: timestamp,
nonce: nonce,
serial: serial,
signature: signature,
body: body
})
if (!result) {
return
}
// 2.解密body中的resource数据,拿到用户订单信息
let bufferOne = payment.decode(body.resource)
let json = JSON.parse(bufferOne.toString('utf8'))
console.log(json)
return BackCode.buildSuccess()
},
验证签名
js
//验证签名 timestamp,nonce,serial,signature均在HTTP头中获取,body为请求参数
async verifySign({ timestamp, nonce, serial, body, signature }) {
// 拼接参数
let data = `${timestamp}\n${nonce}\n${typeof body == 'string' ? body : JSON.stringify(body)}\n`;
// 用crypto模块解密
let verify = crypto.createVerify('RSA-SHA256');
// 添加摘要内容
verify.update(Buffer.from(data));
// 从初始化的平台证书中获取公钥
for (let cert of this.certificates) {
if (cert.serial_no == serial) {
return verify.verify(cert.public_key, signature, 'base64');
} else {
throw new Error('平台证书序列号不相符')
}
}
}
- 该方法接收一个参数对象,包括时间戳
timestamp
、随机字符串nonce
、平台证书序列号serial
、请求体body
和签名signature
。 - 将
timestamp
、nonce
和body
拼接成一个字符串data
,使用\n
进行分隔符,其中需要注意将body
转换为字符串形式。 - 使用 Node.js 自带的
crypto
模块创建一个Verify
类实例,使用RSA-SHA256
签名算法进行签名验证。 - 对
data
进行摘要计算,在计算中添加需要验证的数据。 - 通过遍历初始化时传入的
certificates
数组,查找到与serial
相对应的平台证书cert
。 - 如果找到了相应的证书,则使用
verify.verify()
方法进行签名验证,传入公钥cert.public_key
、签名signature
和编码方式base64
,返回验证结果。 - 如果未找到相应的平台证书,则抛出错误提示平台证书序列号不相符。
需要注意的是,该方法使用 RSA-SHA256
算法进行签名验证,同时还需要从初始化传入的平台证书数组 certificates
中获取证书公钥进行验证,以确保请求的合法性。
查询订单⽀付状态逻辑封装+快速验证
pay.weixin.qq.com/wiki/doc/ap...
商户订单号查询
-
查询订单逻辑封装
csharp//通过out_trade_no查询订单 async getTransactionsByOutTradeNo(params) { return await this.wxSignRequest({ pathParams: params, type: 'getTransactionsByOutTradeNo' }) }
-
验证订单状态查询
javascriptconst { payment } = require('./config/wechatPay'); (async () => { let wechatOrder = await payment.getTransactionsByOutTradeNo({ out_trade_no: '123456789wqjeqjwdiqhdhqd' }) console.log(wechatOrder.data) })()
传入商户订单号,和直连商户号mchid 使用 wxSignRequest
进行请求。