nodejs微信公众号开发

网上关于node开发公众号的资料相当缺乏,本文旨在以node的视角对公众号开发做一个阐述。

公众号简介

公众号类型

目前公众号主要分为三种:服务号、订阅号、小程序;还有企业微信只针对企业用户使⽤用,暂且不算在内。

微信公众平台:https://mp.weixin.qq.com/

名称

服务对象

业务类型

关注后的位置

服务号

企业

任意

联系⼈人列列表

订阅号

个⼈人或媒体

信息传播

归纳在订阅号

小程序

企业

任意

归纳在最近使⽤用

服务号:给企业提供用户管理与业务服务的能⼒,实现业务扩张。 订阅号:给个⼈或媒体提供信息传播的能⼒,与读者建⽴更好的沟通。 小程序:作⽤基本同服务号,比服务号H5应用体验更更好,但无法替代非H5的沟通系统,可以实现互补。

对于个⼈而⾔,无论是学习还是维护——个⼈公众号,只要不不涉及⽀支付环节,注册一个订阅号足以。如果需要⽀付功能,那么需要注册服务号,服务号注册时需要企业相关证书。

成为开发者

参考资料 微信开发者工具说明 https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1455784140 npm 库 wechat和wechat-api,以及微信开发者文档。

如果需要结合⾃身业务进⾏定制,那么就需要申请成为开发者,然后调用微信提供的api结合⾃身业务进⾏扩展。

  1. 首先在左侧菜单中找到:开发 => 基本配置
  2. 然后接受同意,成为开发者
  3. 获取开发者ID(AppID)与开发者密码(AppSecret),并妥善保存(很重要)
  4. 配置URL⽩白名单,把⾃自⼰己的服务器器IP填上去,保证只有指定的服务器器能获取到access_token(很重 要)

名词解释

  • AppID:公众应用唯⼀一身份认证
  • AppSecret:公众应⽤密码,需妥善保存
  • access_token:调⽤微信接⼝所需的凭证,每个接⼝调用都需要,可通过AppID和AppSecret获 取
  • URL白名单:增加获取accesstoken的安全性,当密码泄露时,通过白名单过滤⾮法请求(官方说明:https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&key=1495617578&version=1&lang=zhCN&platform=2)

开通测试账号

测试账号使用文档:https://blog.csdn.net/hzw2312/article/details/69664485

在进⾏公众号开发时,通常会先在测试账号中进⾏开发调试,经测试确认无误后,再把新功能切换到正式账号。
开通测试账号,将具有所有的权限!
  1. 后台管理左侧菜单 => 开发 => 开发者⼯工具
  1. 开发者⼯具⻚面 => 公众平台测试账号

2. 记录测试账号的appID与appsecret

3 . 测试账号的服务器配置

4. 测试账号的JS接口安全域名配置

5. 扫⼀扫关注⾃⼰的测试账号,然后会在用户列表⾥展现

6. 创建⼏个模板消息,供将来测试使⽤。

sunny-ngrok实现内网穿透

公众号开发时,总是面临各种上传文件服务器的操作,极其不便。而sunny-ngrok提供了内网穿透功能。它可以把你本机的ip发布到外网。

安装sunny-ngrok实现外网的映射:https://www.ngrok.cc/

注册,登录。

点击隧道管理理,打开"开通隧道"

编辑隧道信息-- 填入隧道名(随便填),前置域名(如www.yyy.baidu.com中的yyy,其实就是在该域名下开了一个前缀给你,因此只要写前缀就行了,选一个别⼈人没有⽤过的),本地映射的端⼝,则 是要和web项⽬目的http访问端⼝对应。

确认开通后回到隧道管理。就拿到了隧道id。

Mac

下载客户端。放到usr/local下。并在此打开命令行:

./sunny clientid d5324b15e9e99905 你的隧道id

看到这个就配置成功了。下面起一个node服务器来验证一下。

npm init
npm i koa koa-router koa-static koa-bodyparser -S
// index.js
const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static')
const bodyParser = require('koa-bodyparser');
const app = new Koa()
app.use(bodyParser())
const router = new Router()
app.use(static(__dirname + '/'))

app.use(router.routes()); /*启动路由*/
app.use(router.allowedMethods());
app.listen(3000);

