key排序算法
const obj = {}
const res = Object.keys(obj).sort().reduce((pre,cur)=>({..pre,[cur]:obj[cur]}))md5加密
cosnt crypto = require('crypto-js')
cosnt signResult = crypto.createHash('MD5')
.update(qs.stringify(sortedParams,{encode:false}))
.digest("hex").toUpperCase();
return signResult拓展:qs.stringify拼接对象
const qs = require("qs");
const obj = { a: 1, b: { c: { d: 1 } } };
console.log(qs.stringify(obj)); // a=1&b%5Bc%5D%5Bd%5D=1
console.log(qs.stringify(obj, { encode: false })); // a=1&b[c][d]=1步骤流程
参考 https://www.bilibili.com/video/BV12w411o7wJ?p=2&vd_source=11e14f37a256537712e73b4b7f52411c
小程序wx.login获取code 小程序发送code到后端,
后端拿到code,向wx获取数据(session_key,openid), ps:session_key 服务端使用,openid客户端使用 需要参数appid,secret,js_code,grant_type(固定为authorization_code)
后端返回openid,小程序把openid存起来
小程序开始支付 
先调后端接口,后端去下单,小程序再发起请求
后台生成预支付订单 
guid --全局唯一id,也是全宇宙唯一id,一共128位
let guid = require("guid")
module.exports = {
getGuid(){
return guid.create().value.replace(/-/g,'')
}
}这里后台会报headers athorization的问题 因为要求请求头需要Authorization字段,value也有要求 
准备Authorization
生成签名--signature
https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml
文档给的get只是例子,实际还是我们原来的那个请求,也就是之前说headers有问题那个post请求 注意:随机字符串、时间戳前后需要是同一个值,就是和请求params里的得一一对应
// 格式
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n
// 请求时间戳最好与请求参数里的那个一样
`post\n
/v3/pay/transactions/jsapi\n
${Math.round(+new Date() / 1000)}\n
${JSON.stringify(params)}\n
`
// 上面就是签名串计算签名值
对签名串进行私钥签名再进行base64编码,最后结果就是Authorization 的signature 
私钥签名在编码的详细操作
java php 等提供了相应方法,nodejs则没有 步骤是 sha256 with rsa 然后 base64编码 https://developers.weixin.qq.com/community/pay/doc/000ae48c5b0c709d938bdc46e56c00?jumpto=comment&commentid=0004ae8eb20ea0bf948b3a29a5fc 大神封装
// https://github.com/TheNorthMemory/wechatpay-axios-plugin/blob/master/lib/rsa.js
const {
publicEncrypt, privateDecrypt, createSign, createVerify,
constants: { RSA_PKCS1_PADDING, RSA_PKCS1_OAEP_PADDING },
} = require('crypto');
/** @constant 'sha1' */
const sha1 = 'sha1';
/** @constant 'utf8' */
const utf8 = 'utf8';
/** @constant 'base64' */
const base64 = 'base64';
/** @constant 'sha256WithRSAEncryption' */
const sha256WithRSAEncryption = 'sha256WithRSAEncryption';
/**
* @param {number} code - Supporting `RSA_PKCS1_OAEP_PADDING` or `RSA_PKCS1_PADDING`, default is `RSA_PKCS1_OAEP_PADDING`.
* @throws {RangeError} - While the padding isn't `RSA_PKCS1_OAEP_PADDING` nor `RSA_PKCS1_PADDING`.
* @returns {void}
*/
const paddingModeLimitedCheck = (code) => {
if (!(code === RSA_PKCS1_PADDING || code === RSA_PKCS1_OAEP_PADDING)) {
throw new RangeError(`Doesn't supported the padding mode(${code}), here's only support RSA_PKCS1_OAEP_PADDING or RSA_PKCS1_PADDING.`);
}
};
/**
* Provides some methods for the RSA `sha256WithRSAEncryption` with `RSA_PKCS1_OAEP_PADDING`.
*/
class Rsa {
/**
* Alias of the `RSA_PKCS1_OAEP_PADDING` mode
*/
static get RSA_PKCS1_OAEP_PADDING() { return RSA_PKCS1_OAEP_PADDING; }
/**
* Alias of the `RSA_PKCS1_PADDING` mode
*/
static get RSA_PKCS1_PADDING() { return RSA_PKCS1_PADDING; }
/**
* Encrypts text with sha256WithRSAEncryption/RSA_PKCS1_OAEP_PADDING.
* Recommended Node Limits Version >= 12.9.0 (`oaepHash` was available), even if it works on v10.15.0.
*
* @param {string} plaintext - Cleartext to encode.
* @param {string|Buffer} publicKey - A PEM encoded public certificate.
* @param {number} padding - Supporting `RSA_PKCS1_OAEP_PADDING` or `RSA_PKCS1_PADDING`, default is `RSA_PKCS1_OAEP_PADDING`.
*
* @returns {string} Base64-encoded ciphertext.
* @throws {RangeError} - While the padding isn't `RSA_PKCS1_OAEP_PADDING` nor `RSA_PKCS1_PADDING`.
*/
static encrypt(plaintext, publicKey, padding = RSA_PKCS1_OAEP_PADDING) {
paddingModeLimitedCheck(padding);
return publicEncrypt({
oaepHash: sha1,
key: publicKey,
padding,
}, Buffer.from(plaintext, utf8)).toString(base64);
}
/**
* Decrypts base64 encoded string with `privateKey`.
* Recommended Node Limits Version >= 12.9.0 (`oaepHash` was available), even if it works on v10.15.0.
*
* @param {string} ciphertext - Was previously encrypted string using the corresponding public certificate.
* @param {string|Buffer} privateKey - A PEM encoded private key certificate.
* @param {number} padding - Supporting `RSA_PKCS1_OAEP_PADDING` or `RSA_PKCS1_PADDING`, default is `RSA_PKCS1_OAEP_PADDING`.
*
* @returns {string} Utf-8 plaintext.
* @throws {RangeError} - While the padding isn't `RSA_PKCS1_OAEP_PADDING` nor `RSA_PKCS1_PADDING`.
*/
static decrypt(ciphertext, privateKey, padding = RSA_PKCS1_OAEP_PADDING) {
paddingModeLimitedCheck(padding);
return privateDecrypt({
oaepHash: sha1,
key: privateKey,
padding,
}, Buffer.from(ciphertext, base64)).toString(utf8);
}
/**
* Creates and returns a `Sign` string that uses `sha256WithRSAEncryption`.
*
* @param {string|Buffer} message - Content will be `crypto.Sign`.
* @param {string|Buffer} privateKey - A PEM encoded private key certificate.
*
* @returns {string} Base64-encoded signature.
*/
static sign(message, privateKey) {
return createSign(sha256WithRSAEncryption).update(message).sign(
privateKey,
base64,
);
}
/**
* Verifying the `message` with given `signature` string that uses `sha256WithRSAEncryption`.
*
* @param {string|Buffer} message - Content will be `crypto.Verify`.
* @param {string} signature - The base64-encoded ciphertext.
* @param {string|Buffer} publicKey - A PEM encoded public certificate.
*
* @returns {boolean} True is passed, false is failed.
*/
static verify(message, signature, publicKey) {
return createVerify(sha256WithRSAEncryption).update(message).verify(
publicKey,
signature,
base64,
);
}
}
module.exports = Rsa;
module.exports.default = Rsa;其他方案:网友评论
const nonceStr = Math.random().toString(36).slice(-10)
const timestamp = (new Date().getTime() / 1000).toFixed(0)
const message = `GET\n/v3/certificates\n${timestamp}\n${nonceStr}\n\n`
const signature = crypto.createSign('RSA-SHA256')
.update(message, 'utf-8')
.sign(fs.readFileSync('./apiclient_key.pem')
.toString(), 'base64')以上两种可以试一试,哪个靠谱用哪个 以下使用第一种进行演示
const Rsa = require('./rsa.js);// 就是第一个方案的文件
function calcSign(msg){
let pem = fs.readFileSync('./apiclient_key.pem')
return Rsa.sign(msg,pem).toString('base64')
}
验签
利用官方提供的工具可以验证你计算的签名值是否正确 https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
选择文件
密码是商户号
计算前的参数拷贝进去,看官方工具计算结果和你的一不一样 

设置Athorization
所以后端的headers最后设置成 注意:保留双引号的
axios.post('xx',[params],{
headers:{'Authorization':`WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"`}
})最后请求示例
let timestamp = util.getTimestamp() // 用到的地方都是这个值
let nonceStr = util.getGuid() // 用到的地方都是这个值
let body = {
appid: config.APPID,
mchid: config.MCHID,
description:'测试一下',
out_trade_no: util.getGuid(),
notify_url: "http: //simbajs.com:8089/notify',
amount: { total: money },
payer: { openid }
}
let AuthSecondParams = {
mchid,
serial_no,
nonce_str,
timestamp,
signature:'xxxxxxxxxxxxxxxxxx' // 计算后的签名值
}
function toJoin(obj){
return Object.keys(obj).map(k=>`${k}="${obj[k]}"`).join()
}
const PAY_API = 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi'
const Authorization = 'WECHATPAY2-SHA256-RSA2048'+' '+ toJoin(AuthSecondParams)
let { data } = await axios.post(PAY_API, body,{
headers:{
'Authorization':Authorization
}
})后端返回预支付订单号
前面一切顺利就可以拿到prepay_id了 后台需要给前端返回前端需要的数据 前端需要的数据也就是小程序调支付( wx.requestPayment)要的数据 看这里 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
上面paySign使用signature是不对的,还得重新计算一遍另一个签名值 查看文档可知
计算paySign签名值
签名串一共有四行,每一行为一个参数。行尾以\n(换行符,ASCII编码值为0x0A)结束,
包括最后一行。
如果参数本身以\n结束,也需要附加一个\n
// 格式
小程序appId\n
时间戳\n
随机字符串\n
订单详情扩展字符串\n
// 实例
wx8888888888888888\n
1414561699\n
5K8264ILTKCH16CQ2502SI8ZNMTM67VS\n
prepay_id=wx201410272009395522657a690389285100\n
// 老样子还是用之前的计算签名值方法 sha256 with rsa base64那套
const Rsa = require('./rsa.js); // 就是第一个方案的文件
function calcSign(msg){
let pem = fs.readFileSync('./apiclient_key.pem')
return Rsa.sign(msg,pem).toString('base64')
}
function joinMessage(...args){
return args.join('\n)+'\n'
}
let package ="prepay_id= + prepay_id
let paySign = calcSign(joinMessage(config.APPID,timeStamp,nonceStr,package))
let { data:{prepay_id} } = await axios.post( 'https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi ', body,{
headers:{ 'Authorization':Authorization});
res.json({
nonceStr,
package: package,
paysign: paysign,
timeStamp,
signType:"RSA'
})前端接收
小程序需要的参数看这里 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml
| 参数名 | 变量 | 类型[长度限制] | 必填 | 描述 |
|---|---|---|---|---|
| 小程序ID | appId | string[1,32] | 是 | 商户申请的小程序对应的appid,由微信支付生成,可在小程序后台查看 |
| 示例值:wx8888888888888888 | ||||
| 时间戳 | timeStamp | string[1,32] | 是 | 时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数。注意:部分系统取到的值为毫秒级,需要转换成秒(10位数字)。 |
| 示例值:1414561699 | ||||
| 随机字符串 | nonceStr | string[1,32] | 是 | 随机字符串,不长于32位。推荐随机数生成算法 |
| 。 | ||||
| 示例值:5K8264ILTKCH16CQ2502SI8ZNMTM67VS | ||||
| 订单详情扩展字符串 | package | string[1,128] | 是 | 小程序下单接口返回的prepay_id参数值,提交格式如:prepay_id=*** |
| 示例值:prepay_id=wx201410272009395522657a690389285100 | ||||
| 签名方式 | signType | string[1,32] | 是 | 签名类型,默认为RSA,仅支持RSA。 |
| 示例值:RSA | ||||
| 签名 | paySign | string[1,512] | 是 | 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值 |
| 示例值:oR9d8PuhnIc+YZ8cB== |

xxxsjan Docs