小试编码

版本一,测通公众号

配置测试公众号

1.进入微信官方文档(公众号);html

2.接口测试号申请(微信公众号的注册有必定门槛) node

接口测试号申请

3.测试帐号管理 git

测试帐号管理

编码

1.编写启动项文件github

// app-1.js
/** * @建立码农: pr * @建立日期: 2019-12-26 19:47:02 * @最近更新: pr * @更新时间: 2019-12-26 19:47:02 * @文件描述: 启动项文件 */
const Koa = require('koa');
const sha1 = require('sha1');

// 配置文件
const config = {
  wechat: {
    appID: 'wxb284d7a53fa2da16',
    appSecret: '24af419d8f6c997b5582fd46eafb377c',
    token: 'ruizhengyunpr840690384'
  }
};
const PORT = 1989;
const app = new Koa();

// 中间件
app.use(function*(next) {
  console.log('query', this.query);

  const token = config.wechat.token;
  const signature = this.query.signature;
  const nonce = this.query.nonce;
  const timestamp = this.query.timestamp;
  const echostr = this.query.echostr;
  const str = [token, timestamp, nonce].sort().join('');
  const sha = sha1(str);

  if (sha === signature) {
    this.body = echostr + '';
  } else {
    this.body = 'wrong';
  }
});

app.listen(PORT);
console.log(`正在监听:${PORT}`);
复制代码

2.执行启动项编程

node --harmony app-1.js
复制代码

3.ngrok 启动json

./ngrok http 1989
复制代码

第一个版本测试

4.拷贝 ngrok 产生的 https 地址微信公众号 > 接口配置信息 > URL 中,而后点击 提交,结果以下api

测试结果

版本二,抽取公众号参数的校验中间件

验证公众号

微信 - token、timestamp、nonce - 字典排序 - sha1 加密 - (r === signature) - echostrbash

  • token、timestamp、nonce 三个参数进行字典排序
  • 将三个参数字符串拼接成一个字符串进行 sha1 加密;
  • 将加密后的字符串与 signature 对比,若是相同,表示这个请求来源于微信,直接原样返回 echostr 参数内容,接入验证就成功了;

编码

1.编写启动项文件微信

// app-2.js
/** * @建立码农: pr * @建立日期: 2019-12-27 19:47:02 * @最近更新: pr * @更新时间: 2019-12-27 19:47:02 * @文件描述: 启动项文件,版本二 */

const Koa = require('koa');
const weChatMiddleWare = require('./app-2/weChat');

// 配置文件
const config = {
  wechat: {
    appID: 'wxb284d7a53fa2da16',
    appSecret: '24af419d8f6c997b5582fd46eafb377c',
    token: 'ruizhengyunpr840690384'
  }
};
const PORT = 1989;
const app = new Koa();

// 中间件
app.use(weChatMiddleWare(config.wechat));

app.listen(PORT);
console.log(`正在监听:${PORT}`);
复制代码

2.编写中间件网络

// app-2/weChat.js
/** * @建立码农: pr * @建立日期: 2019-12-27 19:47:02 * @最近更新: pr * @更新时间: 2019-12-27 19:47:02 * @文件描述: 公众号校验中间件 */

const sha1 = require('sha1');

// 中间件
module.exports = function(opts) {
  return function*(next) {
    console.log('query', this.query);

    const token = opts.token;
    const signature = this.query.signature;
    const nonce = this.query.nonce;
    const timestamp = this.query.timestamp;
    const echostr = this.query.echostr;
    const str = [token, timestamp, nonce].sort().join('');
    const sha = sha1(str);

    if (sha === signature) {
      this.body = echostr + '';
    } else {
      this.body = 'wrong';
    }
  };
};
复制代码

3.执行启动项、ngrok 启动、拷贝 ngrok 产生的 https 地址

版本三,解决 access_token(凭证)失效问题

特性

  • access_token **每2小时(7200秒)**自动失效,须要从新获取;
  • 只要更新了 access_token,以前的 access_token 就没用了;

设计方案

  • 系统每隔 2 小时启动去刷新一次 access_token,这样不管何时调用接口,都是最新的;
  • 为了方便频繁调用,因此的把更新 access_token 存储在一个惟一的地方(必定不要存内存);

编码

1.启动项改写,这块要建个 /app-3/config/access-token.txt

