使用koa 开发微信公众号

935 阅读3分钟

准备阶段

注册微信测试号 mp.weixin.qq.com/debug/cgi-b…

内网穿透 选择一个可用的

1.www.ngrok.cc/login.html

2.www.cpolar.io

创建koa项目

项目结构

.
├── doc    文档
├── logs   日志
└── src
    ├── config   	 配置文件
    ├── controllers  控制器
    │   └── service
    ├── lib          部分依赖
    ├── models  	 模型
    ├── public  	 静态文件
    │   └── uploads
    ├── routes 		 路由
    └── utils  		 工具

server配置

修改接口配置信息需要后台返回echostr参数 需开发下面的WXauth接口接口

修改接口配置信息 填写本地服务器URL和token

微信服务器会发送一个GET请求

根据请求参数 字典序后 sha1加密 进行比对 如果一致则返回 echostr

WXauth接口

WXauth(ctx) {
    console.log("--WXauth--");
    const { signature, echostr, timestamp, nonce } = ctx.query;
    const sha1Str = sha1([timestamp, nonce, token].sort().join(""));
    if (sha1Str === signature) {
      ctx.body = echostr;
    } else {
      ctx.status = 401;
      ctx.body = "error";
    }
  }

处理公众号消息

微信服务器会发送一个POST请求

1.根据请求参数 字典序后 sha1加密 进行比对 如果一致则 证明是来自微信的请求

2.将请求体中的数据 由xml转为json

3.调用 处理用户发送的消息类型和内容组件 根据 配置规则 return 返回内容

4.调用 消息模板组件 将返回内容组合为xml后 return 返回内容

5.将内容响应

// 处理微信消息
  async WXMsg(ctx) {
    const { signature, timestamp, nonce } = ctx.query;
    const sha1Str = sha1([timestamp, nonce, token].sort().join(""));
    if (sha1Str !== signature) {
      ctx.status = 401;
      ctx.body = "error";
    } else {
      let xmlData = "";
      xmlData += ctx.request.body;
      // 将xml数据解析为js对象
      let Msg = await parseXMLAsync(xmlData);
      Msg = formatMessage(Msg);
      const options = reply(Msg);
      // 最终回复用户的信息
      let replyMessage = template(options);
      ctx.body = replyMessage;
    }
  }

此次校验环节与上次相同 应封装为auth方法

获取AccessToken

凭借AccessToken可以调用微信公众号后台功能 如设置公众号菜单等

后续生成JsSdkSignature也需要这个参数

1.通过 appid appsecret 组和url 请求后获得 access_token 和 expires_in(有效时间)

2.access_token 有效期为2小时,一天只能请求2000次,需要做缓存

async getAccessToken() {
    // 定义请求地址
    const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appid}&secret=${appsecret}`;

    const { access_token, expires_in } = await rp({
      method: "GET",
      url,
      json: true,
    });
    const time = Date.now() + (expires_in - 300) * 1000;
    const wxAToken = { key: "accessToken", value: access_token, time };
    return wxAToken;
  }

调用微信后台功能接口

1.此处 ctx.state.wxToken 为 上一步获取的access_token

调用创建菜单

前端需要发送 公众号菜单的JSON格式结构

// 创建菜单
  async creatMenu(ctx) {
    console.log("--创建菜单--");
    // 定义请求的地址
    const url = `https://api.weixin.qq.com/cgi-bin/menu/create?access_token=${ctx.state.wxToken}`;
    // 发送请求
    const res = await rp({
      method: "POST",
      url,
      json: true,
      body: ctx.request.body,
    });
    if (res.errcode === 0) {
      ctx.body = {
        status: 200,
        msg: "创建成功",
        data: res,
      };
    } else {
      ctx.body = {
        status: 400,
        msg: "创建失败",
        data: res,
      };
    }
    console.log(res);
  }

调用删除菜单

前端需要发送 公众号菜单的JSON格式结构

// 删除菜单
  async deleteMenu(ctx) {
    console.log("--删除菜单--");
    // 定义请求地址
    const url = `https://api.weixin.qq.com/cgi-bin/menu/delete?access_token=${ctx.state.wxToken}`;
    // 发送请求
    const res = await rp({
      method: "GET",
      url,
      json: true,
    });
    if (res.errcode === 0) {
      ctx.body = {
        status: 200,
        msg: "删除成功",
        data: res,
      };
    } else {
      ctx.body = {
        status: 400,
        msg: "删除失败",
        data: res,
      };
    }
    console.log(res);
  }

获取jsApiSignatur

如果要在微信公众号网页内调用js-sdk需要获取jsApiSignatur

1.设置JS接口安全域名 不能加协议名 如 http://

2.根据access_token组合url请求后获得ticket

同样只有2小时有效期 需要做缓存

