使用nodejs开发对接微信公众号实现回复

实现效果

使用nodejs开发后台,接收用户发送的信息,并给予回应。

使用工具

  1. vs code
  2. 内网映射工具ngrok,我用的小米球(安装包在文末)
  3. 微信
  4. 微信开发文档 文档地址
  5. 微信开发测试账号 申请地址
  6. 用到的包
npm i express 
npm i sha1
npm i  request-promise-native
npm i xml2js

开发过程

  1. 开启小米球内网映射工具,将本地服务暴露公网,可供微信服务器推送信息。如图:
    在这里插入图片描述
    打开微信申请的微信测试号,在接口配置信息中URL中填入小米球生成的外网地址(以下简称外网ip),将本地127.0.0.0:3000映射到外网该域名ip上,任何人都可以访问
    Token那栏自己自定义即可,主要是为了进行加密解密验证自己服务器与微信服务器的交互。
    在这里插入图片描述
  2. 使用nodejs模块express搭建一个本地服务。使用本地ip和外网ip都可访问。
const express = require('express')
const app = express()
 // 接收处理所有数据
app.use((req,res,next)=>{})
app.listen(3000,()=>{
  console.log('服务启动成功');
})
  1. 验证信息来自于微信服务器
字段含义
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('服务启动成功');
})
  1. 获取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)
      })
  }
}
  1. 接收微信服务器发送过来的信息
    当普通微信用户向公众账号发消息时,微信服务器将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/

  • 2
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值