November 15, 2020

使用egg搭建微信公众号开发,转发流量到本地服务

最近在折腾一下微信公众号的开发,在这个过程中还是遇到挺多麻烦的事,当然,每一件麻烦的事情,在 goole 下都能找到对应的解决方案,这次是把整个过程记录一下,从开始到放弃。

这次文章的目标是,在本地能够启动 node.js 服务,并能够接收用户发送给公众号的消息,进行响应处理。

1. 申请公众号

(废话,下一个)

2. 搭建本地 node.js 开发

最开始的时候,是选中 koa 来作为本地开发的,因为研发手写的代码简单,就那么几行,就可以在一个完全空白的项目跑起来了。但是后面发现,koa虽简单,但是功能也比较简单,如果要进行完整的服务端开发,还需自己搭建非常多的内容。于是选中 egg 作为开发的框架。

1
2
3
4
5
# 下面命令是初始化 egg 相关项目,并启动
$ mkdir egg-example && cd egg-example
$ npm init egg --type=simple
$ npm i
$ npm run dev

运行完上面的命令之后,打开本地链接 http://localhost:7001 即可看到 egg 启动的界面了。

要让微信公众号的信息正确发到本地服务,这些东西还不够,要暴露一个外部的 ip,让微信把公众号接收到的消息发送到这个 外部 ip。这个时候需要一个代理工具:localtunnel

3. 使用 localtunnel 代理请求

localtunnel 可以把本地的接口,生成一个暴露对外的域名地址;当外部进行发送消息的时候,把发送到该域名的地址,转发到本地的服务。简单如图:

image.png

下面是安装的简单过程

1
2
3
4
5
6
# 也可以使用 npx localtunnel --port 7001,不过通常后续会多次执行,还是全局安装之后,后面执行起来比较快
$ npm install -g localtunnel
# 7001 这个端口就是使用 egg 启动占用的端口
$ lt --port 7001
# 执行完命令,会返回url,例如下面的地址。如果处理出错,重复多两次一般就可以
your url is: https://rude-robin-7.loca.lt

拿到这个url之后,就需要复制到浏览器打开,通常会出现下面的界面:

image.png

然后点击按钮,就能看到访问http://localhost:7001内容一致。后续刷新,就不会出现以上界面,而是直接返回本地服务的内容。

4. 公众号后台配置localtunnel域名

执行完上一步之后,本地开发的服务,就可以通过域名访问了。然后去到微信公众号后台修改配置,没有开启,则需要开启:开发 => 基本配置 => 服务器配置。

image.png

URL 那一栏就填写通过 localtunnel 获取的域名;Token 那一栏现在可以随便填一下,具体后面的使用待会说到。然后点击,提交,会发现,出错。因为我们的服务还没有按照微信的要求,对请求进行验证。

5. 对公众号后台配置的服务进行验证

微信要求设定的URL需要进行验证,否则不能作为公众号接收的服务,简要的验证逻辑如下。官方对于验证的文档在这里

image.png

知道验证的方法之后,就可以着手进行处理,就按照初始化的 egg 文件内容来处理,修改 app/controller/home.jsindex() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 需要依赖该加密工具
const sha1 = require('sha1')

class HomeController extends Controller {
  async index() {
    const { ctx } = this;
    const { query } = ctx
    // 在微信公众号后台的配置
    const token = 'testtoken'
    const { signature, echostr, timestamp, nonce } = query

    if (!echostr) return

    const newSignature = sha1([token, timestamp, nonce].sort().join(''))

    ctx.body = newSignature === signature
      ? echostr
      : 'error'
  }
}

确认服务重启之后,回去到公众号后台配置的地方,再次点击“提交”。什么?还是出错,那大概率是 localtunnel 的问题,因为这个服务不太稳定,导致微信那边访问也会有波动。点多两次提交就可以了。

6. 接收微信公众号的信息

当我们发送消息到公众号的时候,公众号后台会对信息进行处理,然后以一个 post 方法的 http 请求,发送到刚才在公众号后台配置的链接中,请求的内容是以 xml 为格式的内容。官方关于接收消息文档如下

