敏捷快速实现获取微信 Access Token 服务

409 阅读3分钟

背景

本来想用阿里云的云函数 FC,便捷地实现调用微信接口功能的。结果微信要求只有在「IP 白名单」里的服务才能调用 Access Token 的接口。FC 倒是也能绑定固定 IP,但是要开通 NAT 网关和弹性公网 IP,无论包年包月还是按量付费,每年也得两三百块钱。

恰巧笔者有一个最低配置的华为云服务器 ECS,是固定 IP 的,不用白不用,索性就在这上面快速低成本(无任何第三方依赖)的弄一个获取微信 Access Token 的 Nodejs 服务吧。

正文

准备

首先,有个微信公众号,知道自己的开发者ID(AppID)和开发者密码(AppSecret)是必须的。另外,还要把服务器的 IP 填写到白名单里。

image.png

然后,服务器要是 Linux 系统的,笔者用的是 Ubuntu 20.04 server 64bit,在服务器里安装 nodejs 是必须得。另外,强烈推荐用 pm2 用来管理服务,每个 nodejs 服务开发者必备技能,没有了解过的强烈建议先去学一下,很简单的。

注意:在本例中,服务器的安全组配置,要在入向放开 3000 端口,否则服务是打不通的。

最后,就是服务代码了,纯原生,无任何第三方依赖,绝对敏捷。为方便阅读,先做一下文件介绍:

  1. index.js - 主逻辑文件,除了起一个 http 服务的模板代码之外,最重要的就是 getAccessToken 方法,本文所有重点都在这里
  2. utils.js - 为了方便阅读,把次要的发起请求的方法放到这里了,选读即可;
  3. 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