用Node.js開發微信墻簡明教程

https://github.com/akira-cn/wxdev

這是一個簡單的用Node.js開發微信墻的教程,在這個教程中,包括以下幾部分內容:

  • 驗證服務器有效性
  • 接收用戶通過微信訂閱號發給服務器的消息
  • 解析收到的XML文本消息格式為JSON
  • 用模板構造應答用戶的XML文本消息
  • 將接收到的消息通過WebSocket服務廣播
  • 獲取消息發送人的用戶基本信息(名字和頭像)

微信服務大體上分為兩類,一類是消息服務,一類是數據服務。

消息服務是由用戶在微信服務號中發送消息,然后微信服務講消息推送給開發者服務器,因此它是由微信主動發起,開發者服務器被動接收的。

微信主動發起

消息服務的數據體格式是XML,微信服務與開發者服務器之間通過約定token保證數據傳輸的真實和有效性。

//verify.js

    var PORT = 9529;
    var http = require("http");
    var qs = require("qs");

    var TOKEN = "yuntu";

    function checkSignature(params, token){
      //1. 將token、timestamp、nonce三個參數進行字典序排序
      //2. 將三個參數字符串拼接成一個字符串進行sha1加密
      //3. 開發者獲得加密后的字符串可與signature對比,標識該請求來源于微信

      var key = [token, params.timestamp, params.nonce].sort().join("");
      var sha1 = require("crypto").createHash("sha1");
      sha1.update(key);

      return  sha1.digest("hex") == params.signature;
    }

    var server = http.createServer(function (request, response) {

      //解析URL中的query部分,用qs模塊(npm install qs)將query解析成json
      var query = require("url").parse(request.url).query;
      var params = qs.parse(query);

      console.log(params);
      console.log("token-->", TOKEN);

      if(checkSignature(params, TOKEN)){
        response.end(params.echostr);
      }else{
        response.end("signature fail");
      }
    });

    server.listen(PORT);

    console.log("Server runing at port: " + PORT + ".");

事實上,token驗證僅用來給開發者服務器驗證消息來源確實是微信,而不是偽造的(因為別人不知道具體的token),作為消息發起方的微信并不要求必須驗證,也就是說,開發者也可以偷懶不做驗證(后果是別人可以模仿微信給服務post請求)。

//noverify.js

    /**
      TOKEN 校驗是保證請求的真實有效,微信自己并不校驗TOKEN,
      開發者服務器也可以不校驗直接返回echostr,
      但是這樣的話意味著第三方也可以很容易偽造請求假裝成微信發送給開發者服務器
     */

    var PORT = 9529;
    var http = require("http");
    var qs = require("qs");

    var server = http.createServer(function (request, response) {
      var query = require("url").parse(request.url).query;
      var params = qs.parse(query);

      response.end(params.echostr);
    });

    server.listen(PORT);

    console.log("Server runing at port: " + PORT + ".");

將微信服務號的服務器配置為開發服務器的URL,就可以接收到微信服務號的消息了

注意:其實理論上一個服務器可以接受和處理多個服務號/訂閱號的消息,可以通過消息體的ToUserName來加以區別這個消息是發給哪個微信號的

