使用NodeJs开发微信公众号

2,532 阅读7分钟

前言

为什么我们需要去了解微信公众号开发流程?如今在公司写业务时或多或少都接触过微信公众号、企业微信、钉钉等H5应用开发,但在开发的时候我们通常都是如下三步骤:

  • 引入js文件,在需要调用JS接口的页面引入JS-SDK文件
  • 通过config接口注入权限验证配置
  • 通过ready、error接口分别处理成功、失败验证 在和微信对接的过程我们很少去关心服务端都做了那些工作,以至于我们平常谈起公众号总是模模糊糊不是很清晰,今天我们就使用nodejs来彻底打通微信公众号的前后端对接,从而熟悉微信公众号开发的整体流程

准备

**1. 微信公众号:**公众号可以在微信公众平台去申请,本次开发使用的是普通订阅号,微信公众号分为普通订阅号、认证订阅号、普通服务号、认真服务号、区别就是不同的公众号类型具备不同的接口权限、比如微信支付必须是认证过的服务号才可以使用,其它的权限说明可以查看权限官方文档

**2. 服务器:**由于我们的服务器和微信服务器进行交互,那么服务器肯定是必不可少的,大家可以自行购买阿里或者腾讯服务器,如果不想花钱也开始使用一些内网穿透工具,将本地的IP暴露到公网中,常用的有NgrokFrp等等,具体的使用可以自行点击了解,不过大多是这种工具都不是特别稳定,还是推荐大家购买一台服务器比较方便,说不准以后公众号运营的比较好了有了一定的粉丝,这些都是小意思🙈

3. 域名:域名就不具体阐述了,大家可以找一些域名厂商购买,值得注意的是,有了域名以后能备案就尽早备案,因为有的时候域名是需要备案的

接入

准备工作我们做完以后,紧接着就是如何接入了

**1. 填写服务器配置:**登录微信公众平台官网后,在公众平台官网的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。

**2. 验证服务器地址的有效性:**配置完以后,点击提交,微信服务器将发送GET请求到填写的服务器地址URL上,会携带signature、timestamp、nonce、echostr,我们收到请求以后需要做的就是如下:

  • 将token、timestamp、nonce三个参数进行字典序排序
  • 将三个参数字符串拼接成一个字符串进行sha1加密
  • 将加密后的字符串与signature对比、如果相同、标识该请求来源于微信、我们原样返回echostr参数,这样接入验证就成功了,完整接入代码如下:
const Koa = require('koa')
const crypto = require('crypto')
const config = require('./config/wx')
const app = new Koa()

app.use(async (ctx, next) => {
    const method = ctx.method
    const signature = ctx.query.signature
    const timestamp = ctx.query.timestamp
    const nonce = ctx.query.nonce
    const echostr = ctx.query.echostr
    // token就是我们配置时的token
    const token = config.token
    // 将token、timestamp、nonce三个参数进行字典序排序
    const str = [token, timestamp, nonce].sort().join('')
    // 将三个参数字符串拼接成一个字符串进行sha1加密
    const shaStr = crypto.createHash('sha1').update(str).digest('hex')
  if (method === 'GET') {
    // 将加密后的字符串与signature对比、如果相同、标识该请求来源于微信、我们原样返回echostr参数
    if (shaStr === signature) {
      ctx.body = echostr
    } else {
      ctx.body = '服务器验证失败'
    }
  }
})
app.listen(7000)

代码编写完成以后,我们点击提交,如果接入成功就会出现如下提示:

至此我们就完成了跟微信服务器认证的过程,接下来我们就编写一些业务代码,比如有用户主动关注我们,我们回复一条文本消息

关注自动回复