上面我们的 egg 应用只是对 GET 请求进行处理,还没有对 POST 请求进行响应,所以做了以下两步处理:

1
2
3
4
5
6
7
// app/router.js 新增处理
module.exports = app => {
  const { router, controller } = app;
  router.get('/', controller.home.index);
  // 下面这一行为新增
  router.post('/', controller.home.post);
};

在官方文档中可以知道,需要对 XML 的文件格式进行处理,而 POST 请求是基于事件,类似流式接收二进制数据;且针对 XML,需要 xml2js 的工具,进行转换为常用的 js 对象。

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
const xml2js = require('xml2js');

class HomeController extends Controller {
  // 为之前处理 GET 请求的方法
  async index() {}

  async post() {
    let data = '';
    const { req } = this.ctx;

    // 马上对响应进行返回,让公众号后台能够收到请求已收到,避免公众号后台进行重试
    this.ctx.body = 'success';

    req.setEncoding('utf8');
    req.on('data', function (chunk) {
      data += chunk;
    });
    req.on('end', function () {
      xml2js.parseString(data, { explicitArray: false }, function (err, json) {
        if (err) {
          // TODO
          return
        }
        // 拿到后台发送过来的数据
        console.log(json.xml)
      });
    });
  }
}

以上的两个文件更改,即可接收 POST 请求了;但是 egg 默认对 POST 请求做了 csrf 防御的处理,如果需要快速解决这个问题,可以先把配置关掉:

1
2
3
4
5
6
7
// /config/config.default.js
const config = exports = {
  // ...
  security: {
    csrf: false
  },
}

完成以上步骤之后,用户发送到微信公众号的数据,本地服务基本就可以接收到了。

优化版

为什么会有优化版?因为 localtunnel 实在太不稳定了,经常会掉线;掉线之后,重新获取域名之后,域名再次发生更改,还需要在公众号后台更改对应的域名。

所以优化版是针对有远端服务器进行处理,代理部分,从 localtunnel 改为 云服务器

image.png

接下来,需要对原来 localtunnel 实现转发这一步,改为我们手动来实现。而有一个需要注意的是,微信后台配置的链接,只能配置 http/https,分别对应 80/443 端口。因为没有证书,所以只能够在 80 端口下手了。而 80 端口,第一时间想到 nginx。通常来说,使用 nginx 进行反向代理,通过一定的规则,把访问 80 端口的一部分流量代理到其他端口。

另外,为了把线上的流量代理到本地,可以使用 ssh 的功能进行转发。ssh 命令为:

1
$ ssh -NfR 8080:localhost:7001 username@48.50.50.50 -i ./my.pem

上面这句命令的意思是,把服务器端口为 8080 的流量代理到本地 7001 端口,i- ./my.pem 是使用对应的密钥进行登录;如果没有,则使用密码进行处理。

为什么是 8080 端口,而不是 80 端口呢。因为 80 端口已经被 nginx 占用了。会提示出错。所以,还需要配合ngxin,把针对微信的流量进行转发,更改配置如下:

1
2
3
4
5
6
7
8
9
# /etc/nginx/sites-enabled/default
server {
  # 其他配置...

  # 把微信的请求,代理到 8080 端口
  location /wxapi/ {
    proxy_pass http://127.0.0.1:8080/;
  }
}

这个时候,就需要更改公众号后台的开发者接口配置

image.png

上面填写的 URL 的ip地址就是云服务器的地址,/wxapi/ 就是 ngxin 设置的地址。

所以总体来说,自己搭建代理的方式如下:

image.png

通过这个优化之后,后续只需要本地启动 egg 服务,然后至少上述 ssh 的命令即可;ssh 代理命令也可以设置为开机启动,那样子就可以再继续减少一步。

结尾

整体流程其实并不复杂,但是需要把各个知识点都串联起来,各种工具、配置的简要知识都需要了解一下。完。