目录
1-1)解决80端口正在被nginx服务的端口占用情况,完成服务端验证
2-1)在express项目中新增一个监听 post 请求的方法来获取用户消息
1、配置服务器并填写相关信息
接入微信公众号的第一步,首先要登录微信公众平台官网,在公众平台官网左侧导航栏的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey。
其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。
当填写好以上的信息后,微信服务器将发送GET请求到填写的服务器地址URL上,请求会携带以下的参数:
开发者通过下方的检验方式对请求进行校验。若确认此次GET请求来自微信服务器,则原样返回echostr参数内容,便接入成功,否则接入失败。加密/校验流程如下:
1)将token(就是我们设置的token)、timestamp、nonce三个参数进行字典序排序;
2)将三个参数字符串拼接成一个字符串进行sha1加密;
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信;
1-1)解决80端口正在被nginx服务的端口占用情况,完成服务端验证
根据官方文档给出的校验方法和示例,完成 express 的校验服务,并进行部署:
const express = require('express');
const crypto = require('crypto');
app.get('/wechat-test', (req, res)=>{
const token = 'xxx_xxx';
let {signature, timestamp, nonce, echostr} = req.query;
let arr = [nonce, timestamp, token];
arr.sort(); // 排序
let str = arr.join(''); // 转成字符串
let shanum = crypto.createHash('sha1');
let mysignature = shanum.update(str).digest('hex');
let result = (mysignature === signature);
if(result){
res.send(echostr);
}else{
res.send();
}
});
app.listen(80, ()=>{
console.log('80 listening');
})
填写 http 协议,80 端口的服务器地址。 将上面的 express 服务端程序部署到服务器中,试运行:
cd 项目部署的目录下
pm2 start index.js --name wechat-test
发现报错:EADDRINUSE,即该80端口已经被占用。查看80端口的占用情况:
sudo lsof -i :80
发现80端口nginx正在监听并提供服务:
为了不破坏主页面原有的服务,选择使用请求转发的方式,将express项目的监听端口设置为3001,并在控制台安全组中开放该端口,在nginx中配置将80的访问转发到3001端口:
server{
listen 80;
......
# 请求转发到本地的3001端口进行处理
location /wechat-test {
proxy_pass http://localhost:3001;
}
}
请求 http://www.example.cn/wechat-test 将转发到 http://www.example.cn:3001/wechat-test。
重新在服务器中运行express服务,运行成功后在微信提交服务器配置,微信会get请求到目标URL进行验证,验证成功后即显示接入成功,选择启动服务器配置:
2、接收用户的消息
当普通微信用户向公众账号发消息时,微信服务器将发送一个post请求到我们填写的URL地址中,并携带 XML 数据(包含用户输入的消息和一些其他的信息)。
a. 文本消息XML:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[this is a test]]></Content>
<MsgId>1234567890123456</MsgId>
<MsgDataId>xxxx</MsgDataId>
<Idx>xxxx</Idx>
</xml>
其中:
toUserName | 开发者微信号 |
FromUserName | 发送方账号(openid) |
CreateTime | 消息创建的时间 |
MsgType | 消息的类型,如果是文本即为text |
Content | 文本消息的内容 |
b.图片消息:
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>1348831860</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<PicUrl><![CDATA[this is a url]]></PicUrl>
<MediaId><![CDATA[media_id]]></MediaId>
<MsgId>1234567890123456</MsgId>
<MsgDataId>xxxx</MsgDataId>
<Idx>xxxx</Idx>
</xml>
其中:
PicUrl | 由系统生成的图片链接 |
MediaId | 图片消息媒体id,可以调用获取临时素材接口拉取数据 |
2-1)在express项目中新增一个监听 post 请求的方法来获取用户消息
直接添加到原来的项目文件中即可。
// 监听的路径要和填写的URL路径一致
app.post('/wechat-test',(req, res)=>{
// 用来保存xml数据结果
let data = '';
// 接收xml数据
req.on('data',(chunk)=>{
data += chunk;
})
req.on('err',(err)=>{
res.send('');
})
req.on('end',()=>{ // 数据接收完毕
console.log('数据接收完毕\n', data);
res.send();
})
})
在公众号中发送消息:1234。使用 pm2 logs 查看运行日志,查看项目的输出内容:
此时在微信公众号中看不到任何来自公众号的回应。
2-2)使用 xmljson 解析和处理用户消息
下载 xmljson :npm i xmljson;
基本使用,将 xml 内容转换成对象的形式:
// 直接引入一个方法,将xml内容转换成对象
const toJson = require('xmljson').to_json;
app.post('/wechat-test',(req, res)=>{
let data = '';
req.on('data', chunk=>{
data += chunk;
})
req.on('err',(err)=>{
res.send('');
})
req.on('end',()=>{
console.log('接收完毕:\n',data,'\n\n');
toJson(data, (err, xmlObj)=>{
if(err){
console.log('转换错误');
res.send('处理错误');
}else{
console.log('转换完成:\n', xmlObj);
res.send('');
}
})
})
})
在公众号中发送消息:你好。
转换后的对象又包含一个 xml 对象,该对象中的内容即为 “xml标签名:标签体内容”的形式,我们要得到用户的消息,只需要 xmlObj.xml.Content 即可。
3、回复用户的消息-文本回复
回复的内容也需要是XML格式的,如:
其中,Content 中的内容为回复的消息内容。ToUSerName 和 FromUserName 都是必须携带的内容。
下面这个示例将回复用户:`你好,${用户发送过来的消息}`;
app.post('/wechat-test',(req, res)=>{
let data = '';
req.on('data',(chunk)=>{
data += chunk;
}).on('end',()=>{
toJson(data, (err, obj)=>{
if(err){
console.log('错误:',new Date().toLocaleString());
res.send(`
<xml>
<ToUserName><![CDATA[${obj.xml.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${obj.xml.ToUserName}]]></FromUserName>
<CreateTime>${new Date().getTime()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[处理错误]]></Content>
</xml>
`)
}else{
let ans = `
<xml>
<ToUserName><![CDATA[${obj.xml.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${obj.xml.ToUserName}]]></FromUserName>
<CreateTime>${new Date().getTime()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好,${obj.xml.Content}]]></Content>
</xml>
`
res.send(ans);
}
})
})
})
在公众号发送消息,查看回复:
注意,服务器需要在5s内做出回复,否则微信服务器将在5s后断掉连接,并重新发送请求,总共会发送3次请求,若服务器都没能按时回复,则会按超时处理。
如果服务器无法保证在5秒内做出回复,可以考虑直接回复success,或返回一个空字符串(则什么都不回复)。
如果服务器没有在5s内返回数据,或返回的数据不是xml格式的,微信都会在公众号对话中向用户发送系统提示消息“该公众号暂时无法提供服务,请稍后再试”。
4、回复用户的内容-图片
4-1)到公众号的素材库中上传图片:
4-2) 获取 access-token
获取素材库中的素材需要使用到 access-token ,可以发送以下的请求来后期 access-token:
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
如果要获取的是 access-token 则 grant_type 填写 client_credential;
appid 和 secret 可以到公众号-基本配置中配置并获取:
请求成功后,在res.data 中会有以下的信息,其中expires_in 是有效时间s:
参考:微信开放文档
4-3)获取公众号中的图片资源(永久素材列表)
获取永久素材列表可以发送以下请求:
post
https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=ACCESS_TOKEN
access-token 即刚刚获取到的access_token
需要携带的data:{
"type":"image", // 表示请求图片资源,还可以是视频(video)、语音 (voice)、图文(news)
"offset":0, // 从全部素材的该偏移位置开始返回,0表示从第一个素材 返回
"count":5, // 表示返回的数量在 1-n 之间,当前为 1-5
}
一个示例:
// 处理图片类型的消息-返回固定的图片
function dealImgMsg(xmlObj, res){
// 先获取 access_token
axios.get('https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${YOU_APPID}&secret=${YOU_SECRET}').then((result)=>{
// 提取出来的 access_token
let access_token = result.data.access_token;
reqData = {
'type':'image',
'offset':0,
'count':5
}
// 再请求获取图片类型的资源
axios.post(`https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=${access_token}`,reqData).then(result1=>{
let data = result1.data;
// 图片资源存放在data.item 中 ,查看结果
console.log(data.item, data.item[0]);
res.send();
})
})
}
输出的内容(media_id 即我们需要使用到的内容):
参考:微信开放文档
4-4)返回一个图片给用户作为回复
const axios = require('axios');
app.post('/wechat-test',(req, res)=>{
let data = '';
req.on('data',(chunk)=>{
data += chunk;
}).on('end',()=>{
toJson(data, (err, result)=>{
let xmlObj = result.xml;
if(err){
console.log('错误:',new Date().toLocaleString());
res.send()
}else{
switch (xmlObj.MsgType) {
case 'image':
dealImgMsg(xmlObj, res);
break;
case 'text':
dealTextMsg(xmlObj, res);
break;
default:
res.send()
}
}
})
})
})
// 处理图片类型的消息-返回固定的图片
function dealImgMsg(xmlObj, res){
axios.get(`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${YOU_APPID}&secret=${YOU_SECRET}`).then((result)=>{
let access_token = result.data.access_token;
reqData = {
'type':'image',
'offset':0,
'count':5
}
axios.post(`https://api.weixin.qq.com/cgi-bin/material/batchget_material?access_token=${access_token}`,reqData).then(result1=>{
let data = result1.data;
let resXML = `
<xml>
<ToUserName><![CDATA[${xmlObj.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${xmlObj.ToUserName}]]></FromUserName>
<CreateTime>${new Date().getTime()}</CreateTime>
<MsgType><![CDATA[image]]></MsgType>
<Image>
<MediaId><![CDATA[${data.item[0].media_id}]]></MediaId>
</Image>
</xml>
`
res.send(resXML);
})
})
}
// 处理文字类型的消息-返回用户发送来的消息加自定义的内容
function dealTextMsg(xmlObj, res){
let resXML = `
<xml>
<ToUserName><![CDATA[${xmlObj.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${xmlObj.ToUserName}]]></FromUserName>
<CreateTime>${new Date().getTime()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好, ${xmlObj.Content}]]></Content>
</xml>
`;
res.send(resXML);
}