Node.js实现微信支付v3JSAPI

最近做的小程序需要用到微信支付,但是由于后端只会Node.js,所以只能用Node.js实现微信支付功能了,微信支付官方没有提供Node.js的SDK,只好手动实现了,确实有点点麻烦

下单流程

具体实现

后端获取openid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var request = require("../utils/request").request
function code2Session(code) {
return new Promise((resolve, reject) => {
if (!code) {
reject({ errcode: "error_noCode" })
} else {
request({
url: "https://api.weixin.qq.com/sns/jscode2session",
data: {
appid: "wx0da2fc08*******",
secret: "19083b917c9d08f***************",
js_code: code,
grant_type: "authorization_code"
},
method: "GET",
}).then(res => {
let resObj = JSON.parse(res)
if (resObj.openid) {
resolve({ errcode: "success", data: resObj })
}
else if (resObj.errcode == 40163) {
reject({ errcode: "error_codeBeenUsed" })
} else if (resObj.errcode == 40029) {
reject({ errcode: "error_invalidCode" })
} else {
reject({ errcode: "error_unknownError" })
}
})
}
})
}

request是封装axios的方法

后端prepay接口

POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
mchid: config.mchid,
out_trade_no: getUUID(),
appid: config.appid,
description: "test",
notify_url: "",
amount: {
total: money
},
payer: {
openid: openid
}
}

数字签名格式

1
WECHATPAY2-SHA256-RSA2048 mchid="${mchid}",nonce_str="${nonce_str}",signature="${signature}",timestamp="${timestamp}",serial_no="${serial_no}"

mchid为商户号,serial_no为证书序列号

数字签名写法

微信支付的数字签名算法是SHA256withRSA后再base64,一开始打算用crypto实现,但是一直报错,所以用jsrsasign来写了,content是签名内容,privateKey是私钥,hash这里是SHA256withRSA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// rsa.js

const { KJUR, hextob64 } = require('jsrsasign')
/**
* rsa签名参考:https://www.jianshu.com/p/145eab95322c
*/
var SignUtil = {
/**
* rsa签名
* @param content 签名内容
* @param privateKey 私钥,PKCS#1
* @param hash hash算法,SHA256withRSA,SHA1withRSA
* @returns 返回签名字符串,base64
*/
rsaSign: function (content, privateKey, hash) {
// 创建 Signature 对象
const signature = new KJUR.crypto.Signature({
alg: hash,
//!这里指定 私钥 pem!
prvkeypem: privateKey
})
signature.updateString(content)
const signData = signature.sign()
// 将内容转成base64
return hextob64(signData)
},
}

module.exports = SignUtil

需信息拼接

需加密信息格式如下

1
2
3
4
5
HTTP请求方法\n
URL\n
请求时间戳\n
请求随机串\n
请求报文主体\n

使用joinMessage函数拼接

1
2
3
function joinMessage(...args) {
return args.join("\n") + "\n"
}

发请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
const fs = require("fs")
const axios = require("axios")
const getUUID = require("./getUUID")
const config = require("../config.js")
const utils = require("./utils")
const SignUtil = require("./rsa")
const pem = fs.readFileSync("./certs/apiclient_key.pem").toString()

function calcSign(msg) {
return SignUtil.rsaSign(msg, pem, "SHA256withRSA")
}

function prepay(money, openid) {
return new Promise((resolve, reject) => {
let nonce_str = getUUID()
let ts = utils.getTimeStamp()
let reqBody = {
mchid: config.mchid,
out_trade_no: getUUID(),
appid: config.appid,
description: "test",
notify_url: "https://waimai.diaoan.xyz/callback",
amount: {
total: money
},
payer: {
openid: openid
}
}
let msg = utils.joinMessage("POST", "/v3/pay/transactions/jsapi", ts, nonce_str, JSON.stringify(reqBody))
let signature = calcSign(msg)
let auth = `WECHATPAY2-SHA256-RSA2048 mchid="${config.mchid}",nonce_str="${nonce_str}",signature="${signature}",timestamp="${ts}",serial_no="${config.serial_no}"`
console.log(msg);
console.log(signature);
console.log(auth);
axios.post("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi", reqBody, {
headers: {
Authorization: auth
}
}).then(res => {
if (res.status == 200) {
let package = `prepay_id=${res.data.prepay_id}`
let paySignMsg = utils.joinMessage(config.appid, ts, nonce_str, package)
let paySign = calcSign(paySignMsg)
resolve({
nonce_str: nonce_str,
ts: ts,
prepay_id: res.data.prepay_id,
paySign: paySign
})
}
}).catch(err => {
console.log(err)
reject(err)
})
})
}

生成paySign

顺序是appid,ts,nonce_str,package,不能弄错,拼接后,再进行一次数字签名,传到前端

前端唤起微信支付

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
api.request({
url: "/prepay",
method: "POST",
data: {
money: 1,
openid: wx.getStorageSync('user').openid
}
}).then(res => {
console.log(res)
let data = {
nonceStr: res.data.nonce_str,
package: `prepay_id=${res.data.prepay_id}`,
paySign: res.data.paySign,
timeStamp: res.data.ts.toString(),
signType: "RSA"
}
console.log(data)
wx.requestPayment(data)
}).catch(err => {
console.log(err)
})

api.request是对wx.request的封装

这样就完成了微信支付小程序的下单

用到的几个函数

calcSign

1
2
3
4
function calcSign(msg) {
return SignUtil.rsaSign(msg, pem, "SHA256withRSA")
}
// msg为需加密内容,pem是用fs模块读取证书文件并转成字符串

getTimeStamp

1
2
3
4
function getTimeStamp() {
return Math.round(+new Date / 1000)
}
// 返回10位时间戳

joinMessage

1
2
3
4
function joinMessage(...args) {
return args.join("\n") + "\n"
}
// 将传入的参数以每行一个的形式转化成字符串

getUUID

1
2
3
4
5
var guid = require("guid")
function getUUID() {
return guid.create().value.replace(/-/g, "").slice(0, 30)
}
// 生成32位UUID