// app-3.js
/** * @建立码农: pr * @建立日期: 2019-12-28 19:47:02 * @最近更新: pr * @更新时间: 2019-12-28 19:47:02 * @文件描述: 启动项文件,版本三 */
const Koa = require('koa');
const path = require('path');
const util = require('./app-3/util');
const weChatMiddleWare = require('./app-3/weChat');
const wechat_file = path.join(__dirname, './app-3/config/access-token.txt');

// 配置文件
const config = {
  wechat: {
    appID: 'wxb284d7a53fa2da16',
    appSecret: '24af419d8f6c997b5582fd46eafb377c',
    token: 'ruizhengyunpr840690384',
    getAccessToken: () => {
      return util.readFileAsync(wechat_file);
    },
    saveAccessToken: data => {
      data = JSON.stringify(data);
      return util.writeFileAsync(wechat_file, data);
    }
  }
};
const PORT = 1989;
const app = new Koa();

// 中间件
app.use(weChatMiddleWare(config.wechat));

app.listen(PORT);
console.log(`正在监听:${PORT}`);
复制代码

2.编写中间件

// app-3/weChat.js
/** * @建立码农: pr * @建立日期: 2019-12-28 19:47:02 * @最近更新: pr * @更新时间: 2019-12-28 19:47:02 * @文件描述: 公众号校验中间件 */

// 依赖包引入
const sha1 = require('sha1');
const Promise = require('bluebird');
const request = Promise.promisify(require('request'));

// 参数定义
const prefix = 'https://api.weixin.qq.com/cgi-bin/';
const api = {
  accessToken: `${prefix}token?grant_type=client_credential`
};

// 判断 access_token 是否过时
function AccessTokenInfo(opts) {
  const that = this;
  this.appID = opts.appID;
  this.appSecret = opts.appSecret;
  this.getAccessToken = opts.getAccessToken;
  this.saveAccessToken = opts.saveAccessToken;

  that
    .getAccessToken()
    .then(function(data) {
      try {
        data = JSON.parse(data);
      } catch (e) {
        // 不合法就从新更新 access_token
        return that.updateAccessToken();
      }

      if (that.isValidAccessToken(data)) {
        // 判断是否有效
        // 取到合法 access_token
        that.access_token = data.access_token;
        that.expires_in = data.expires_in;
        that.saveAccessToken(data);
      } else {
        // 不合法就从新更新 access_token
        return that.updateAccessToken();
      }
    })
}

// 验证 access_token 是否有效
AccessTokenInfo.prototype.isValidAccessToken = function(data) {
  if (!data || !data.access_token || !data.expires_in) {
    return false;
  }

  const now = new Date().getTime();
  if (now < data.expires_in) {
    return true;
  }
  return false;
};

// 更新 access_token
AccessTokenInfo.prototype.updateAccessToken = function() {
  const url = `${api.accessToken}&appid=${this.appID}&secret=${this.appSecret}`;
  console.log('url', url);
  return new Promise((resolve, reject) => {
    request({
      url,
      json: true
    }).then(res => {
      const data = res.body;
      console.log('data', data);

      const now = new Date().getTime();

      // 缩短 20 秒(算上网络请求时间)
      const expires_in = now + (data.expires_in - 20) * 1000;

      data.expires_in = expires_in;
      resolve(data);
    });
  });
};

// 中间件
module.exports = function(opts) {
  const accessTokenInfo = new AccessTokenInfo(opts);

  return function*(next) {
    console.log('query', this.query);

    const token = opts.token;
    const signature = this.query.signature;
    const nonce = this.query.nonce;
    const timestamp = this.query.timestamp;
    const echostr = this.query.echostr;
    const str = [token, timestamp, nonce].sort().join('');
    const sha = sha1(str);

    if (sha === signature) {
      this.body = echostr + '';
    } else {
      this.body = 'wrong';
    }
  };
};
复制代码

3.编写工具库

/** * @建立码农: 芮正云 16396@etransfar.com * @建立日期: 2019-12-28 16:58:04 * @最近更新: 芮正云 16396@etransfar.com * @更新时间: 2019-12-28 16:58:04 * @文件描述: 工具库 */
const fs = require('fs');
const Promise = require('bluebird');

// access_token 读操做
exports.readFileAsync = (fpath, encoding) => {
  return new Promise((resolve, reject) => {
    fs.readFile(fpath, encoding, function(err, content) {
      if (err) {
        reject(err);
      } else {
        resolve(content);
      }
    });
  });
};

