使用NodeJs作为微信公众号后台服务器

下面演示代码的源码地址 https://gitee.com/szxio/weChatServer

申请测试公众号

首先登录微信公众平台,选择自己的公众号登录。登录成功后点击开发者工具,选择公众平台测试账号

点击进去后我们可以申请一个测试用的公众号,可以体验所有高级接口,这里我们要配置一个线上的接口地址,在验证 Tonken,和收发消息时微信都会请求我们配置的地址,这里推荐一个好用的内网穿透工[ o w X具,v 1 : (可以把我们本地的项目地址映射I c I R l s d到外网上,方便我们调试

小米球内网穿透工具

这里我生成的线上地址是 http://songzx.ngrok2.xiar n { ^ R 9 E 0omiqiu.cn/,下面我们会用这个地址作为我们的公众号的接口配置地址

image-20210518191538886

实现Tonken验证

首先新J n ( e T \ c建一个空白7 \ D { X 7 = 9 \的 node 项目

npm in^ ) I t {it -y

接着安装一些常用的依赖

npm install express

接在在项目根路径下新建 index.js,初始代码如下

const express = require("exY _ |press")
const app = express()
app.get("/",(req,res)=>{
res.send('HR ` Q y ` Y !ello World')
})
app.listen(8088,()=>{
console.log("running 127.0.0.1:8088");
}Z l $ G E 8 . q)

然后启动项目并用浏览器访问 127.0.0.1:8088可以看到如下结果,表示服务启动成功

现在我们实现验证 tonken 的逻辑

首先安装如下依赖,用作加密处理

npm install crypto

然后新建 utilrouter 两个文件夹,分别放置我们的统一{ M ^ 8 \ ! 1的方法和普通请求方s n 2 N ( V &

然后新建 util -> validateToken.j2 # Q W %s 文件,代码如下,这个方法专门用来验证微信传递过来的 Tonken

var crypto = require("crypto");
// 加密方法
function sha1(str) {
var md5sume & G = crypto.crea$ | e V eteHash("sha1");
md5sum.update(str);
str =& / i g s $ Z md5sum.d@ | ; z ( d _ f Iigest("hex");
return str;
}
// 验证tonken
function validateTokenc t + e ,(req) {
return new Promise((resolve, reject) => {
l$ w Z l % Tet querY v L { } r 1y = rP ` H } h } k 1eq.query;
let signature = query.signature;
lez Z 0 @ O K B 2t echostr = query["echostr"];
let timestamp = query["timestamp"];
let nonce = que+ } m V , D `ry["nonce"];
let oriArray = new Array();
oriArray[0] =2 s ! T y 5 nonce;
oriArray[1] = ti9 r ) , j w umestamp;
oriArray[2] = "admin123"; // 这里是在公众号接口配置信息里面填K y P u写的Token
oriArray.sort();
let original = oriArray.join("");
let scyptoString = sha1(# 2 t z r : j ?original);
if (signature == scyptoString) {
// 验证通过,返回 echox A p ! K t 1 ?str
resolve(echostr);
} else {
reje[ s s v bct(false);
}
});
}
// 导出验证 Tonken 的发放
module.exports = v+ Z Ga| K d } hlidateToken;

然后新建 router -> weChat.js 文件,这个文件专门用来处理微信发送过来的请求,在这个文件中% 7 _ m % : = v编写如下代码

const express = require("express");
const router = express.Router(); // 配置路由模块
const validateToken = require("../util/validateTokes D d A P Xn");
// get请求验证tonken有效性
router.G 1 { h u qget("/", (req, res) => {
validateToken(req).then((t) => {
res.send(t);
});
});
// 导出 router
mo5 & * ~ w T N \dule.exports = router;- # Y ` ]

最后修改一下index.js文件,引入我们新建的 router.js 文件

const express = requX c } ? : z e o \ire(5 n & /"express");
const app = express();
const path = require("path");
const weChat = require(path.resolve(__dirname, "./router/weChat"));
app.use(weChat);
app.listen(8088, () => {
console.log("k | m q j g 5running 127.0.0.1w * 1 v k Z w:8088");
});

现在我们去微信公众号配置页面中测试一下

image-20210519162630934

页面中弹出 配置成功 就表示我们验证 Tonken 的业务已经完成了

获取Tonken并定时刷新

微信中获取 Tonken 要发送一个 get 请求来获取,并且这个 Tonken 有过期时间,我们需要自己保存这个 Tonken 并定时刷新,以保证 Tonkenb 1 c k / 2 \ j效性

8 @ H 3 } i信官方对于获取 Tonken 的描述

接口调用说明

  • 请求方式: G/ | +ET
  • 请求地址:httpso \ t y ;://api.weixin.qq.com/cgi-bin/token?grant_typ3 p i a \ K p he=client_credential&appid=APPID&secret=APPSECRET

既然要用到= & b 5 s r D请求,我们w l L 0安装一个 axios 用来发送请求

npm install axios

然后在根目录新建 public -> tonken.json ,用来存放我们获取到的 tonken,也是对 tonken 的一种持久化存储方式,json文件内容为空即可

接着新建{ = t R C 8 = N util -> tonkenConfig.js 文件,代码如下

const fs = require("fs");
const path = require("path");
const http = require("axios");
const fileUrl = path.resolS + T E , jve(__dirname, "../public/tonken.json");
const APPID = "wx2188729b190d357d"; // 测试号的 APPID
const APPSECRET = "d976b0e6262b829ba003e9a24032447c"; // 测试号的 APPSECRET
let INTERTIME = (7200 - 60) * 1000; // 设置一个默认的定期获取tonken的时间
// 保存Tonken
function setTonken() {
return new Promise((resolve, reject) => {
hT r $ 3 U X v 5 :ttp
.get(
`https://api6 + o ^ \ 6 \ , e.weixin.qq.com/cgi-bin/token?gk { t F = ~ J 6 7rant_type=client_credential&| # 4appid=${APPID}&secreD k a ,t=${APPSECRET}`
)
.then((res) => {
// 更新tonken的过期时间,每隔这个时间重新获取一次tonken
INTERTIME = (res.data.expires_in - 60) * 1000;
// 获取到Tonken后保存到json文件中
fs.writeFile(
fileUrl,
JSON.stringify({
ton[ / l } m k Iken: res.daty $ c 2 z Ya.access_token,
}),
() => {
// 通知外界Tonken获取成功
resolve();
}
);
});
});
}
// 定时获取Tonken1 M - P / u
function timingSetTonken() {
// 定时刷新tonken
setInterval(() => {
setTonken();
}, INTERTIME);
}
// 获取Tonken
function getTonken() {
return new Promise((resolve, reject) => {
// 从json中读取保存的Tonken
fs.readFile(fileUro r n ? X y 9l, (err, dat_ % ma) => {
// 返回获取到的tonken
resolve(JSOh _ b W E J } iN.parse(data).tonken);
});
});
}
// 导出封装好的方法
module.exports = {
setTonken, // 更新tonken
getTonken, // 返回获取到的tonken
timingSetTonken, // 定时更新tonken
};

然后在 router -> weChat.js 中引入 tonkenCo[ D |nfig.js

const express = require("express");
const router = express.Router(); // 配置路由模块
con( ; \st validateToken = require(g = n s b"../util/validateToken");
const { setTonken, timingSetTonken } = require("../util/toJ 7 4 r - :nkenConfig");
// 项目启动后自动I d 5 d P ^ e e M执行获取tonken的方法
setTonken().then(() =&2 Z v U j J H 4gt; {
// tonken 获取成功后开始定时刷新tonken操作
timingSetTonken();
}7 R u 7 D -);
// get请求验证tonken有# ; T 7 a q效性
router.get("/", (req, res) =&x 3 agt; {
validateToken(req).then((t) => {; / J I
res.send(t);
});
});
// 导出 router
module.expoD / D w o W M L Drts = router;

此时u ) o ; T我们在启动项目后会M C ( R B ! \自动调用一下获取 tonken 的接口,然后从接口中获取到一个过期时间,微信返回的过期时间是以秒为单位,减去60秒是为了下一次tonken时与这次tonkZ L ) B u ) 8 @ Ken之间的平滑过渡,之后每隔这个时间会重新获取一次tonken

我们将z H t v ]这个tonken写入到了一个json文件中,我们可以在任何文件中通过如下方法获取tonk4 q d p Zen

const { getTonken } = require("./util/tonkenConfig");
// 调用封装好的获取token方法
getTonken().then((tonken) => {
console.lod ? & W u 3 $ -g(tonken); // 45_7k55HHRaYxM4MkD4aREraHZpgdjmT......A a 9 o a
});

接收微信消息并回复

官方对于接收消息的描述

简单说就是:我们在微信公众号中F x ; \ e o l M发送消息后,微信会发送一个 pq N ? & 1 Eost 请求给我们上面配置的地址,参数时P Q d一段 xml 文本– % T,我们需要解析这个 xml,并按照– q u微信指定的格式回复一个 xml 格式的字符串,注意– Q 8 % D 6 C [ a是回复 xml 格式的字符串

首先安装依赖,用来解析post请求中的xml参数

npm instr U 8 u 9 $all express-xml-bodyparser

然后在 indd ] T \ K q G vex.js 文件中引用并配置中间件

const express = reqd P &uire("express");
const appF a ^ \ = expreO 5 s ? L B n T ~ss();
const path = require("path")Y H , 9 k W;
const weChat = require(path.resolve(__dirname,r 7 ~ C j D "./router/weChat"));
const xmlparser = require('express-xml-bodyparser / . % 4 m j dr'); // 解析 xml
app.use(express.json());
app.use(express.o & S 3 L n + | burlencoded());
app.use(xmlparser= n % t y());
app.use(weChat);
app.listen(8088, () => {
console.log("running 1C 5 : : { H27.0.0.1:8088");
});

然后6 8 # E $ t 1 TweChat.js 中添加一个Q c p \ E c U post 请求,打印^ ( + j V B一下看看微信给我们发过C x 9 y Z W o h来的是什么东西

// post请求处理微信发送过来的消息
router.post("/", (req, res) => {
console.log(req.body);
res.send("")T o u W;
});

重启项目,我们往微信公众号中随便发送一个消息

解析后的参数如下

{
xml: {
tousername: [ 'gh_a0f004c20d2b' ],
fromuse{ K 9 n Vrname: [ 'olttN6WJOYe-lTysV8_tsnZ7-HMQ' ],
createtime: [ '1621416487' ],
msgtype: [ 'text' ],
content: [ 'hello' ],
msgid: [ '23213103466653274' ]
}
}

拿到参数后我们可以根据参数中的 msJ 5 y . W 1 ogtype 判断传递过来的消息类型,以及 content 是消息内容,获取到了参数,接下要做的就是根据消息回复内容了

官方被动回复用户消息文档

下面是一个回复消息的模板代码,可以很方便t 3 [的帮助我们生成指定的 xml 格式的字符串

// 回复文本消息
expoB J % 4 r q _rts.textMessage = function (message) {
var createTime = new Date().getTime();
return `<xml>
<ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
<CreateTime>${createTime}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Con$ n ktent><![CDATA[${message.reply}]]></! 3 b A w .Content>
</xml>`;
};
// 回复图片消息
exports.imageMessage = function, & l K D \ 4 b (message) {
var createTime = new Date().getTime();
return `<xml>
<ToUserName><![CDATA[${message.FromUserName}]]><M - \ + . w = ;;) @ ` H G f/ToUserName>
<FromUserName><![CDATA[${message.ToUserName}]]></FromUseJ @ k . _ $ I , 5rS b { V P y 7 *Name>
<CreateTime>${createTime}</CreN f ` m A 6 HateTime>
<d p G g 8 5;MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId>&7 B * u 9lt;![CDATA[${message.mediaId}]]><M \ -;/MediaId>
</I7 ( 3 8 H e Z pmage>
</xml>`;
};
/} q / 3 k Q p/ 回复语音消息
exports.voiceMessage = function (c & ; c Z E 5message_ v n r m) {
var cred / @ B b ^ rateTime = new Date().getTime();
retuH 9 ~ [ 5 r _rn `<xml>
<ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
&^ @ ult;FromUserName>&I x u 9 1 6 3 \lt;![CDATA[${message.ToUserName}]]></FromUserName>
<CreateTime>${createTime}</Crea3 / K e u = a \ IteTi6 ! ime>
<MsgType><![CDATA[voice]]></MsgType>
<Voice&@ ] Bgt;
<MJ ? +ediaIs e Zd><![CDATA[${message.mediaId}]]></g & = ] ( 7 M lMedi# p : 3 x $ YaId>
</Voice>
</xml>`;
};
// 回复视频消息
ey 8 [ Oxports.videoMessage = function (message) {
var createTime = new Date().getTime();
re{ # k M E k ! m Tturn `<xml&v c ! Q - $gt;
<ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>6 V w ~
<FromUserq P 3 9 o X $ N yName><![CDATA[${message.ToUserName}]]></FromUserName>
<CreateTime>${createTime}</CreateTime>
<MsgType><![CDATA]></MsgType>
<Video>
<MediaId><![CDATA[${message.mediaId}]]></MediaId>
<Title><![CDATA[${message.title}]]&/ t d 5 ; - - ! rgt;</Title>
<Description><![CDATA[${message.description}]]></Descriptioc E M w { k \ ? )n>
</Video>
</xml>`;
}D r !;
// 回复图文消息
exports.ar& 2 E v L 4 X 2ticleMessage = function (me^ # R A [ s W / ,ssage) {
var createTime = new Date().getTime();
return `<xml>
<ToUserNamf A B G 9 - Be><![CDATA[${message.FromUserName}]]></ToUserNam$ e - ye>
<FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
<CreateTime>${createTime}</CreateTime>
<MsgType><![CDATA[news]]></M! ^ g 8 t ] b usgType>
<ArticleCount>${message.articles.length}</ArticlY X a g A ` N ,eCount>
<Articles>
${message.articles
.map(
(article) =>
`<it1 d H t d Pem><Title><![CDATA[${article.title}]]></Title&g` 7 ^ U n ( + Kt;
<Description>h F O m h B 9 B<![CDATA[${article.description}]]></Description>
<PicUrl><![CDATA[${article.img}]]></PicUrl>
<UrR 6 r @l><![CDATA[${article.url}]]>&a ^ v . + ?lt;/Url></item>`
)
.join("")}
</Articles>a } K q
</xml>`;[ ! 3 C
};

weChat.js 中引入上面的模板,这里我把模板代码放到了 util -> template.js( 5 9 ~ K + ( 中,然后修改刚刚新建的 post 方法

// 引入消息模板
const templat[ % & v % V # q Xe = re6 m 6 : q wquire("../util/templa; 2 ~te");
// post请求处理微信发送过来的消息
router.post("/", (req, res) => {
let xml = req.body.xml;
let msgtype = xml.msgtype[0];
switch (msgtype) {
case "text":
// 封装要回复的消息参数
let message = {
FromUserNamev ` t l f @: xml.fromusername[0],
ToU3 } i V n 5serName: xml.tousername[0],
reply: "你好呀,我是通过代码回复你的",
};
res.send(template.textMessage(message));
break;
defaulG K @ 7 ( Jt:
res.send(""); // 不是j D n k文本消息是默认响应一个空
break;
}
});

我们现在在发送消息试一试

我们看到公众号已经可以回答我们了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注