前言
为什么我们需要去了解微信公众号开发流程?如今在公司写业务时或多或少都接触过微信公众号、企业微信、钉钉等H5应用开发,但在开发的时候我们通常都是如下三步骤:
- 引入js文件,在需要调用JS接口的页面引入JS-SDK文件
- 通过config接口注入权限验证配置
- 通过ready、error接口分别处理成功、失败验证 在和微信对接的过程我们很少去关心服务端都做了那些工作,以至于我们平常谈起公众号总是模模糊糊不是很清晰,今天我们就使用nodejs来彻底打通微信公众号的前后端对接,从而熟悉微信公众号开发的整体流程
准备
**1. 微信公众号:**公众号可以在微信公众平台去申请,本次开发使用的是普通订阅号,微信公众号分为普通订阅号、认证订阅号、普通服务号、认真服务号、区别就是不同的公众号类型具备不同的接口权限、比如微信支付必须是认证过的服务号才可以使用,其它的权限说明可以查看权限官方文档
**2. 服务器:**由于我们的服务器和微信服务器进行交互,那么服务器肯定是必不可少的,大家可以自行购买阿里或者腾讯服务器,如果不想花钱也开始使用一些内网穿透工具,将本地的IP暴露到公网中,常用的有Ngrok、Frp等等,具体的使用可以自行点击了解,不过大多是这种工具都不是特别稳定,还是推荐大家购买一台服务器比较方便,说不准以后公众号运营的比较好了有了一定的粉丝,这些都是小意思🙈
3. 域名:域名就不具体阐述了,大家可以找一些域名厂商购买,值得注意的是,有了域名以后能备案就尽早备案,因为有的时候域名是需要备案的
接入
准备工作我们做完以后,紧接着就是如何接入了
**1. 填写服务器配置:**登录微信公众平台官网后,在公众平台官网的开发-基本设置页面,勾选协议成为开发者,点击“修改配置”按钮,填写服务器地址(URL)、Token和EncodingAESKey,其中URL是开发者用来接收微信消息和事件的接口URL。Token可由开发者可以任意填写,用作生成签名(该Token会和接口URL中包含的Token进行比对,从而验证安全性)。EncodingAESKey由开发者手动填写或随机生成,将用作消息体加解密密钥。
- 将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格式的数据包,那我们该如何来处理呢,大概分为如下几个步骤:
- 处理POST类型的控制逻辑,取出原始数据,接受微信给我们推送的XML的数据包
- 解析数据包,将xml解析为json
- 将我们要回复的消息包装成XML的格式
- 在五秒钟之内给返回(微信服务器在五秒之内如果收不到响应会断掉链接,并且重新发送请求,总共重试三次)
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}×tamp=${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 || '获取签名错误')
})
总结
今天我们通过配置到最后前端的接入,讲解了一下微信开放平台的基础常见功能,相信大家对微信的开发也有所了解了,如果你正在开发微信公众号,希望可以帮助到你,我们下期再会。