当用户在关注时,我们是如何接收到这些信息的呢?答案是微信会把这个事件推送给我们,方便我们给用户下发欢迎回复,但是这里有一个点需要注意的就是,微信给我们返回的数据是XML格式的数据包,那我们该如何来处理呢,大概分为如下几个步骤:

  1. 处理POST类型的控制逻辑,取出原始数据,接受微信给我们推送的XML的数据包
  2. 解析数据包,将xml解析为json
  3. 将我们要回复的消息包装成XML的格式
  4. 在五秒钟之内给返回(微信服务器在五秒之内如果收不到响应会断掉链接,并且重新发送请求,总共重试三次)
const Koa = require('koa')
const crypto = require('crypto')
const getRawBody = require('raw-body')
const xml2js = require('xml2js')
const config = require('./config/wx')

const app = new Koa()

const parseXml = xml => {
  return new Promise((resolve, reject) => {
    xml2js.parseString(xml, { trim: true }, (err, data) => {
      if (err) {
        return reject(err)
      }
      resolve(data)
    })
  })
}

// 将xml2js解析出来的对象转换成直接可访问的对象
const formatMessage = result => {
  const message = {}
  if (typeof result === 'object') {
    for (let key in result) {
      if (!Array.isArray(result[key]) || !result[key].length) {
        continue
      }
      if (result[key].length === 1) {
        const val = result[key][0]
        if (typeof val === 'object') {
          message[key] = formatMessage(val)
        } else {
          message[key] = (val || '').trim()
        }
      } else {
        message[key] = result[key].map(item => formatMessage(item))
      }
    }
  }
  return message
}

app.use(async ctx => {
  const method = ctx.method
  const signature = ctx.query.signature
  const timestamp = ctx.query.timestamp
  const nonce = ctx.query.nonce
  const echostr = ctx.query.echostr

  const token = config.token
  const str = [token, timestamp, nonce].sort().join('')
  const shaStr = crypto.createHash('sha1').update(str).digest('hex')

  if (method === 'GET') {
    if (shaStr === signature) {
      ctx.body = echostr
    } else {
      ctx.body = '服务器验证失败'
    }
  } else if (method === 'POST') {

    // 判断签名值是不是合法的
    if (shaStr !== signature) {
      ctx.body = '服务器验证失败'
    }

    // 取原始数据
    const xml = await getRawBody(ctx.req, {
      length: ctx.request.length,
      limit: '1mb',
      encoding: ctx.request.charset || 'utf-8'
    })

    // 解析xml
    const result = await parseXml(xml)

    // 将xml解析为json
    const message = formatMessage(result.xml) 
    
    // 关注回复消息
    if (message.MsgType === 'event') {
      if (message.Event === 'subscribe') {
        ctx.body = `
          <xml>
            <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName>
            <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName>
            <CreateTime>${new Date().getTime()}</CreateTime>
            <MsgType><![CDATA[text]]></MsgType>
            <Content><![CDATA[Hello,欢迎关注富小国!]]></Content>
          </xml>
        `
      }
    }
  }
})

app.listen(7000, () => {
  console.log('Server runing at 7000')
})

通过上面的代码,我们就完成了关注自动回复的功能,这时就可以来测试啦,正常情况下微信会返回我们配置的如下信息:

当然了这里只是做了关注自动回复的功能,还有一些其它的例如图片消息、语音消息、视频消息等等一系列的消息,我在这里就不一一举例了,原理都是一样的。

调用微信接口

在调用微信接口之前,有一个重要的点就是调用微信的所有接口都需要一个access_token,access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token,有效期目前为2个小时,需定时刷新,重复获取将导致上次获取的access_token失效,具体官方说明请点击这里,那么如何来获取呢,来看下面一段代码:

const redis = require('redis')
const promise = require('bluebird')
const request = promise.promisify(require('request'))

const redisConfig = {
  host: 'localhost',
  port: 6379
}
const key = 'access_token'