然后写一个vue的页面:

<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=0">
    <script src="https://unpkg.com/vue@2.1.10/dist/vue.min.js"></script>
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    <script src="https://unpkg.com/cube-ui/lib/cube.min.js"></script>
    <script src="https://cdn.bootcss.com/qs/6.6.0/qs.js"></script>
    <script src="http://res.wx.qq.com/open/js/jweixin-1.4.0.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/cube-ui/lib/cube.min.css">
    <style>
        /* .cube-btn {
            margin: 10px 0;
        } */
    </style>
</head>

<body>
    <div id="app">
        <cube-input v-model="value"></cube-input>
        <cube-button @click='click'>Click</cube-button>
    </div>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                value: 'input'
            },

            methods: {
                click: function () {
                    console.log('click')
                }
            },
            mounted: function () {

            },
        });
    </script>
</body>

</html>

输入http://djtao.free.idcfengye.com/。访问成功:

客服消息接口

co-wechat实现消息收发业务

有一个不错的半官方的库 co-wechat:https://github.com/node-webot/co-wechat 把它安装了。

然后新建一个配置。

// config.js
module.exports={
    appid:测试号的appid,
    appsecret:测试号的appsecret,
    token:你自己定的token
}

然后根据co-wechat文档写一个接口:

const config=require('./config')

const wechat=require('co-wechat')

router.all('/wechat',wechat(config).middleware(
    async message=>{
        console.log('wecaht',message);
        return `hello world ${message.Content}`
    }
))

启动服务器。这时候可以配置测试号了。(服务不启动时,无法通过验证)

此时试一试发消息:

后台console的信息是:

原理

以上这个过程是怎么实现的呢?原理必然是重点。

这是服务器验证微信的过程

首先简单描述一下微信收发信息流程:

假设我们不需要co-WeChat这个库,自己写这个收发流程。可以是这样:

// index.js
const Koa = require('koa')
const Router = require('koa-router')
const static = require('koa-static') 
const xml2js = require('xml2js') // xml转化为json
const app = new Koa()
const url = require('url')
const conf = require('./conf')
const crypto = require('crypto') // 加密模块
const xmlParser = require('koa-xml-body') //解析xml数据

app.use(xmlParser())
const router = new Router()
app.use(static(__dirname + '/'))

// 验证
router.get('/wechat', ctx => {
        console.log('校验url', ctx.url) 
      const {query} = url.parse(ctx.url, true)
    const {
            signature, // 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。
            timestamp, // 时间戳
      nonce, // 随机数
      echostr // 随机字符串
    } = query
    console.log('wechat', query)

        // 将 token timestamp nonce 三个参数进行字典序排序并用sha1加密
    let str = [conf.token, timestamp, nonce].sort().join('');
    console.log('str',str)
    let strSha1 = crypto.createHash('sha1').update(str).digest('hex');
        console.log(`自己加密后的字符串为:${strSha1}`); 
      console.log(`微信传入的加密字符为:${signature}`); 
      console.log(`两者比较结果为:${signature == strSha1}`);

        // 签名对比,相同则按照微信要求返回echostr参数值
      if (signature == strSha1) {
        ctx.body = echostr
    } else {
            ctx.body = "你不是微信" }
        }
)

// 接受信息
router.post('/wechat', ctx => {
    const {xml: msg} = ctx.request.body
    console.log('Receive:', msg)
    const builder = new xml2js.Builder()
    const result = builder.buildObject({
        xml: {
            ToUserName: msg.FromUserName,
            FromUserName: msg.ToUserName,
            CreateTime: Date.now(),
            MsgType: msg.MsgType,
            Content: 'Hello ' + msg.Content
                } 
    })
    ctx.body = result
})

app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
接受消息

首先是微信向服务器发送get请求。

微信发出GET请求通常包括4个常见字段。

参数

描述

signature

加密签名,包括token、timestamp和nonce加密混成

timestamp

时间戳

nonce

随机数n+once

echostr

随机字符串

而微信发送消息,请求除了带上token、timestamp和nonce,还会带上一个xml数据包。

  • 对token/timestamp/nonce进行字典排序
  • 对排序完之后的字段拼接,sha1加密
  • 以加密结果对比signature,二者相等则通过校验