// access_token 写操做
exports.writeFileAsync = (fpath, content) => {
  return new Promise((resolve, reject) => {
    fs.writeFile(fpath, content, function(err, content) {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
};
复制代码

4.执行启动项、ngrok 启动、拷贝 ngrok 产生的 https 地址

5.你会发现 /app-3/config/access-token.txt 文件生成 access_token 信息内容

版本四,实现自动回复

实现流程

  • 处理 POST 类型的控制逻辑,接收这个 XML 数据包;
  • 解析 XML 数据包;
  • 拼写须要定义的消息;
  • 包装成 XML 格式;
  • 5 秒内返回回去;

测试号二维码

关注公众号后

测试号二维码

控制台

  • ToUserName,接收方帐号;
  • FromUserName,发送方帐号,即 openid;
  • CreateTime,发送消息时间;
  • MsgType,消息类型。

编码

1.启动项

// app-4.js
/** * @建立码农: pr * @建立日期: 2019-12-29 19:47:02 * @最近更新: pr * @更新时间: 2019-12-29 19:47:02 * @文件描述: 启动项文件,版本四 */
const Koa = require('koa');
const path = require('path');
const fileAsync = require('./app-4/util/fileAsync');
const weChatMiddleWare = require('./app-4/middleWare/weChat');
const wechat_file = path.join(__dirname, './app-4/config/access-token.txt');

// 配置文件
const config = {
  wechat: {
    appID: 'wxb284d7a53fa2da16',
    appSecret: '24af419d8f6c997b5582fd46eafb377c',
    token: 'ruizhengyunpr840690384',
    getAccessToken: () => {
      // 读取文件
      return fileAsync.readFileAsync(wechat_file);
    },
    saveAccessToken: data => {
      // 写入文件
      return fileAsync.writeFileAsync(wechat_file, JSON.stringify(data));
    }
  }
};
const PORT = 1989;
const app = new Koa();

// 中间件
app.use(weChatMiddleWare(config.wechat));

app.listen(PORT);
console.log(`正在监听:${PORT}`);
复制代码

2.编写中间件

// app-4/middleWare/weChat.js
/** * @建立码农: pr * @建立日期: 2019-12-29 19:47:02 * @最近更新: pr * @更新时间: 2019-12-29 19:47:02 * @文件描述: 公众号校验中间件 */

// 依赖包引入
const sha1 = require('sha1');
const rawBody = require('raw-body');
const fileAsync = require('../util/fileAsync');
const AccessTokenInfo = require('../util/AccessTokenInfo');

// 中间件
module.exports = function(opts) {
  const accessTokenInfo = new AccessTokenInfo(opts);

  return function*(next) {
    console.log('query', this.query);

    const token = opts.token;
    const signature = this.query.signature;
    const nonce = this.query.nonce;
    const timestamp = this.query.timestamp;
    const echostr = this.query.echostr;
    const str = [token, timestamp, nonce].sort().join('');
    const sha = sha1(str);

    /** * GET 验证开发者身份 * POST */
    if (sha !== signature) {
      this.body = '❌';
      return false;
    }
    if (this.method === 'GET') {
      this.body = echostr + '';
    } else if (this.method === 'POST') {
      // 依赖包 raw-body 能够把 this 上的 request 对象(http 模块中的 request 对象),拼写它的数据,最终拿到一个 buffer 的 XML
      const data = yield rawBody(this.req, {
        length: this.length,
        limit: '1mb',
        encoding: this.charset
      });

      const content = yield fileAsync.parseXMLAsync(data);
      const message = fileAsync.formatMessage(content.xml);
      if (message.MsgType === 'event') {
        if (message.Event === 'subscribe') {
          const now = new Date().getTime();
          this.status = 200;
          this.type = 'application/xml';
          // 文本模板,后面能够把这块业务抽离处理
          this.body = `<xml> <ToUserName><![CDATA[${message.FromUserName}]]></ToUserName> <FromUserName><![CDATA[${message.ToUserName}]]></FromUserName> <CreateTime>${now}</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[${`你好,${message.FromUserName},欢迎来到本公众号。`}]]></Content> <MsgId>1234567890123456</MsgId> </xml>`;
          console.log('message', this.body);
          return;
        }
      }
      console.log('message', message);
    }
  };
};
复制代码

3.编写工具库

// app-4/uitl/fileAsync.js
/** * @建立码农: pr * @建立日期: 2019-12-29 16:58:04 * @最近更新: pr * @更新时间: 2019-12-29 16:58:04 * @文件描述: 工具库 */

const fs = require('fs');
const Promise = require('bluebird');
const xml2js = require('xml2js');

// access_token 读操做
exports.readFileAsync = (fpath, encoding) => {
  return new Promise((resolve, reject) => {
    fs.readFile(fpath, encoding, function(err, content) {
      if (err) {
        reject(err);
      } else {
        resolve(content);
      }
    });
  });
};

// access_token 写操做
exports.writeFileAsync = (fpath, content) => {
  return new Promise((resolve, reject) => {
    fs.writeFile(fpath, content, function(err, content) {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
};

// 格式化 xml 消息
function formatMessage(data) {
  const message = {};
  if (typeof data === 'object') {
    const keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i];
      const item = data[key];
      const len = item.length;
      if (!(item instanceof Array) || len === 0) {
        continue;
      }

      if (len === 1) {
        const value = item[0];
        if (typeof value === 'object') {
          message[key] = formatMessage(value);
        } else {
          message[key] = (value || '').trim();
        }
      } else {
        message[key] = [];
        for (let i = 0; i < len; i++) {
          message[key].push(formatMessage(item[i]));
        }
      }
    }
  }
  return message;
}

exports.formatMessage = formatMessage;

exports.parseXMLAsync = function(xml) {
  return new Promise(function(resolve, reject) {
    xml2js.parseString(xml, { trim: true }, function(err, data) {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
};
复制代码
// app-4/util/AccessTokenInfo.js
/** * @建立码农: pr * @建立日期: 2019-12-29 19:47:02 * @最近更新: pr * @更新时间: 2019-12-29 19:47:02 * @文件描述: 公众号校验中间件 */

// 依赖包引入
const Promise = require('bluebird');
const request = Promise.promisify(require('request'));

// 参数定义
const prefix = 'https://api.weixin.qq.com/cgi-bin/';
const api = {
  accessToken: `${prefix}token?grant_type=client_credential`
};

// 判断 access_token 是否过时
function AccessTokenInfo(opts) {
  const that = this;
  this.appID = opts.appID;
  this.appSecret = opts.appSecret;
  this.getAccessToken = opts.getAccessToken;
  this.saveAccessToken = opts.saveAccessToken;

  that.getAccessToken().then(function(data) {
    try {
      data = JSON.parse(data);
    } catch (e) {
      // 不合法就从新更新 access_token
      return that.updateAccessToken();
    }

    if (that.isValidAccessToken(data)) {
      //判断是否有效
      // Promise.resolve(data);
      // 取到合法 access_token
      that.access_token = data.access_token;
      that.expires_in = data.expires_in;
      that.saveAccessToken(data);
    } else {
      // 不合法就从新更新 access_token
      return that.updateAccessToken();
    }
  });
  // .then(function(data) {
  // // 取到合法 access_token
  // that.access_token = data.access_token;
  // that.expires_in = data.expires_in;
  // that.saveAccessToken(data);
  // });
}

// 验证 access_token 是否有效
AccessTokenInfo.prototype.isValidAccessToken = function(data) {
  if (!data || !data.access_token || !data.expires_in) {
    return false;
  }

  const now = new Date().getTime();
  if (now < data.expires_in) {
    return true;
  }
  return false;
};

// 更新 access_token
AccessTokenInfo.prototype.updateAccessToken = function() {
  const url = `${api.accessToken}&appid=${this.appID}&secret=${this.appSecret}`;
  console.log('url', url);
  return new Promise((resolve, reject) => {
    request({
      url,
      json: true
    }).then(res => {
      const data = res.body;
      const now = new Date().getTime();

      // 缩短 20 秒(算上网络请求时间)
      const expires_in = now + (data.expires_in - 20) * 1000;

      data.expires_in = expires_in;
      resolve(data);
    });
  });
};

module.exports = AccessTokenInfo;
复制代码

5.执行启动项、ngrok 启动、拷贝 ngrok 产生的 https 地址

6.扫描关注公众号

关注公众号后控制台

扫描关注公众号后

文档和示例地址

你能够

上一篇:本地访问和外网访问

编程之上
相关文章
相关标签/搜索