const redisClient = redis.createClient(redisConfig)
const getAsync = key => {
  return new Promise((resolve, reject) => {
    redisClient.get(key, (err, data) => {
      if (err) {
        reject(err)
      }
      resolve(data)
    })
  })
}
// 获取
const getAccessToken = async () => {
  const accessToken = await getAsync(key)
  if (!accessToken) {
    return updateAccessToken()
  }
  return accessToken
}
// 更新
const updateAccessToken = async () => {
  const { appId, appSecret } = config
  const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
  const { body } = await request({ url: url, json: true })
  const { access_token: accessToken } = body
  await redisClient.set(key, accessToken, 'EX', 7000)
  return accessToken
}

module.exports = getAccessToken

这里我们做的就是通过微信提供的access_token接口,获取access_token,参数有三个,一个是grant_type,这个是一个固定的值client_credential,appId和appSecret分别是微信公众平台配置的appid和secret,拿到以后把他存到redis里面(当然存到任意一个地方都是可以,这里我们就模拟一下真实场景,直接存到redis里面),设置一个过期时间(官方文档给的access_token是7200秒,这里我们设置7000的时候就去获取),下次取的时候如果没有过期我们就从redis直接取出来,如果过期从新获取,下面是部署到服务器成功的log。

获取用户列表

上面我们完成了对accesstoken的获取,这时候我们就可以调用微信的任意接口了,来看一下官方给出的获取用户列表的接口文档

文档其实说的很详细了,那我们就直接来上代码:

app.use(async (ctx, next) => {
  try {
    const accessToken = await accessToken()
    const url = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${accessToken}`
    const { body: users } = await request({ url, json: true })

    console.log('users: ', users);
  } catch (error) {
    console.log('获取用户列表失败')
  }
  await next()
})

这段代码非常简单,相信大家都能看明白,调用其他的微信接口的方式都是大同小异的,这里就不一一举例了。

网页授权前端JS-SDK的调用

JS-SDK调用的最重要一步就是生成签名signature,但是在生成签名之前必须了解一下jsapi_ticket,jsapi_ticket是公众号用于调用微信JS接口的临时票据。正常情况下,jsapi_ticket的有效期为7200秒,通过access_token来获取,其实ticket跟accesstoken非常类似,而且有效时间也一样,所以我们可以直接通过上面获取accessToken的方式来获取ticket,这里我们就不上具体的代码了,重点说一下生成签名(假设我们已经拿到了ticket),来看下面一段代码:

router.get('/signature', async (ctx, next) => {
  const jsApiTicket = await getJsApiTicket()

  const nonceStr = Math.random().toString(36).substr(2, 15)
  const timestamp = `${parseInt(new Date().getTime() / 1000)}`  
  const url = ctx.request.url

  // 对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1:
  const str = `jsapi_ticket=${jsApiTicket}&nonceStr=${nonceStr}&timestamp=${timestamp}&url=${url}`
  // 对string1进行sha1签名,得到signature:
  const signature = crypto.createHash('sha1').update(str).digest('hex')

  ctx.body = {
    signature,
    timestamp,
    nonceStr,
    appId,
  }
})

签名生成规则如下:参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。这里需要注意的是所有参数名均为小写字符。对string1作sha1加密,字段名和字段值都采用原始值,不进行URL 转义,这里我们就生成了签名,并且返回给了前端,接下来就是前端的调用,也就是我们开篇所讲的三步骤

axios.get('/signture').then(res => {
  wx.config({
    debug: true,
    timestamp: res.timestamp,
    signature: res.signature,
    nonceStr: res.nonceStr,
    appId: res.appId,
    jsApiList: ['chooseImage'],
  })

  wx.ready(() => {
    wx.chooseImage({
      // do something
    })
  })
}).catch(error => {
  console.log(error.message || '获取签名错误')
})

总结

今天我们通过配置到最后前端的接入,讲解了一下微信开放平台的基础常见功能,相信大家对微信的开发也有所了解了,如果你正在开发微信公众号,希望可以帮助到你,我们下期再会。