背景
本来想用阿里云的云函数 FC,便捷地实现调用微信接口功能的。结果微信要求只有在「IP 白名单」里的服务才能调用 Access Token 的接口。FC 倒是也能绑定固定 IP,但是要开通 NAT 网关和弹性公网 IP,无论包年包月还是按量付费,每年也得两三百块钱。
恰巧笔者有一个最低配置的华为云服务器 ECS,是固定 IP 的,不用白不用,索性就在这上面快速低成本(无任何第三方依赖)的弄一个获取微信 Access Token 的 Nodejs 服务吧。
正文
准备
首先,有个微信公众号,知道自己的开发者ID(AppID)和开发者密码(AppSecret)是必须的。另外,还要把服务器的 IP 填写到白名单里。
然后,服务器要是 Linux 系统的,笔者用的是 Ubuntu 20.04 server 64bit,在服务器里安装 nodejs 是必须得。另外,强烈推荐用 pm2 用来管理服务,每个 nodejs 服务开发者必备技能,没有了解过的强烈建议先去学一下,很简单的。
注意:在本例中,服务器的安全组配置,要在入向放开 3000 端口,否则服务是打不通的。
最后,就是服务代码了,纯原生,无任何第三方依赖,绝对敏捷。为方便阅读,先做一下文件介绍:
index.js- 主逻辑文件,除了起一个 http 服务的模板代码之外,最重要的就是getAccessToken方法,本文所有重点都在这里;utils.js- 为了方便阅读,把次要的发起请求的方法放到这里了,选读即可;token.json- 用来存储 token 和 expireTime 的文件。是的,就是这么简单粗暴。因为用了 pm2,服务是多进程的,所以不能把 token 保存在内存里。既然要敏捷,直接文件走起。
代码
index.js
注意:使用时记得修改 appid 和 secret
// index.js
const http = require('http');
const fs = require('fs');
const path = require('path');
const { serverRequest } = require('./utils');
// https://mp.weixin.qq.com/advanced/advanced
const appid = '<your appid>';
const secret = '<your secret>';
const port = 3000;
let token = null;
let expiryTime = 0;
const tokenFile = path.join(__dirname, 'token.json');
async function getAccessToken() {
// 读取 token 文件,如果存在且未过期,则直接返回缓存的 token
try {
if (fs.existsSync(tokenFile)) {
const tokenData = fs.readFileSync(tokenFile, 'utf-8');
const tokenObj = JSON.parse(tokenData);
if (tokenObj.token && tokenObj.expiryTime - 300 > Date.now()) {
token = tokenObj.token;
console.log('cached token: %s, expiryTime: %s', token, expiryTime);
return token;
}
}
} catch (error) {
console.error('读取 token 文件失败:', error);
}
const result = await serverRequest(
{
hostname: 'api.weixin.qq.com',
path: `/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${secret}`,
method: 'GET',
},
'https',
);
const data = JSON.parse(result);
token = data.access_token;
expiryTime = Date.now() + data.expires_in * 1000;
fs.writeFileSync(tokenFile, JSON.stringify({ token, expiryTime }));
console.log('new token: %s, expiryTime: %s', token, expiryTime);
return token;
}
const server = http.createServer(async (req, res) => {
// res.writeHead(200, {'Content-Type': 'text/plain'});
const token = await getAccessToken();
res.end(token);
});
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
utils.js
// utils.js
const http = require('http');
const https = require('https');
const serverRequest = (options, type) =>
new Promise((resolve, reject) => {
const req = (type === 'https' ? https : http).request(options, (res) => {
let data = '';
// A chunk of data has been received.
res.on('data', (chunk) => {
data += chunk;
});
// The whole response has been received.
res.on('end', () => {
resolve(data);
});
});
// Handle errors.
req.on('error', (error) => {
reject(error);
});
if (typeof options.body === 'string') {
req.write(options.body);
}
// End the request.
req.end();
});
module.exports = {
serverRequest,
};
token.json
1 {"token":"xxxxxxxxxxxxxxxxxxxx","expiryTime":1714227075153}
启动服务
假如以上三个文件都在服务器的 wechat-token/ 目录下,那么只需要运行以下命令,即可启动服务:
$ pm2 start wechat-token/index.js -n wechat-token
然后在浏览器访问你的服务器 IP:3000 查看一下 xxx.xxx.xxx.xxx:3000。如果正常返回 token 了就大功告成了。如果没有正常返回,那么检查一下「安全组」的配置,是否放开了入向的 3000 端口。
结语
如果你已经有了 NAT + 弹性公网 IP,上面的代码完全可以直接用云函数 FC 实现。笔者只是暂时还没有现成的 NAT + IP,如果以后有了,还是会选择把这个服务迁到云函数上的。
另外提一嘴,本来打算用负载均衡 ALB + 云函数 FC,来解决固定 IP 问题的。尝试了一下,发现不行。因为发起请求的是 FC,它的 IP 不是固定的,所以还是过不了 IP 白名单校验这一关。
后续还有给服务绑定域名之类的工作,就不在这里展开了。
最后,付上一些常用的 pm2 命令吧:
# 查看服务 log
$ pm2 log wechat-token
# 查看全部服务
$ pm2 ls
# 重启服务
$ pm2 restart wechat-token
# 停止服务
$ pm2 stop wechat-token
$ pm2 stop all