小試編碼

版本一,測通公衆號

配置測試公衆號

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.掃描關注公衆號

關注公衆號後控制檯

掃描關注公衆號後

文檔和示例地址

你能夠

上一篇:本地訪問和外網訪問

編程之上
相關文章
相關標籤/搜索