async getJsApiTicket(access_token) {
    // 定义请求地址
    const url = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${access_token}&type=jsapi`;

    let { ticket, expires_in } = await rp({ method: "GET", url, json: true });
    const time = Date.now() + (expires_in - 300) * 1000;
    const JSTicket = { key: "jsTicket", value: ticket, time };
    return JSTicket;
  }

3.开发getJsSdkSignature接口 前端需要使用返回的参数 通过config接口注入权限验证配置

参与签名的字段包括noncestr(随机字符串), 有效的jsapi_ticket, timestamp(时间戳), url(当前网页的URL,不包含#及其后面部分) 。对所有待签名参数按照字段名的ASCII 码从小到大排序(字典序)后,使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串string1。

// 获取jsApiSignatur
  getJsSdkSignature(ctx) {
    console.log("--获取jsSdkSignatur--");
    const noncestr = Math.random().toString().split(".")[1];
    const timestamp = Date.now();
    const signatureArr = [
      `noncestr=${noncestr}`,
      `jsapi_ticket=${ctx.state.jsToken}`,
      `timestamp=${timestamp}`,
      `url=${ctx.query.url}`,
    ];
    // 字典序 以&拼接
    const signatureStr = signatureArr.sort().join("&");
    const signatur = sha1(signatureStr);
    ctx.body = {
      status: 200,
      msg: "获取成功",
      data: {
        noncestr,
        timestamp,
        signatur,
        appid,
      },
    };
    console.log(signatur);
  }

4.前端拿到签名及其他参数后申请权限

jsSdkGet() {
            wx.config({
              debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
              appId: this.WXdata.appid, // 必填,公众号的唯一标识
              timestamp: this.WXdata.timestamp, // 必填,生成签名的时间戳
              nonceStr: this.WXdata.noncestr, // 必填,生成签名的随机串
              signature: this.WXdata.signatur, // 必填,签名
              jsApiList: [
                "updateAppMessageShareData",
                "updateTimelineShareData",
                "onMenuShareAppMessage",
                "onMenuShareQQ",
                "chooseImage",
                "getNetworkType",
                "getLocation",
                "scanQRCode",
                "openLocation",
                "openAddress",
              ], // 必填,需要使用的JS接口列表
            });

            wx.ready((res) => {
              console.log("--wx--ready--");

              console.log(res);
            });
          },

微信登陆

1.前端向后端微信登陆接口发起请求

auth() {
  window.location.href = "/wx/WXAuthorize";
},

2.后端收到请求后组合url并重定向到该url

跳转后需要用户手动授权

授权后重定向至 设置的回调地址

WXAuthorize(ctx) {
    console.log("--WXAuthorize-- ");
  	// state 可以携带参数 微信回调时仍会携带
    const state = ctx.query.id;
    console.log("ctx..." + ctx.href);
    // 回调地址
    let redirectUrl = "http://2dad98c2.cpolar.io/wx/WXCallback";
  	// 授权方式 静默授权 or 提示授权
    const scope = "snsapi_userinfo";
    const url = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUrl}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
    ctx.redirect(url);
  }

3.编写微信回调接口 此时微信已经携带了code

本地服务器拿到Code 组合 appid appsecret Code

获得 access_token, refresh_token, openid, scope

携带OpenID 返回前端

async WXCallback(ctx) {
    console.log("--WXCallback--");
    const code = ctx.query.code; // 授权码
    // 通过code换取网页授权 access_token
    const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${appid}&secret=${appsecret}&code=${code}&grant_type=authorization_code`;
    // 获得 user_access_token, refresh_token, openid, scope
    let res = await rp({
      method: "GET",
      url,
      json: true,
    });
    console.log(res);
    // 返回前端并携带opendi 
    // Openid 用户唯一标识
  	// 每个appid 对应有自己的access_token(user_access_token)
    ctx.redirect("/?openid=" + res.openid);
  }

此时获取的信息

{
  access_token: '38_aweFz-5puKajzU1ragwaufxfPCHzJQu6vmGSOsQLEF1nlkvGwCW_BJybmNB_wVnLWsQiHuJY7UyBY93A0nK7pA',
  expires_in: 7200,
  refresh_token: '38_CMWsQi0GbFmLm3Nq9F3PjtpxfUdW3tDmuvXJGgIAUFqItEY4lSgiXkl-G3opXyMXvrLeBEBuMEzB5Lkk8wDjJQ',
  openid: 'oLGmwxImbJsLroS---------------',
  scope: 'snsapi_userinfo'
}

4.前端根据openid请求用户信息

此时前端URL 2dad98c2.cpolar.io/?openid=oLG… web端请求方法

getUser() {
            // 获取url
            let arr = location.search.slice(1).split("&");
            let qs = {};
            arr.forEach((item) => {
              let itemArr = item.split("=");
              qs[itemArr[0]] = itemArr[1];
            });
            const openid = qs.openid;
            console.log(qs.openid);
            axios
              .get(`${this.baseUrl}/wx/getUserInfo`, {
                params: {
                  openid,
                },
              })
              .then((res) => {
                console.log(res.data);
              })
              .catch((e) => {
                console.log("获取数据失败");
              });
          },

5.后端根据openID查询对应的user_access_token 拼接URL请求微信服务器获得用户信息

async getUserInfo(ctx) {
    console.log("--getUserInfo--");
    // 接收openID
    const openId = ctx.query.openid;
  
    // 回调时通过code获取的access_token
    // TODO 此处应改为从数据库中查询 这里直接复制了上个方法的返回结果了😄
    const ASToken = '38_m3i_N1WejF3gm6a1b_L3W_MATTqrXt9hHUw3wyPb6-UgMCMWoeGzu3ffbGxkc_0oGFakQRp7OQ0JUWzjZAL56Q'
    
    const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${ASToken}&openid=${openId}&lang=zh_CN`;
    let res = await rp({
      method: "GET",
      url,
      json: true,
    });
    ctx.body = res
  }

6.前端接收返回参数 并渲染页面 前端收到

{
openid: "oLGmwxImbJsLroSufafI_8m0zr4k"
nickname: "阿鹏"
sex: 1
language: "zh_CN"
city: "杭州"
province: "浙江"
country: "中国"
headimgurl: "https://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eqWLdsBA4vNNcVg5ELMaDz5h0JmSI8q3PWFWDR5icV3fW6330Ff1WoibpheqZxP7b1z8Ehk6RKvmjxw/132"
privilege: []
}