這是一個簡單的用Node.js開發微信牆的教程,在這個教程中,包括以下幾部分內容:
- 驗證伺服器有效性
- 接收使用者通過微信訂閱號發給伺服器的訊息
- 解析收到的XML文字訊息格式為JSON
- 用模板構造應答使用者的XML文字訊息
- 將接收到的訊息通過WebSocket服務廣播
- 獲取訊息傳送人的使用者基本資訊(名字和頭像)
微信服務大體上分為兩類,一類是訊息服務,一類是資料服務。
訊息服務是由使用者在微信服務號中傳送訊息,然後微信服務講訊息推送給開發者伺服器,因此它是由微信主動發起,開發者伺服器被動接收的。
訊息服務的資料體格式是XML,微信服務與開發者伺服器之間通過約定token保證資料傳輸的真實和有效性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
//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請求)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//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來加以區別這個訊息是發給哪個微訊號的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
//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 + "."); |
接收到的訊息大概格式如下:
1 2 3 4 5 6 7 |
<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格式資料:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//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,我們可以通過簡單的模板來生成應答訊息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
//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返回,使用者就能在服務號裡面收到應答的訊息了:
1 2 3 4 5 6 7 8 9 10 11 |
//獲取到了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 伺服器,它只有一個廣播模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
//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 服務推送:
1 2 3 4 5 6 7 8 9 10 11 12 |
// 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。
在這裡我們用最簡單的檔案快取,如在分散式的和高併發的情況下,我們可以選擇其他任意的持久化儲存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// 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,就可以進一步獲取使用者基本資訊了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// 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 推送的訊息中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 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 推送到網頁,這樣我們就實現了一個功能完整的“微信牆”的服務端程式。
以下是完整程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
/** 上一個例子的微信牆沒有獲得使用者頭像、名字等資訊 這些資訊要通過另一類微信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
有興趣的同學可以註冊一個微信訂閱號,配置好伺服器,自己嘗試一下~