发送消息
  • 消息解析为字符串,获取XML数据(接收方,发送方,内容等)。
  • 构造发回的xml数据包(注意原来的发送方变为接收方)
  • 通过片echo把构造好的数据发出去!
SHA1算法

安全哈希算法(Secure Hash Algorithm)主要适用于数字签名标准 (Digital Signature Standard DSS)里面定义的数字签名算法(Digital Signature Algorithm DSA)。对于长度小于2^64位的消息, SHA1会产生一个160位的消息摘要。当接收到消息的时候,这个消息摘要可以用来验证数据的完整性。在传输的过程中,数据很可能会发生变化,那么这时候就会产生不同的消息摘要。SHA1有如下特性: 不可以从消息摘要中复原信息;两个不同的消息不会产生同样的消息摘要,(但会有1x10 ^ 48分之一的机 率出现相同的消息摘要,一般使用时忽略)。

哈希: 不可变长 -> 摘要固定长度

  • 摘要
  • 雪崩效应:密文变化后看不出来。
  • 类似MD5 SHA256

官方api调用

实际上微信不仅是收发消息那么简单。

官方文档:https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140183

为了简化操作,你可以调用一个库,co-wechat的好基友—— co-wechat-api

https://github.com/node-webot/co-wechat-api

!image-20190804010154636

acess_token

accesstoken是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用accesstoken。开发者需要进行妥善保存。accesstoken的存储至少要保留512个字符空间。accesstoken的有效期目前为2个小时(7200s),需定时刷新,重复获取将导致上次获取的access_token失效。

公众平台的API调用所需的access_token的使用及生成方式说明:

1、建议公众号开发者使用中控服务器统一获取和刷新accesstoken,其他业务逻辑服务器所使用的accesstoken均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突,导致access_token覆盖而影响业务;

2、目前accesstoken的有效期通过返回的expirein来传达,目前是7200秒之内的值。中控服务器需要根据这个有效时间提前去刷新新accesstoken。在刷新过程中,中控服务器可对外继续输出的老accesstoken,此时公众平台后台会保证在5分钟内,新老access_token都可用,这保证了第三方业务的平滑过渡;

3、accesstoken的有效时间可能会在未来有调整,所以中控服务器不仅需要内部定时主动刷新,还需要提供被动刷新accesstoken的接口,这样便于业务服务器在API调用获知accesstoken已超时的情况下,可以触发accesstoken的刷新流程。

公众号和小程序均可以使用AppID和AppSecret调用本接口来获取access_token。AppID和AppSecret可在“微信公众平台-开发-基本配置”页中获得(需要已经成为开发者,且帐号没有异常状态)。调用接口时,请登录“微信公众平台-开发-基本配置”提前将服务器IP地址添加到IP白名单中,点击查看设置方法,否则将无法调用成功。小程序无需配置IP白名单。

https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET

需要在后端引入axios,获取token则应该这么写:

router.get('/getTokens',async (ctx)=>{
    const APPID=config.appid;

    const APPSECRET=config.appsecret;

    const api=`https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
    const res = await axios.get(api);
    console.log(res);
    Object.assign(tokenCache, res.data, {
        updateTime: Date.now()
    });
    ctx.body = res.data
})

前端调用这个接口,得到:

因为我经常要用,所以要写一个获取方法:

const getTokens=async function(){
    if(!tokenCache.access_token||Date.now()-7200*1000>tokenCache.updateTime){
        // console.log(222,Date.now()-7200*1000,tokenCache.updateTime)
        const APPID = config.appid;
        const APPSECRET = config.appsecret;
        const api = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${APPID}&secret=${APPSECRET}`
        const res = await axios.get(api);
        return Object.assign(tokenCache, res.data, {
            updateTime: Date.now()
        })
    }else{

        return tokenCache;
    } 
}

实现关注用户列表

// 获取关注者列表
router.get('/getFollowers', async ctx => {
    await getTokens();
    const api = `https://api.weixin.qq.com/cgi-bin/user/get?access_token=${tokenCache.access_token}`
    const res = await axios.get(api)
    ctx.body = res.data
})

api接口于api调用,是微信验证我们的服务器。

