NodeJS 微信公共号开发 - 获取并缓存 access_token 和 jsapi_ticket

背景

使用 NodeJS 进行微信公共号开发,后端调用各接口时都需使用 access_token,前端调用 js-api 则需要后端根据 jsapi_ticket 生成 signature。
由于 access_tokenjsapi_ticket 都有 7200 秒的有效时间和每日的调用次数上限,所以对两者都需要定时调用,并实现服务器缓存。

修改公众号JS接口安全域名

wt1
进入 设置 => 公共号设置 => 功能设置 来设置JS接口安全域名

wt2
设置JS接口安全域名时需从将微信提供的 txt 文件上传至填写域名或路径下

实现 mongoose 的 model 来存储 access_tokenjsapi_ticket

access_tokenjsapi_ticket 均有 7200 秒的有效时间和每日的调用次数上限,则需要将两者混存到服务器。
本文以 mongoose 存储到 mongodb 作为解决方案,具体 mongoose 的使用会在日后其他博文中进行讨论。model 具体如下:

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
let mongoose = require('mongoose');

let WXAccessTokenSchema = new mongoose.Schema({
accessToken: {
type: String,
required: true,
minlength: 1,
unique: true,
default: 'empty'
},
jsApiTicket: {
type: String,
required: true,
minlength: 1,
unique: true,
default: 'empty'
},
expiresAt: {
type: Date,
required: true,
default: 0
}
});

const WXAccessToken = mongoose.model('WXAccessToken', WXAccessTokenSchema);
module.exports = {WXAccessToken};

实现 access_tokenjsapi_ticket 的获取和更新

首先实现 updateAccessToken 函数从微信获取 access_tokenjsapi_ticket 。先以微信公共号的 id 和 secret 获取 access_token,然后再根据 access_token 获取 jsapi_ticket,在两者皆获取并计算过期时间后使用 mongoose 存储到 mongodb,最后再存储到 process 内。
然后实现 asyncToken 函数通过 mongoose 将 mongodb 内存储的数据获取后,以过期时间作为依据来决定是调用 updateAccessToken 还是更新 process 内数据。
最后再使用 setInterval 进行 asyncToken 的循环调用即可。
具体代码如下

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
58
59
60
61
62
const axios = require('axios');

const {WXAccessToken} = require('../models/wxAccessToken');

async function updateAccessToken() {
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${process.env.WX_ID}&secret=${process.env.WX_SECRET}`;
let resp = await axios.get(url);
console.log('get token from wx');
console.log(resp.data);
if (resp.data.access_token) {
let jsapi_resp = await axios.get(`https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${resp.data.access_token}&type=jsapi`);
if (jsapi_resp.data.ticket) {
let tokenDoc = await WXAccessToken.findOneAndUpdate({}, {
accessToken: resp.data.access_token,
jsApiTicket: jsapi_resp.data.ticket,
expiresAt: +new Date() + parseInt(resp.data.expires_in) * 1000,
}, {
upsert: true,
new: true
});
console.log(tokenDoc);
process.env.accessToken = tokenDoc.accessToken;
process.env.jsApiTicket = tokenDoc.jsApiTicket;
console.log('token updated');
console.log(process.env.accessToken);
}else{
console.log('async jsapi ticket error');
await updateAccessToken();
}
} else {
console.log('async token error');
await updateAccessToken();
}
}

async function asyncToken() {
let tokenDoc = await WXAccessToken.findOne();
console.log('get token from local start');
if (!tokenDoc || (tokenDoc.expiresAt && (tokenDoc.expiresAt - new Date) < 30 * 60 * 1000)) {
console.log('start async token');
await updateAccessToken();
console.log('async token success');
} else {
process.env.accessToken = tokenDoc.accessToken;
process.env.jsApiTicket = tokenDoc.jsApiTicket;
}
}

asyncToken().then(() => {
console.log('get token from local finished');
});

setInterval(async () => {
try {
console.log('check token');
await asyncToken();
console.log('check token finished');
} catch (e) {
console.error('async token error');
console.error(e);
}
}, 10 * 60 * 1000);

生成微信 js 所需的 signature

wx.config 调用时候需要根据 jsapi_ticket 生成 signature,具体接口代码如下:

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
const express = require('express');
const router = express.Router();
const crypto = require('crypto');

function sha1(str) {
let shasum = crypto.createHash("sha1");
return shasum.update(str, 'utf-8').digest("hex");
}

router.get('/', async function (req, res, next) {
try {
let nonceStr = Math.floor(Math.random() * 10000).toString();
let timestamp = Math.floor(+new Date() / 1000);
let url = req.header('Referer');
let str = `jsapi_ticket=${process.env.jsApiTicket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
let signature = sha1(str);
let appId = process.env.WX_ID;
res.sendSuccess({appId, nonceStr, timestamp, signature, url});
// res.send();
} catch (e) {
next(e);
}
});

module.exports = router;

可访问 微信 JS 接口签名校验工具 验证 signature