//simple_read.js

    /**
      這個例子演示從微信服務接收到的消息格式

      從console.log里可以看到,這個消息是一段XML,格式大概是:

      <xml><ToUserName><![CDATA[gh_7fa37bf2b746]]></ToUserName>
      <FromUserName><![CDATA[oZx2jt4po46nfNT7mnBwgu8mGs3M]]></FromUserName>
      <CreateTime>1458697521</CreateTime>
      <MsgType><![CDATA[text]]></MsgType>
      <Content><![CDATA[測試]]></Content>
      <MsgId>6265058147855266278</MsgId>
      </xml>
    */

    var PORT = 9529;
    var http = require("http");
    var qs = require("qs");

    var TOKEN = "yuntu";

    function checkSignature(params, token){
      //1. 將token、timestamp、nonce三個參數進行字典序排序
      //2. 將三個參數字符串拼接成一個字符串進行sha1加密
      //3. 開發者獲得加密后的字符串可與signature對比,標識該請求來源于微信

      var key = [token, params.timestamp, params.nonce].sort().join("");
      var sha1 = require("crypto").createHash("sha1");
      sha1.update(key);

      return  sha1.digest("hex") == params.signature;
    }

    var server = http.createServer(function (request, response) {

      //解析URL中的query部分,用qs模塊(npm install qs)將query解析成json
      var query = require("url").parse(request.url).query;
      var params = qs.parse(query);

      if(!checkSignature(params, TOKEN)){
        //如果簽名不對,結束請求并返回
        response.end("signature fail");
        return;
      }

      if(request.method == "GET"){
        //如果請求是GET,返回echostr用于通過服務器有效校驗
        response.end(params.echostr);
      }else{
        //否則是微信給開發者服務器的POST請求
        var postdata = "";

        request.addListener("data",function(postchunk){
          postdata += postchunk;
        });

        //獲取到了POST數據
        request.addListener("end",function(){
          console.log(postdata);
          response.end("success");
        });
      }
    });

    server.listen(PORT);

    console.log("Server runing at port: " + PORT + ".");

接收到的消息大概格式如下:

<xml><ToUserName><![CDATA[gh_7fa37bf2b746]]></ToUserName>
    <FromUserName><![CDATA[oZx2jt4po46nfNT7mnBwgu8mGs3M]]></FromUserName>
    <CreateTime>1458697521</CreateTime>
    <MsgType><![CDATA[text]]></MsgType>
    <Content><![CDATA[測試]]></Content>
    <MsgId>6265058147855266278</MsgId>
    </xml>

由于消息體是一段XML文本,我們可以將它解析成更容易操作的JSON格式數據:

//in parse_message.js

        //獲取到了POST數據
        request.addListener("end",function(){
          var parseString = require("xml2js").parseString;

          parseString(postdata, function (err, result) {
            if(!err){
              //我們將XML數據通過xml2js模塊(npm install xml2js)解析成json格式
              console.log(result)
              response.end("success");
            }
          });
        });

我們可以回復消息給微信服務,它將這個應答消息轉給對應的發消息的用戶,格式同樣是一段XML,我們可以通過簡單的模板來生成應答消息:

//in read_reply.js

    function replyText(msg, replyText){
      if(msg.xml.MsgType[0] !== "text"){
        return "";
      }
      console.log(msg);

      //將要返回的消息通過一個簡單的tmpl模板(npm install tmpl)返回微信
      var tmpl = require("tmpl");
      var replyTmpl = "<xml>" +
        "<ToUserName><![CDATA[{toUser}]]></ToUserName>" +
        "<FromUserName><![CDATA[{fromUser}]]></FromUserName>" +
        "<CreateTime><![CDATA[{time}]]></CreateTime>" +
        "<MsgType><![CDATA[{type}]]></MsgType>" +
        "<Content><![CDATA[{content}]]></Content>" +
        "</xml>";

      return tmpl(replyTmpl, {
        toUser: msg.xml.FromUserName[0],
        fromUser: msg.xml.ToUserName[0],
        type: "text",
        time: Date.now(),
        content: replyText
      });
    }

將這個消息作為response返回,用戶就能在服務號里面收到應答的消息了:

    //獲取到了POST數據
        request.addListener("end",function(){
          var parseString = require("xml2js").parseString;

          parseString(postdata, function (err, result) {
            if(!err){
              var res = replyText(result, "消息推送成功!");
              response.end(res);
            }
          });
        });

接下來我們創建一個簡單的 WebSocket 服務器,它只有一個廣播模式:

//in lib/ws.js

    /**
      這是一個簡單的WebSocket服務
      只提供一個廣播的功能,足夠微信墻用了
     */

    var WS_PORT = 10001;

    var WebSocketServer = require("ws").Server
      , wss = new WebSocketServer({ port: WS_PORT });

    wss.on("connection", function connection(ws) {
      ws.on("message", function incoming(message) {
        console.log("received: %s", message);
      });

      console.log("new client connected.");
    });

    wss.broadcast = function broadcast(data) {
      wss.clients.forEach(function each(client) {
        client.send(JSON.stringify(data));
      });
    };

    module.exports = {
      wss: wss
    };

    console.log("Socket server runing at port: " + WS_PORT + ".");

我們可以將收到的消息用 WebSocket 服務推送:

// in weixin_ws1.js

      parseString(postdata, function (err, result) {
        if(!err){
          if(result.xml.MsgType[0] === "text"){
            //將消息通過websocket廣播
            wss.broadcast(result);
            var res = replyText(result, "消息推送成功!");
            response.end(res);
          }
        }
      });

這樣我們就可以在頁面上接收微信消息了。

不過……

因為消息應答體中并沒有發送者的用戶信息,比如姓名、性別、頭像等等,因此我們需要獲取這些信息,這就要用到微信的第二種服務:數據服務

數據服務是由開發者服務器主動調用微信服務API獲得信息的服務,包括用戶管理、素材管理、智能接口、客服接口等等,這類服務從開發者服務器向微信服務主動發起,微信需要驗證請求的合法性,采用了與消息服務不同的鑒權機制。

數據服務的請求是https的,返回數據格式通常是JSON。

開發者調用微信數據接口,需要先獲取接口調用憑據 access_token。接口調用憑據有效期為2小時,超時或重復獲取將導致上次獲取的access_token失效。每天每個服務號不能請求超過2000個access_token,因此我們需要自己緩存獲取到的access_token。

在這里我們用最簡單的文件緩存,如在分布式的和高并發的情況下,我們可以選擇其他任意的持久化存儲。

// in lib/token.js

    /**
        這個模塊用來獲得有效token
        使用:

        var appID = require("./config").appID,
          appSecret = require("./config").appSecret;

        getToken(appID, appSecret).then(function(token){
          console.log(token);
        });

        http://mp.weixin.qq.com/wiki/14/9f9c82c1af308e3b14ba9b973f99a8ba.html
     */

    var request = require("request");
    var fs = require("fs");

    function getToken(appID, appSecret){
      return new Promise(function(resolve, reject){
        var token;

        //先看是否有token緩存,這里選擇用文件緩存,可以用其他的持久存儲作為緩存
        if(fs.existsSync("token.dat")){
          token = JSON.parse(fs.readFileSync("token.dat"));
        }

        //如果沒有緩存或者過期
        if(!token || token.timeout < Date.now()){
          request("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid="+appID+"&secret=" + appSecret, function(err, res, data){
            var result = JSON.parse(data);
            result.timeout = Date.now() + 7000000;
            //更新token并緩存
            //因為access_token的有效期是7200秒,每天可以取2000次
            //所以差不多緩存7000秒左右肯定是夠了
            fs.writeFileSync("token.dat", JSON.stringify(result));
            resolve(result);
          });      
        }else{
          resolve(token);
        }

      });
    }

    module.exports = {getToken: getToken};

獲取到有效的 access_token,就可以進一步獲取用戶基本信息了:

// in lib/user.js

    /**
      這個模塊用來獲得用戶基本信息

      使用方法:

      getUserInfo("oZx2jt4po46nfNT7mnBwgu8mGs3M").then(function(data){
        console.log(data);
      });

      http://mp.weixin.qq.com/wiki/1/8a5ce6257f1d3b2afb20f83e72b72ce9.html
     */

    var appID = require("./config").appID;
    var appSecret = require("./config").appSecret;

    var getToken = require("./token").getToken;

    var request = require("request");

    function getUserInfo(openID){
      return getToken(appID, appSecret).then(function(res){
        var token = res.access_token;

        return new Promise(function(resolve, reject){
          request("https://api.weixin.qq.com/cgi-bin/user/info?access_token="+token+"&openid="+openID+"&lang=zh_CN", function(err, res, data){
              resolve(JSON.parse(data));
            });
        });
      }).catch(function(err){
        console.log(err);
      });  
    }

    module.exports = {
      getUserInfo: getUserInfo
    };

