一.验证服务器的有效性
1.安装ngrok实现内网穿透
2.将最外层的包拖拽到控制台使用npm init初始化
3.安装express模块 使用npm i express命令
4.将ngrok的数据写入微信接口信息 使用Webstorm接受消息
//引入express模块
const express = require("express");
//创建app应用对象
const app = express();
//验证服务器有效性
// ngrok http 3000
app.use((req,res,next) =>{
console.log(req.query);
})
//监听端口号
app.listen(3000,() => console.log('服务器启动成功'));
5.验证消息是否来自微信服务器
// 1.验证消息是否来自微信服务器 // 目的:计算得出signature与微信传过来的值相比较 // 1.1将微信加密签名的三个参数(timestamp,nonce,token)按照字典序排序组合在一起成一个数组 // 1.2将数组里面所有参数拼接成一个字符串,进行sha1加密 // 1.3加密完成生成signature,与微信服务器发过来的进行比较 // 如果一样 返回echostr给微信服务器 // 如果不一样说明不是来自微信服务器 返回error
5.1 将定义的配置对象写出来
const config={ //定义配置对象
token:'abc',
appID:'wxb4fd0fa596cad699',
appsecret:'49ebd101f6825cc60f5525975dda960f'
}
// 1.1将微信加密签名的三个参数(timestamp,nonce,token)按照字典序排序组合在一起成一个数组
const arr = {timestamp,nonce,token};
const arrSort=arr.sort(); //字典排序方法
console.log(arrSort);
打开终端窗口安装sha1 再引入sha1模块
// 1.2将数组里面所有参数拼接成一个字符串,进行sha1加密
const str = arr.join(''); // 字符串拼接
console.log(str);
const arrStr = sha1(str); //进行加密
console.log(arrStr);
// 1.3加密完成生成signature,与微信服务器发过来的进行比较
if(arrStr===signature){
// 如果一样 返回echostr给微信服务器
res.send(echostr);
}else{
// 如果不一样说明不是来自微信服务器 返回error
res.end('error');
}
第一步完成效果
//引入express模块
const express = require("express");
//引入sha1模块
const sha1 = require("sha1");
//创建app应用对象
const app = express();
//验证服务器有效性
// ngrok http 3000
const config={ //定义配置对象
token:'abc',
appID:'wxb4fd0fa596cad699',
appsecret:'49ebd101f6825cc60f5525975dda960f'
}
app.use((req,res,next) =>{
// console.log(req.query);
/*{
signature: '964a71830c1839fd177b02b3cf9681f4611c3550', //微信的加密签名
echostr: '8895758246790034999', //微信的随机字符串
timestamp: '1654859299', //微信发送请求的时间戳
nonce: '373056014' //微信的随机数字
}*/
const {signature,echostr,timestamp,nonce}=req.query; //解构赋值
const {token}=config;
// 1.验证消息是否来自微信服务器
// 1.1将微信加密签名的三个参数(timestamp,nonce,token)按照字典序排序组合在一起成一个数组
const arr = [timestamp,nonce,token];
const arrSort=arr.sort(); //字典排序方法
console.log(arrSort);
// 1.2将数组里面所有参数拼接成一个字符串,进行sha1加密
const str = arr.join(''); // 字符串拼接
console.log(str);
const arrStr = sha1(str); //进行加密
console.log(arrStr);
// 1.3加密完成生成signature,与微信服务器发过来的进行比较
if(arrStr===signature){
// 如果一样 返回echostr给微信服务器
res.send(echostr);
}else{
// 如果不一样说明不是来自微信服务器 返回error
res.end('error');
}
})
//监听端口号
app.listen(3000,() => console.log('服务器启动成功'));
二.模块化封装处理
三.获取accessToken
// 整理思路: // 读取本地文件( readAccessToken) // -本地有文件 // 判断它是否过期(isValidAccessToken) // -过期了 // -重新请求获取access_ token(getAccessToken), 保存下来覆盖之前的文件(保证文件是唯一 -的) (saveAccess Token) // -没有过期 // -直接使用 // -本地没有文件 // -发送请求获取access_ token(getAccessToken), 保存下来(本地文件) (saveAccessToken), 直接使用 // get方式请求地址https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
安装request和request-promise-native库(只需要引入request-promise-native库)
1.1模拟测试
//引入config
const {appID,appsecret} = require('../config/config');
//引入request-promise-native库
const rp=require('request-promise-native');
class Wechat{
constructor() {
}
getAccessToken() { //获取access_token
//定义请求地址
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appsecret}`;
//发送请求
rp({method: 'GET', url:url, json: true})
.then(res => { //成功时的回调
console.log(res);
console.log('成功');
})
.catch(err => { //失败时的回调
console.log(err);
})
}
}
//模拟测试
const test = new Wechat();
test.getAccessToken();
1.2getAccessToken完整设置方法
//引入config
const {appID,appsecret} = require('../config/config');
//引入request-promise-native库
const rp=require('request-promise-native');
class Wechat{
constructor() {
}
getAccessToken() { //获取access_token
//定义请求地址
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appsecret}`;
return new Promise((resolve,reject) =>{
//发送请求
rp({method: 'GET', url:url, json: true})
.then(res => { //成功时的回调
console.log(res);
console.log('成功');
// access_token: '57_ZQgtMRumuV5zTyEujjT8K86Xn7eds3CSRmKFEaSHQL7r2xHNpcDWb5-760xWom3He4e_SaPWO00UxE0AAjdmlpDv77P6FrPybFXWGaef4ovG3jZTW1fb
// ZhulAWYG56Bou7Qoh7vLLkddAh3uUYPaABANRI',
// expires_in: 7200 过期时间
//设置access_token的过期时间 -300是提前五分钟 单位是秒所以乘以1000
res.expire_in = Date.now() + (res.expire_in - 300) * 1000;
//将Promise的状态改为成功的状态
resolve(res);
})
.catch(err => { //失败时的回调
console.log(err);
//将Promise的状态改为失败的状态
reject('getAccessToken请求出现了问题'+err);
})
})
}
}
//模拟测试
const test = new Wechat();
test.getAccessToken();
引入fs库模块
完整代码:
// 整理思路:
// 读取本地文件( readAccessToken)
// -本地有文件
// 判断它是否过期(isValidAccessToken)
// -过期了
// -重新请求获取access_ token(getAccessToken), 保存下来覆盖之前的文件(保证文件是唯一 -的) (saveAccess Token)
// -没有过期
// -直接使用
// -本地没有文件
// -发送请求获取access_ token(getAccessToken), 保存下来(本地文件) (saveAccessToken), 直接使用
// get方式请求地址https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
//引入request-promise-native库
const rp=require('request-promise-native');
//引入fs模块
const {writeFile,readFile} = require('fs');
//引入config
const {appID,appsecret} = require('../config/config');
class Wechat{
constructor() {
}
//获取access_token
getAccessToken() {
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appsecret}`;
return new Promise((resolve,reject) =>{
//发送请求
rp({method: 'GET', url:url, json: true})
.then(res => { //成功时的回调
console.log('获取access_token成功');
// access_token: '57_ZQgtMRumuV5zTyEujjT8K86Xn7eds3CSRmKFEaSHQL7r2xHNpcDWb5-760xWom3He4e_SaPWO00UxE0AAjdmlpDv77P6FrPybFXWGaef4ovG3jZTW1fb
// ZhulAWYG56Bou7Qoh7vLLkddAh3uUYPaABANRI',
// expires_in: 7200 过期时间
//设置access_token的过期时间 -300是提前五分钟 单位是秒所以乘以1000
res.expire_in = Date.now() + (res.expire_in - 300) * 1000;
//将Promise的状态改为成功的状态
resolve(res);
})
.catch(err => { //失败时的回调
console.log(err);
//将Promise的状态改为失败的状态
reject('getAccessToken请求出现了问题'+err);
})
})
}
//保存accesstoken
saveAcessToken(accessToken){
return new Promise((resolve,reject) => {
accessToken = JSON.stringify(accessToken) //将对象保存成JSON字符串
//将access保存成一个文件
writeFile("./accessToken",accessToken,err => {
if(!err){
console.log("文件保存成功");
resolve();
}else{
console.log("文件保存失败")
reject("getAccessToken方法出了问题"+err);
}
})
})
}
//读取accesstoken
readAcessToken(){
return new Promise((resolve,reject) => {
readFile("./accessToken",(err,data) => {
if(!err){
console.log("文件读取成功");
//将JSON字符串转化为js对象
data = JSON.parse(data);
resolve(data);
}else{
console.log("文件读取失败")
reject("readAccessToken方法出了问题"+err);
}
})
})
}
//检查accessToken是不是有效的
isValidaccesstoken(data){
//检查传入的参数是否有效
if(!data && !data.accessToken && !data.expire_in){
//代表access_token无效
return false;
}
//检查access_token是否在有效期内
// if(data.expire_in < Date.now()){
// //过期
// return false;
// }else{
// return true;
// }
return data.expire_in > Date.now();
}
}
//模拟测试
const test = new Wechat();
// 读取本地文件( readAccessToken)
// -本地有文件
// 判断它是否过期(isValidAccessToken)
// -过期了
// -重新请求获取access_ token(getAccessToken), 保存下来覆盖之前的文件(保证文件是唯一 -的) (saveAccess Token)
// -没有过期
// -直接使用
// -本地没有文件
// -发送请求获取access_ token(getAccessToken), 保存下来(本地文件) (saveAccessToken), 直接使用
// get方式请求地址https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
new Promise((resolve,reject) => {
test.readAcessToken()
.then(res => {
//本地有文件
// 判断它是否过期(isValidAccessToken)
if(test.isValidaccesstoken(res)){
//有效的
resolve(res);
}else{
//过期了
test.getAccessToken()
.then(res => {
// 保存下来(本地文件) (saveAccessToken), 直接使用
test.saveAcessToken(res)
.then(() => {
resolve(res);
})
})
}
})
.catch(err => {
//本地没有文件
//-发送请求获取access_ token(getAccessToken)
test.getAccessToken()
.then(res => {
// 保存下来(本地文件) (saveAccessToken), 直接使用
test.saveAcessToken(res)
.then(() => {
resolve(res);
})
})
})
})
.then(res => {
console.log("获取成功")
console.log(res);
})
添加fetchAccessToken方法优化代码
// 整理思路:
// 读取本地文件( readAccessToken)
// -本地有文件
// 判断它是否过期(isValidAccessToken)
// -过期了
// -重新请求获取access_ token(getAccessToken), 保存下来覆盖之前的文件(保证文件是唯一 -的) (saveAccess Token)
// -没有过期
// -直接使用
// -本地没有文件
// -发送请求获取access_ token(getAccessToken), 保存下来(本地文件) (saveAccessToken), 直接使用
// get方式请求地址https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET
//引入request-promise-native库
const rp=require('request-promise-native');
//引入fs模块
const {writeFile,readFile} = require('fs');
//引入config
const {appID,appsecret} = require('../config/config');
class Wechat{
constructor() {
}
//获取access_token
getAccessToken() {
const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appID}&secret=${appsecret}`;
return new Promise((resolve,reject) =>{
//发送请求
rp({method: 'GET', url:url, json: true})
.then(res => { //成功时的回调
console.log('获取access_token成功');
// access_token: '57_ZQgtMRumuV5zTyEujjT8K86Xn7eds3CSRmKFEaSHQL7r2xHNpcDWb5-760xWom3He4e_SaPWO00UxE0AAjdmlpDv77P6FrPybFXWGaef4ovG3jZTW1fb
// ZhulAWYG56Bou7Qoh7vLLkddAh3uUYPaABANRI',
// expires_in: 7200 过期时间
//设置access_token的过期时间 -300是提前五分钟 单位是秒所以乘以1000
res.expire_in = Date.now() + (res.expire_in - 300) * 1000;
//将Promise的状态改为成功的状态
resolve(res);
})
.catch(err => { //失败时的回调
console.log(err);
//将Promise的状态改为失败的状态
reject('getAccessToken请求出现了问题'+err);
})
})
}
//保存accesstoken
saveAcessToken(accessToken){
return new Promise((resolve,reject) => {
accessToken = JSON.stringify(accessToken) //将对象保存成JSON字符串
//将access保存成一个文件
writeFile("./accessToken",accessToken,err => {
if(!err){
console.log("文件保存成功");
resolve();
}else{
console.log("文件保存失败")
reject("getAccessToken方法出了问题"+err);
}
})
})
}
//读取accesstoken
readAcessToken(){
return new Promise((resolve,reject) => {
readFile("./accessToken",(err,data) => {
if(!err){
console.log("文件读取成功");
//将JSON字符串转化为js对象
data = JSON.parse(data);
resolve(data);
}else{
console.log("文件读取失败")
reject("readAccessToken方法出了问题"+err);
}
})
})
}
//检查accessToken是不是有效的
isValidAccesstoken(dta){
//检查传入的参数是否有效
if(!data && !data.accessToken && !data.expire_in){
//代表access_token无效
return false;
}
//检查access_token是否在有效期内
// if(data.expire_in < Date.now()){
// //过期
// return false;
// }else{
// return true;
// }
return data.expire_in > Date.now();
}
//获取没有过期的AccessToken
fetchAccessToken() {
if(this.accesss_token && this.expires_in && this.isValidAccesstoken(this)) {
//说明之前保存过access_token,并且他是有效的,直接使用
return Promise.resolve({
access_token:this.accesss_token,
expires_in:this.expires_in
})
}
return this.readAcessToken()
.then(async res => {
//本地有文件
// 判断它是否过期(isValidAccessToken)
if (this.isValidAccesstoken(res)) {
//有效的
// resolve(res);
return Promise.resolve(res);
} else {
//过期了
//-发送请求获取access_ token(getAccessToken)
const res = await this.getAccessToken()
// 保存下来(本地文件) (saveAccessToken), 直接使用
await this.saveAcessToken(res)
//将请求回来的access_token返回出去
// resolve(res);
return Promise.resolve(res);
}
})
.catch(async err => {
//本地没有文件
//-发送请求获取access_ token(getAccessToken)
const res = await this.getAccessToken()
// 保存下来(本地文件) (saveAccessToken), 直接使用
await this.saveAcessToken(res)
//将请求回来的access_token返回出去
// resolve(res);
return Promise.resolve(res);
})
.then(res => {
//将access_token挂载到this上
this.accesss_token = res.accesss_token;
this.expires_in=res.expire_in;
//返回res包装了一层promise对象(此对象为成功的状态)
return Promise.resolve(res);
})
}
}
//模拟测试
const test = new Wechat();
new Promise((resolve,reject) => {
test.readAcessToken()
.then(res => {
//本地有文件
// 判断它是否过期(isValidAccessToken)
if(test.isValidaccesstoken(res)){
//有效的
resolve(res);
}else{
//过期了
test.getAccessToken()
.then(res => {
// 保存下来(本地文件) (saveAccessToken), 直接使用
test.saveAcessToken(res)
.then(() => {
resolve(res);
})
})
}
})
.catch(err => {
//本地没有文件
//-发送请求获取access_ token(getAccessToken)
test.getAccessToken()
.then(res => {
// 保存下来(本地文件) (saveAccessToken), 直接使用
test.saveAcessToken(res)
.then(() => {
resolve(res);
})
})
})
})
.then(res => {
console.log("获取成功");
console.log(res);
})
四.获取用户发送的数据
4.1获取到用户发送的xml数据
新建tool工具包js文件
//工具包函数
module.exports={
getUserDataAsync(req){
return new Promise((resolve,reject) => {
let xmlData = '';
req.on('data',data=>{
//当流式数据传递过来时,会将数据注入回调函数中
console.log(data);
//读取的数据是buffer类型 需要转换成字符串
xmlData += data.toString();
})
.on('end',() => {
//数据接受完毕触发
resolve(xmlData);
})
})
}
}
4.2完善auth模块
//验证服务器的有效性模块
//引入sha1模块
const sha1 = require("sha1");
//引入tool模块
const {getUserDataAsync} = require('../utils/tool');
//引入config模块
const config=require('../config/config');
module.exports= () => {
return async (req, res, next) => {
const {signature, echostr, timestamp, nonce} = req.query; //解构赋值
const {token} = config;
const sha1Str = sha1([timestamp, nonce, token].sort().join(''));
//微信服务器会发送两种类型的消息给开发服务器
//1.get请求 - 验证服务器的有效性
//2.post请求 - 微信服务器会将用户发送的数据以post请求的方式转发到开发者服务器上
if (req.method === 'GET') {
// 1.3加密完成生成signature,与微信服务器发过来的进行比较
if (sha1Str === signature) {
// 如果一样 返回echostr给微信服务器
res.send(echostr);
} else {
// 如果不一样说明不是来自微信服务器 返回error
res.end('error');
}
} else if (req.method === 'POST') {
//微信服务器会将用户发送的数据以post请求的方式转发到开发者服务器上
if (sha1Str != signature) {
//说明消息不是来自微信服务器
res.end('error');
}
//接受请求体中的数据,流式数据
const xmlData = await getUserDataAsync(req);
console.log(xmlData);
//如果开发者服务器没有返回响应给微信服务器,微信服务器会发送三次请求过来
res.end('');
} else {
res.end('error');
}
}
}
完整代码(进一步完善tool和auth模块)
//验证服务器的有效性模块
//引入sha1模块
const sha1 = require("sha1");
//引入tool模块
const {getUserDataAsync,parseXMLAsync,formatMessage} = require('../utils/tool');
//引入config模块
const config=require('../config/config');
module.exports = () => {
return async (req, res, next) => {
const {signature, echostr, timestamp, nonce} = req.query; //解构赋值
const {token} = config;
const sha1Str = sha1([timestamp, nonce, token].sort().join(''));
//微信服务器会发送两种类型的消息给开发服务器
//1.get请求 - 验证服务器的有效性
//2.post请求 - 微信服务器会将用户发送的数据以post请求的方式转发到开发者服务器上
if (req.method === 'GET') {
// 1.3加密完成生成signature,与微信服务器发过来的进行比较
if (sha1Str === signature) {
// 如果一样 返回echostr给微信服务器
res.send(echostr);
} else {
// 如果不一样说明不是来自微信服务器 返回error
res.end('error');
}
} else if (req.method === 'POST') {
//微信服务器会将用户发送的数据以post请求的方式转发到开发者服务器上
if (sha1Str != signature) {
//说明消息不是来自微信服务器
res.end('error');
}
//接受请求体中的数据,流式数据
const xmlData = await getUserDataAsync(req);
// console.log(xmlData);
// <xml><ToUserName><![CDATA[gh_7b0897f3d224]]></ToUserName> //开发者id
// <FromUserName><![CDATA[o6i1N5jHaJKfi9tOCUYcUvq4ec0U]]></FromUserName> //用户open的id
// <CreateTime>1655124555</CreateTime> // 发送的时间戳
// <MsgType><![CDATA[text]]></MsgType> //发送的消息类型
// <Content><![CDATA[233358]]></Content> //发送的消息内容
// <MsgId>23695683538443223</MsgId> //消息id 微信默认保存3天
// </xml>
//将xml数据解析为js对象
const jsData = await parseXMLAsync(xmlData);
console.log(jsData);
// xml: {
// ToUserName: [ 'gh_7b0897f3d224' ],
// FromUserName: [ 'o6i1N5jHaJKfi9tOCUYcUvq4ec0U' ],
// CreateTime: [ '1655126378' ],
// MsgType: [ 'text' ],
// Content: [ '突突突' ],
// MsgId: [ '23695710413710460' ]
// }
//格式化数据
const message = formatMessage(jsData);
console.log(message);
// {
// ToUserName: 'gh_7b0897f3d224',
// FromUserName: 'o6i1N5jHaJKfi9tOCUYcUvq4ec0U',
// CreateTime: '1656656271',
// MsgType: 'text',
// Content: '哈哈哈',
// MsgId: '23717613416201809'
// }
//如果开发者服务器没有返回响应给微信服务器,微信服务器会发送三次请求过来
res.end('');
} else {
res.end('error');
}
}
}
//工具包函数
const {parseString} = require('xml2js'); // 将xml数据转化为js库 2原本是to
module.exports={
getUserDataAsync(req){
return new Promise((resolve,reject) => {
let xmlData = '';
req.on('data',data => {
//当流式数据传递过来时,会将数据注入回调函数中
// console.log(data);
//读取的数据是buffer类型 需要转换成字符串
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("parseXMLAsync方法出了问题"+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 > 0){ 官方的过滤方法
if(Array.isArray(value) && value!=' '){ //自己改写的
//将合法的数据复制到message对象上
message[key] = value[0];
}
}
}
return message;
}
}
五.简单的自动回复 根据文字回复文字
1.设置回复内容
let content = '你在说什么,我不知道';
if(message.MsgType === 'text'){ //判断用户发送的消息类型
if(message.Content === '1'){ //全匹配
content='请不要扣1';
}else if(message.Content.match('520')){ //半匹配
content='520';
}
}
2.auth模块写入xml语句并 注释掉之前回复的空
let replyMessage='<xml>\n' +
' <ToUserName><![CDATA['+message.FromUserName+']]></ToUserName>\n' + //用户id
' <FromUserName><![CDATA['+message.ToUserName+']]></FromUserName>\n' + //开发者id
' <CreateTime>'+Date.now()+'</CreateTime>\n' + //回复的时间戳
' <MsgType><![CDATA[text]]></MsgType>\n' + //回复的类型
' <Content><![CDATA['+content+']]></Content>\n' + //回复的内容
'</xml>';
//
res.send(replyMessage);
//如果开发者服务器没有返回响应给微信服务器,微信服务器会发送三次请求过来
// res.end('');