Co-wechat工具库下更多api的调用

我想在index.html实现以下功能:

-  实际工作中,通常是用库来实现的。比如用户:
const WechatAPI = require('co-wechat-api');
const api = new WechatAPI(conf.appid, conf.appsecret);

router.get('/getFollowers', async ctx => {
    let res = await api.getFollowers();
    // 批量获取用户信息
    let _res=await api.batchGetUsers(res.data.openid);
    ctx.body = _res
})

全局票据的管理

按照上文的api,token是放到服务器运行内存里的。

获取accesstoken的次数是2000次。每次都调用时不现实的。而且在负载均衡情况下,accesstoken是放node1还是node2呢?

答案是放数据库里。

以mongodb为例:

// mongoose.js
// 连接数据库:
const mongoose = require('mongoose')
const {
    Schema
} = mongoose
mongoose.connect('mongodb://localhost:27017/weixin', {
    useNewUrlParser: true
}, () => {
    console.log('Mongodb connected..')
})

exports.ServerToken = mongoose.model('ServerToken', {
    accessToken: String
});
// index.js
const {ServerToken}=require('./mongoose')
const api = new WechatAPI(config.appid,
    config.appsecret,
    async function () {
        return await ServerToken.findOne()
    },
    async function (token) {
        const res = await ServerToken.updateOne({}, token, { upsert: true }) //允许覆盖
    }
)

在new出 WechatAPI实例的时候,实际上提供了第三第四个参数。第一个用于存token,第四个用于存放。

案例:微信在线投票

后端

先装依赖:

npm i koa koa-router koa-static koa-socket co-wechat -s
// index.js

const Koa = require('koa')
const Router = require('koa-router')

const app = new Koa()
const router = new Router()

// 静态文件
const static = require('koa-static')
app.use(static(__dirname + '/'))
const conf = require('./conf')
const wechat = require('co-wechat')

// socket.io
const IO = require('koa-socket')
const io = new IO()
io.attach(app)
app._io.on('connection',socket => {
    console.log('socket connection..')
})

// 消息接口
router.all('/wechat', wechat(conf).middleware(
    async (message, ctx) => {
        console.log('wechart', message)
        app._io.emit('chat',message)
        return '收到!';
    }
))

app.use(router.routes()); 
/*启动路由*/
app.use(router.allowedMethods());
app.listen(3000);

通过socket.io监听收发消息。

比较简单没什么可说的。

前端

前端通过vue和echarts展现数据

<div id="app">
    <div id="chart" style="width:100%;height:50%;"></div>
    <cube-button @click="reset">重置</cube-button>
    <div class="view-wrapper">
      <div class="list" v-for="item in list.slice(0,5)">
        <div class="item">
          <div class="avatar"></div>
          <div class="bubble">
            <p>{{ item.Content }}</p>
          </div>
        </div>
      </div>
    </div>
  </div>
var app = new Vue({
      el: "#app",
      data: {
        list: []
      },
      watch: {
        list: {
          handler(newName) {
            this.renderChart();
          },
          immediate: true,
          deep: true
        }
      },
      methods: {
        initChart() {
          const option = {
            series: [{
              type: "pie",
              selectedMode: "single",
              radius: [0, "70%"],
              label: {
                normal: {
                  position: "inner"
                }
              },
              labelLine: {
                normal: {
                  show: false
                }
              }
            }]
          };
          this.chart = echarts.init(document.getElementById("chart"));
          this.chart.setOption(option);
        },
        // 刷新图表
        renderChart(newName) {
          const data = ["1", "2"]
            .map(key => ({
              name: key,
              value: this.list.filter(v => v.Content === key).length
            }))
            .filter(v => v.value !== 0);
          const option = {
            series: [{
              data
            }]
          };
          this.chart ? this.chart.setOption(option) : "";
        },
        // 重置
        reset() {
          this.list = [];
        }
      },
      mounted() {
        this.initChart();
        const socket = io()
        socket.on('chat', msg => {
          console.log('chart ...', msg)
          this.list.unshift(msg)
        })
      }
    });

效果如下


原文发布于微信公众号 - 一Li小麦(gh_c88159ec1309)

原文发表时间:2019-08-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券