這里面就一個注意點,getUserInfo方法的參數用戶的openID,實際上就是消息體XML里面的FromUserName

所以我們將用戶基本信息獲取出來,附加到 WebSocket 推送的消息中:

// in weixin_ws2.js

        parseString(postdata, function (err, result) {
            if(!err){
              if(result.xml.MsgType[0] === "text"){
                getUserInfo(result.xml.FromUserName[0])
                .then(function(userInfo){
                  //獲得用戶信息,合并到消息中
                  result.user = userInfo;
                  //將消息通過websocket廣播
                  wss.broadcast(result);
                  var res = replyText(result, "消息推送成功!");
                  response.end(res);
                })
              }
            }
        });

最后我們可以得到一段完整的程序,它可以將用戶發送給某個微信服務號的文本消息通過 WebSocket 推送到網頁,這樣我們就實現了一個功能完整的"微信墻"的服務端程序。

以下是完整程序:

/**
      上一個例子的微信墻沒有獲得用戶頭像、名字等信息
      這些信息要通過另一類微信API,也就是由服務器主動調用微信獲得
      這一類API的安全機制不同于之前,不再通過簡單的TOKEN校驗
      而需要通過appID、appSecret獲得access_token,然后再用
      access_token獲取相應的數據

      可以先看以下代碼:
      lib/config.js - appID和appSecret配置
      lib/token.js  - 獲得有效token
      lib/user.js   - 獲得用戶信息
      lib/reply.js  - 回復微信的模板
      lib/ws.js     - 簡單的websocket
    */

    var PORT = 9529;

    var http = require("http");
    var qs = require("qs");
    var TOKEN = "yuntu";

    var getUserInfo = require("./lib/user").getUserInfo;
    var replyText = require("./lib/reply").replyText; 

    var wss = require("./lib/ws.js").wss;

    function checkSignature(params, token){
      //1. 將token、timestamp、nonce三個參數進行字典序排序
      //2. 將三個參數字符串拼接成一個字符串進行sha1加密
      //3. 開發者獲得加密后的字符串可與signature對比,標識該請求來源于微信

      var key = [token, params.timestamp, params.nonce].sort().join("");
      var sha1 = require("crypto").createHash("sha1");
      sha1.update(key);

      return  sha1.digest("hex") == params.signature;
    }

    var server = http.createServer(function (request, response) {

      //解析URL中的query部分,用qs模塊(npm install qs)將query解析成json
      var query = require("url").parse(request.url).query;
      var params = qs.parse(query);

      if(!checkSignature(params, TOKEN)){
        //如果簽名不對,結束請求并返回
        response.end("signature fail");
        return;
      }

      if(request.method == "GET"){
        //如果請求是GET,返回echostr用于通過服務器有效校驗
        response.end(params.echostr);
      }else{
        //否則是微信給開發者服務器的POST請求
        var postdata = "";

        request.addListener("data",function(postchunk){
            postdata += postchunk;
        });

        //獲取到了POST數據
        request.addListener("end",function(){
          var parseString = require("xml2js").parseString;

          parseString(postdata, function (err, result) {
            if(!err){
              if(result.xml.MsgType[0] === "text"){
                getUserInfo(result.xml.FromUserName[0])
                .then(function(userInfo){
                  //獲得用戶信息,合并到消息中
                  result.user = userInfo;
                  //將消息通過websocket廣播
                  wss.broadcast(result);
                  var res = replyText(result, "消息推送成功!");
                  response.end(res);
                })
              }
            }
          });
        });
      }
    });

    server.listen(PORT);

    console.log("Weixin server runing at port: " + PORT + ".");

以上就是微信開發的基本原理,是不是很簡單呢?上面講解的所有的代碼在:

https://github.com/akira-cn/wxdev

有興趣的同學可以注冊一個微信訂閱號,配置好服務器,自己嘗試一下~


所屬標簽

無標簽

25选5玩法中奖