实现效果
使用nodejs开发后台,接收用户发送的信息,并给予回应。
使用工具
npm i express
npm i sha1
npm i request-promise-native
npm i xml2js
开发过程
- 开启小米球内网映射工具,将本地服务暴露公网,可供微信服务器推送信息。如图:
打开微信申请的微信测试号,在接口配置信息中URL中填入小米球生成的外网地址(以下简称外网ip),将本地127.0.0.0:3000映射到外网该域名ip上,任何人都可以访问
Token那栏自己自定义即可,主要是为了进行加密解密验证自己服务器与微信服务器的交互。
- 使用nodejs模块express搭建一个本地服务。使用本地ip和外网ip都可访问。
const express = require('express')
const app = express()
// 接收处理所有数据
app.use((req,res,next)=>{})
app.listen(3000,()=>{
console.log('服务启动成功');
})
- 验证信息来自于微信服务器
字段 | 含义 |
---|---|
signature | 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数 |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
通过检验signature对请求进行校验。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
const express = require('express')
const sha1 = require('sha1')
const app = express()
const config = {
token: 'shenweijian',
appID: 'wx4abdcadb13793f2d',
appsecret: '85c86c0a7db82db92f4883959cb3a662'
}
// 接收处理所有数据
app.use((req,res,next)=>{
console.log(req.query);
/**这是微信服务器发送过来的数据
signature: '3857fe48e93afdfd0c31e60cd8a32f114afd4967', 微信的加密签名
echostr: '5820879817324877807', //微信的随机字符串
timestamp: '1606633034', 微信发送的时间戳
nonce: '2084380469' 微信的随机数字
*/
const {signature,echostr,timestamp,nonce} = req.query
const {token} = config
const arr=[timestamp,nonce,token]
const arrSort = arr.sort() //字典序排序
console.log(arrSort);
// 拼接成字符串 并进行sha1 加密
const str = arr.join('')
const shaStr = sha1(str)
console.log(shaStr);
// 微信发过来的 与 自己生成的进行对比 相等则来源于微信服务器
if(shaStr === signature) {
res.end(echostr)
}else {
res.end('error')
}
})
app.listen(3000,()=>{
console.log('服务启动成功');
})
- 获取Access_Token
access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。开发者需要进行妥善保存。access_token的存储至少要保留512个字符空间。access_token的有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效。
请求地址:
https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
请求方式:GET
定义一个Wechat类,有获取、保存、读取、判断过期
const rp = require('request-promise-native')
const fs = require('fs')
class Wechat {
constructor() {}
/**
* 获取access_token
*
*/
getAccessToken() {
return new Promise((resolve, reject) => {
//定义url
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appsecret}`
//发送请求
rp({
method: 'GET',
url,
json: true
}).then((res) => {
/**
* {
access_token: '39_PaDlof_zDKvJ_Xe7dednnY4iUsTMKu8ryJmfWurfwVviDn1GT9m8DksCuxSV5VrvL-LDsyiSNkeb7cCHiyofkq7sZiMZnOs8CFb1fagETGyibj1cEXePPraLlVIg--UysAo7XORnDECE14yyGPCeADATCP',
expires_in: 7200
}
*/
console.log(res);
//设置过期时间
res.expires_in = Date.now() + (res.expires_in - 300) * 1000
resolve(res)
}).catch((err) => {
console.log(err);
reject('getAccessToken' + err)
})
})
}
/**
* 保存access_token
*/
saveAccessToken(accessToken) {
accessToken = JSON.stringify(accessToken)
return new Promise((resolve, reject) => {
fs.writeFile('./accessToken.txt', accessToken, (err) => {
if (!err) {
console.log('文件保存成功');
resolve()
} else {
reject('saveAccessToke' + err)
}
})
})
}
/**
* 读取access_token
*/
readAccessToken() {
return new Promise((resolve, reject) => {
fs.readFile('./accessToken.txt', (err, data) => {
if (!err) {
console.log('文件读取成功');
data = JSON.parse(data)
resolve(data)
} else {
reject('readAccessToke' + err)
}
})
})
}
/**
* 判断过期
*/
isValidAccessToken(data) {
//检测传入的参数有效
if (!data && !data.expires_in && !data.access_token) {
return false
}
//检测token是否在有效期内
return data.expires_in > Date.now()
}
/**
* 获取access_token
* */
fetchAccessToken() {
if (this.access_token && this.expires_in && this.isValidAccessToken(this)) {
//说明之前保存过accessToken,直接使用
return Promise.resolve({
access_token: this.access_token,
expires_in: this.expires_in
})
}
return this.readAccessToken()
.then(async res => {
if (this.isValidAccessToken(res)) {
resolve(res)
} else {
const res = await this.getAccessToken()
await this.saveAccessToken(res)
return new Promise.resolve(res)
}
})
.catch(async err => {
const res = await this.getAccessToken()
await this.saveAccessToken(res)
return new Promise.resolve(res)
})
.then(res => {
this.access_token = res.access_token
this.expires_in = res.expires_in
return Promise.resolve(res)
})
}
}
- 接收微信服务器发送过来的信息
当普通微信用户向公众账号发消息时,微信服务器将POST消息的XML数据包到开发者填写的URL上。
if(req.method==='POST'){
//接收到的数据
//验证来自于微信服务器
if(shaStr!==signature){
console.log('error');
}
// console.log(req.query);
//接收请求体中的数据,流式数据
const xmlData = await getUserDataAsync(req)
console.log(xmlData);
/**
* <xml><ToUserName><![CDATA[gh_aa06866ca032]]></ToUserName> //开发者的id
<FromUserName><![CDATA[oACvpwGj6Jc6MhEeH8WNoUysfUrE]]></FromUserName> //用户id openid
<CreateTime>1606646835</CreateTime> //发送时间戳
<MsgType><![CDATA[text]]></MsgType> //发送的类型
<Content><![CDATA[你好]]></Content> //内容
<MsgId>23001653569930368</MsgId> //消息id
</xml>
*/
//解析xml数据
const jsData = await parseXMLAsync(xmlData)
// console.log(jsData);
//格式化数据
const message = formatMessage(jsData)
console.log(message);
let content = '无法识别'
if(message.MsgType==='text'){
if(message.Content==='1'){
content = '1的自动回复'
}else if(message.Content==='2'){
content = '2的自动回复'
}else if(message.Content.match('3')){
content = '3的自动回复'
}
}
const resMessage=`<xml>
<ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
<FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
<CreateTime>${Date.now()}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[${content}]]></Content>
</xml>`
//如果开发者服务器没有返回响应给微信服务器,微信服务器会发送3次请求过来
res.send(resMessage)
封装好的方法一些转换方法
const { parseString } = require('xml2js')
module.exports = {
getUserDataAsync(req) {
return new Promise((resolve, reject) => {
let xmlData = ''
req.on('data', data => {
//
console.log(data);
xmlData += data.toString()
})
.on('end', () => {
//当数据接收完毕,会触发当前函数
resolve(xmlData)
})
})
},
parseXMLAsync(xmlData) {
return new Promise((resolve, reject) => {
parseString(xmlData, { trim: true }, (err, data) => {
if (!err) {
resolve(data)
} else {
reject('parseXMLtoJS' + err)
}
})
})
},
formatMessage(jsData) {
let message = {}
//获取xml对象
jsData = jsData.xml
//判断是否是一个对象
if (typeof jsData === 'object') {
//遍历对象
for (let key in jsData) {
let value = jsData[key]
if (Array.isArray(value) && value.length > 0) {
message[key] = value[0]
}
}
}
return message
}
}
最后效果
小米球下载地址:
链接:https://pan.baidu.com/s/1Aub_zTCZtCy5FovQWiWa_g
提取码:6e8j
个人站点:https://yuanxiaoshen.com/