• Stars
    star
    106
  • Rank 325,871 (Top 7 %)
  • Language
    JavaScript
  • Created over 8 years ago
  • Updated over 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

微信开发简明教程

用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

有兴趣的同学可以注册一个微信订阅号,配置好服务器,自己尝试一下~

More Repositories

1

FE_You_dont_know

分享在前端开发中,你不知道的JavaScript、CSS和HTML趣味知识,增加你的知识面。
JavaScript
953
star
2

ICG-WebGL

交互式计算机图形学——基于WebGL的自顶向下方法(第七版)的例子与练习题
JavaScript
766
star
3

moyu

Github 摸鱼大全!
599
star
4

graphics

一些图形系统相关的小例子
JavaScript
498
star
5

code-to-image

Convert blocks of code to a highlighted jpeg base64 image.
JavaScript
273
star
6

matrix

Open source slideshare lab for user to create slideshares by reveal.js.
JavaScript
158
star
7

handlock

手势解锁密码的 demo
JavaScript
116
star
8

http-modular

A universal library for converting server-side functions into ES Modules.
JavaScript
116
star
9

jcode-awesome

码上掘金精选,收集优秀的码上掘金项目,供大家一起学习研究
JavaScript
99
star
10

sublime-gbk

support file with gbk encoding
Python
92
star
11

rpio2

Control Raspberry Pi GPIO pins with node.js. Fast and easy to use.
JavaScript
90
star
12

gl-renderer

JavaScript
88
star
13

node-canvas-webgl

Integration of node-canvas and headless-gl.
JavaScript
81
star
14

sublime-v8

v8 engine for sublime text 2
Python
79
star
15

animator.js

A mini (1.6kb compressed and 1.1kb gziped!) high-performance animation library. Provide promise-based API.
JavaScript
77
star
16

core-wrappers

ES5 function wrappers and ES7/ES2016 decorators.Works great with React/Angular/more!
JavaScript
74
star
17

think-wechat

微信中间件,同时支持 thinkJS 3.0
JavaScript
73
star
18

babel-plugin-transform-gl-matrix

A babel plugin for gl-matrix.
JavaScript
68
star
19

bullshit-generator-js

狗屁不通文章生成器改进版
JavaScript
58
star
20

glsl-doodle

Drawing patterns with glsl shaders on modern browsers.
GLSL
58
star
21

wait-promise

Make a promise, waiting for a specified amount of time, util something is done.
JavaScript
55
star
22

svg-path-to-canvas

Convert SVG d path to canvas commands
JavaScript
45
star
23

await-signal

A simple promise-based signal system for running async tasks.
JavaScript
44
star
24

flappy_bird

flappy bird using cocos2d-html5 and cqwrap
D
44
star
25

remote

Allowing your PC web page to be remote controlled by mobile device
JavaScript
35
star
26

js-expression

High Performance JavaScript Expression Evaluator
JavaScript
29
star
27

http-with-nodejs

从前端到全栈的课程示例代码——实现HTTP服务器
JavaScript
27
star
28

babel-plugin-transform-private

A plugin provide private fields in ES6+ class
JavaScript
26
star
29

a_tour_with_chatgpt

ChatGPT 带你玩转太阳系
21
star
30

upvote-anywhere

Generate a SVG button that allows you to get upvoted anywhere.
JavaScript
18
star
31

consoli

Output the content of console to a DOM element.
JavaScript
14
star
32

lafe

Smaty3的扩展,前端Layout框架,用来定位和输出数据到复杂的页面模块组合中
PHP
13
star
33

intersection

Control a remote web page with your tablet or your mobile phone.
JavaScript
13
star
34

js-homework

特训营 JavaScript 作业
HTML
11
star
35

jsypar

能生成Javascript版编译机的yacc
Perl
10
star
36

Farseer-QWDIY

集成Nginx+CodeIgniter+Smarty3+QWrap
PHP
10
star
37

babel-plugin-transform-coverage

A plugin for Babel 6 that converts the source code to the code with coverage measurement for jscover.
JavaScript
9
star
38

currying-wy

文言之柯裡化法
6
star
39

go

A cross platform go game
JavaScript
6
star
40

pi-node-apps

用 node.js 开发树莓派的例子
JavaScript
6
star
41

JKit

JKit is a php-framework based on kohana 3.2+
PHP
6
star
42

structured-data-view

Make it easier to play with array buffer.😘
JavaScript
6
star
43

boxbox

JavaScript
5
star
44

nova

The android framework that uses html5 to build UI.
JavaScript
5
star
45

think-leancloud

thinkjs leancloud api
JavaScript
5
star
46

highcharts-for-qwrap

the qwrap adapter of highcharts
JavaScript
4
star
47

cocos2dx-cqwrap

javascript binding extensions for cocos2d-x
C
4
star
48

Kohana-python

This module let python classes work with kohana framework
Python
3
star
49

babel-transform-tutorial

JavaScript
3
star
50

kelou-wy

文言之刻漏
2
star
51

cqwrap

the cocos2d-html5 game creator
JavaScript
2
star
52

assets

2
star
53

aircode-app

A simple web app framework for AirCode.
JavaScript
2
star
54

wenyan-demos

demosify cases for wenyan-lang
JavaScript
2
star
55

Traingulate-lib

Tools for compatible triangulations of simple polygons
C#
1
star
56

gpu-renderer

JavaScript
1
star
57

blog

我的博客
HTML
1
star
58

openai-sh

Fast ChatGPT CLI powered by Azure OpenAI.
TypeScript
1
star
59

todolist

JavaScript
1
star
60

cqwrap-x

the cocos2d-x for javascript with cqwrap
JavaScript
1
star
61

todolist-thinkjs

Simple GTD-style todo list.
HTML
1
star
62

airdb-lite

A local file database.
JavaScript
1
star
63

async-wy

文言之異步秘術
1
star
64

http-connect

a module that make http instance compatible with connect middleware
JavaScript
1
star
65

soltarie.js

经典接龙的js AI版
JavaScript
1
star
66

fast-animation-frame

Cross-platform requestAnimationFrame. Super fast.
JavaScript
1
star