├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── gulpfile.js ├── lib ├── index.js └── validations │ └── ack.js ├── package.json └── test └── index.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - v7 4 | - v6 5 | - v5 6 | - v4 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 calidion (calidion.github.io) 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # node-weixin-auth [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url] 2 | 3 | 4 | 微信服务器Auth模块是([node-weixin-api](https://github.com/node-weixin/node-weixin-api) 或者 [node-weixin-express](https://github.com/node-weixin/node-weixin-express))的一个子项目。 5 | 它提供了几个重要的方法 6 | 7 | tokenize: 用于跟服务器验证配置信息 8 | 9 | determine: 用于自动tokenize所有的api请求,而不需要手动在超时时重新请求,通过设置GAP的时间,降低失败率 10 | 11 | ips: 获取服务IP列表 12 | 13 | ack: 用于服务器有效性验证 14 | 15 | 交流QQ群: 39287176 16 | 17 | 18 | ## Installation 19 | 20 | ```sh 21 | $ npm install --save node-weixin-auth 22 | ``` 23 | 24 | 25 | ## Usage 26 | 27 | ```js 28 | 29 | 30 | var nodeWeixinAuth = require('node-weixin-auth'); 31 | var settings = require('node-weixin-settings'); 32 | 33 | var app = { 34 | id: '', 35 | secret: '', 36 | token: '' 37 | }; 38 | 39 | // 调整TIME_GAP来避免重复请求 40 | // 默认是500秒,基本上不会出现失效的情况 41 | nodeWeixinAuth.TIME_GAP = 60; 42 | 43 | //手动得到accessToken 44 | nodeWeixinAuth.tokenize(settings, app, function (error, json) { 45 | var accessToken = json.access_token; 46 | }); 47 | 48 | //自动获得accessToken,并发送需要accessToken的请求 49 | nodeWeixinAuth.determine(settings, app, function () { 50 | //这里添加发送请求的代码 51 | }); 52 | 53 | //获取服务器IP 54 | nodeWeixinAuth.ips(settings, app, function (error, data) { 55 | //error == false 56 | //data.ip_list获取IP列表 57 | }); 58 | 59 | 60 | //与微信对接服务器的验证 61 | var errors = require('web-errors').errors; 62 | var request = require('supertest'); 63 | var express = require('express'); 64 | var bodyParser = require('body-parser'); 65 | 66 | var server = express(); 67 | 68 | server.use(bodyParser.urlencoded({extended: false})); 69 | server.use(bodyParser.json()); 70 | 71 | // 微信服务器返回的ack信息是HTTP的GET方法实现的 72 | server.get('/weixin/ack', function (req, res) { 73 | var data = nodeWeixinAuth.extract(req.query); 74 | nodeWeixinAuth.ack(app.token, data, function (error, data) { 75 | if (!error) { 76 | res.send(data); 77 | return; 78 | } 79 | switch (error) { 80 | case 1: 81 | res.send(errors.INPUT_INVALID); 82 | break; 83 | case 2: 84 | res.send(errors.SIGNATURE_NOT_MATCH); 85 | break; 86 | default: 87 | res.send(errors.UNKNOWN_ERROR); 88 | break; 89 | } 90 | }); 91 | }); 92 | 93 | ``` 94 | 95 | ## License 96 | 97 | Apache-2.0 © [node-weixin](www.node-weixin.com) 98 | 99 | 100 | [npm-image]: https://badge.fury.io/js/node-weixin-auth.svg 101 | [npm-url]: https://npmjs.org/package/node-weixin-auth 102 | [travis-image]: https://travis-ci.org/node-weixin/node-weixin-auth.svg?branch=master 103 | [travis-url]: https://travis-ci.org/node-weixin/node-weixin-auth 104 | [daviddm-image]: https://david-dm.org/node-weixin/node-weixin-auth.svg?theme=shields.io 105 | [daviddm-url]: https://david-dm.org/node-weixin/node-weixin-auth 106 | [coveralls-image]: https://coveralls.io/repos/node-weixin/node-weixin-auth/badge.svg 107 | [coveralls-url]: https://coveralls.io/r/node-weixin/node-weixin-auth 108 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var gulp = require('gulp'); 4 | var eslint = require('gulp-eslint'); 5 | var excludeGitignore = require('gulp-exclude-gitignore'); 6 | var mocha = require('gulp-mocha'); 7 | var istanbul = require('gulp-istanbul'); 8 | var nsp = require('gulp-nsp'); 9 | var plumber = require('gulp-plumber'); 10 | var coveralls = require('gulp-coveralls'); 11 | 12 | gulp.task('static', function () { 13 | return gulp.src('**/*.js') 14 | .pipe(excludeGitignore()) 15 | .pipe(eslint()) 16 | .pipe(eslint.format()) 17 | .pipe(eslint.failAfterError()); 18 | }); 19 | 20 | gulp.task('nsp', function (cb) { 21 | nsp({package: path.resolve('package.json')}, cb); 22 | }); 23 | 24 | gulp.task('pre-test', function () { 25 | return gulp.src('lib/**/*.js') 26 | .pipe(excludeGitignore()) 27 | .pipe(istanbul({ 28 | includeUntested: true 29 | })) 30 | .pipe(istanbul.hookRequire()); 31 | }); 32 | 33 | gulp.task('test', ['pre-test'], function (cb) { 34 | var mochaErr; 35 | 36 | gulp.src('test/**/*.js') 37 | .pipe(plumber()) 38 | .pipe(mocha({reporter: 'spec', timeout: 5000})) 39 | .on('error', function (err) { 40 | mochaErr = err; 41 | throw err; 42 | }) 43 | .pipe(istanbul.writeReports()) 44 | .on('end', function () { 45 | cb(mochaErr); 46 | }); 47 | }); 48 | 49 | gulp.task('watch', function () { 50 | gulp.watch(['lib/**/*.js', 'test/**'], ['test']); 51 | }); 52 | 53 | gulp.task('coveralls', ['test'], function () { 54 | if (!process.env.CI) { 55 | return; 56 | } 57 | 58 | gulp.src(path.join(__dirname, 'coverage/lcov.info')) 59 | .pipe(coveralls()); 60 | }); 61 | 62 | gulp.task('prepublish', ['nsp']); 63 | gulp.task('default', ['static', 'test', 'coveralls']); 64 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint camelcase: [2, {properties: "never"}] */ 3 | 4 | var crypto = require('crypto'); 5 | var restful = require('node-weixin-request'); 6 | var util = require('node-weixin-util'); 7 | var validator = require('node-form-validator'); 8 | var conf = require('./validations/ack'); 9 | 10 | module.exports = { 11 | ACCESS_TOKEN_EXP: 7200 * 1000, 12 | TIME_GAP: 500, 13 | /* should be overridden onAccessToken: function (app, auth) {}*/ 14 | onAccessToken: function () { 15 | }, 16 | generateSignature: function (token, timestamp, nonce) { 17 | var mixes = [token, timestamp, nonce]; 18 | mixes.sort(); 19 | var str = mixes.join(''); 20 | var sha1 = crypto.createHash('sha1'); 21 | sha1.update(str); 22 | return sha1.digest('hex'); 23 | }, 24 | check: function (token, signature, timestamp, nonce) { 25 | var newSignature = this.generateSignature(token, timestamp, nonce); 26 | if (newSignature === signature) { 27 | return true; 28 | } 29 | return false; 30 | }, 31 | determine: function (settings, app, cb) { 32 | var self = this; 33 | settings.get(app.id, 'auth', function (auth) { 34 | var now = new Date().getTime(); 35 | auth = auth || {}; 36 | if (auth.accessToken && auth.lastTime && ((now - auth.lastTime) < (self.ACCESS_TOKEN_EXP - self.TIME_GAP))) { 37 | cb(true); 38 | return; 39 | } 40 | auth.lastTime = now; 41 | settings.set(app.id, 'auth', auth, function () { 42 | self.tokenize(settings, app, function (error) { 43 | cb(error); 44 | }); 45 | }); 46 | }); 47 | }, 48 | tokenize: function (settings, app, cb) { 49 | var baseUrl = 'https://api.weixin.qq.com/cgi-bin/'; 50 | var params = { 51 | grant_type: 'client_credential', 52 | appid: app.id, 53 | secret: app.secret 54 | }; 55 | var url = baseUrl + 'token?' + util.toParam(params); 56 | restful.request(url, null, this._onRequest(settings, app, cb)); 57 | }, 58 | _onRequest: function (settings, app, cb) { 59 | var self = this; 60 | return function (error, json) { 61 | settings.get(app.id, 'auth', function (auth) { 62 | auth = auth || {}; 63 | if (error) { 64 | auth.accessToken = null; 65 | } else { 66 | auth.accessToken = json.access_token; 67 | } 68 | settings.set(app.id, 'auth', auth, function () { 69 | if (self.onAccessToken) { 70 | self.onAccessToken(app, auth); 71 | } 72 | cb(error, json); 73 | }); 74 | }); 75 | }; 76 | }, 77 | extract: function (data) { 78 | return validator.extract(data, conf); 79 | }, 80 | ack: function (token, data, cb) { 81 | var error = validator.validate(data, conf); 82 | if (!error) { 83 | // error 为 false时,表示没有校验行为 84 | cb(true); 85 | return; 86 | } 87 | if (error.code !== 0) { 88 | cb(true, error); 89 | return; 90 | } 91 | data = error.data; 92 | var check = this.check(token, data.signature, data.timestamp, data.nonce); 93 | if (check) { 94 | cb(false, data.echostr); 95 | } else { 96 | cb(true, 2); 97 | } 98 | }, 99 | ips: function (settings, app, cb) { 100 | this.determine(settings, app, function () { 101 | settings.get(app.id, 'auth', function (auth) { 102 | var url = 'https://api.weixin.qq.com/cgi-bin/getcallbackip?' + util.toParam({ 103 | access_token: auth.accessToken 104 | }); 105 | restful.json(url, null, cb); 106 | }); 107 | }); 108 | } 109 | // tokenized: require('./lib/tokenized') 110 | }; 111 | -------------------------------------------------------------------------------- /lib/validations/ack.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | signature: { 3 | type: 'string', 4 | minLength: 3, 5 | maxLength: 64, 6 | required: true 7 | }, 8 | 9 | timestamp: { 10 | type: 'int', 11 | required: true 12 | }, 13 | nonce: { 14 | type: 'string', 15 | required: true 16 | }, 17 | echostr: { 18 | type: 'string' 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-weixin-auth", 3 | "version": "0.7.0", 4 | "description": "auth module for weixin", 5 | "homepage": "", 6 | "author": { 7 | "name": "calidion", 8 | "email": "calidion@gmail.com", 9 | "url": "calidion.github.io" 10 | }, 11 | "files": [ 12 | "lib" 13 | ], 14 | "main": "lib/index.js", 15 | "keywords": [ 16 | "" 17 | ], 18 | "devDependencies": { 19 | "body-parser": "^1.15.0", 20 | "eslint-config-xo-space": "^0.7.0", 21 | "express": "^4.13.4", 22 | "gulp": "^3.9.0", 23 | "gulp-coveralls": "^0.1.0", 24 | "gulp-eslint": "^1.0.0", 25 | "gulp-exclude-gitignore": "^1.0.0", 26 | "gulp-istanbul": "^0.10.3", 27 | "gulp-mocha": "^2.0.0", 28 | "gulp-nsp": "^2.1.0", 29 | "gulp-plumber": "^1.0.0", 30 | "node-weixin-settings": "^0.2.1", 31 | "supertest": "^2.0.0" 32 | }, 33 | "eslintConfig": { 34 | "extends": "xo-space", 35 | "env": { 36 | "mocha": true 37 | } 38 | }, 39 | "repository": "node-weixin/node-weixin-auth", 40 | "scripts": { 41 | "prepublish": "gulp prepublish", 42 | "test": "gulp" 43 | }, 44 | "license": "Apache-2.0", 45 | "dependencies": { 46 | "node-form-validator": "^1.1.0", 47 | "node-weixin-request": "^0.4.0", 48 | "node-weixin-util": "^0.3.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('assert'); 3 | var express = require('express'); 4 | var bodyParser = require('body-parser'); 5 | var settings = require('node-weixin-settings'); 6 | var request = require('supertest'); 7 | 8 | var nodeWeixinAuth = require('../'); 9 | 10 | var callbacks = []; 11 | 12 | var app = { 13 | id: process.env.APP_ID, 14 | secret: process.env.APP_SECRET, 15 | token: process.env.APP_TOKEN 16 | }; 17 | 18 | var server = express(); 19 | server.use(bodyParser.urlencoded({ 20 | extended: false 21 | })); 22 | server.use(bodyParser.json()); 23 | server.post('/weixin', function (req, res) { 24 | var data = nodeWeixinAuth.extract(req.body); 25 | nodeWeixinAuth.ack(app.token, data, function (error, data) { 26 | if (!error) { 27 | res.send(data); 28 | return; 29 | } 30 | switch (error) { 31 | case 1: 32 | res.send('INPUT_INVALID'); 33 | break; 34 | case 2: 35 | res.send('SIGNATURE_NOT_MATCH'); 36 | break; 37 | default: 38 | res.send('UNKNOWN_ERROR'); 39 | break; 40 | } 41 | }); 42 | }); 43 | 44 | server.post('/weixinfail', function (req, res) { 45 | nodeWeixinAuth.ack(app.token, {}, function (error, data) { 46 | if (!error) { 47 | res.send(data); 48 | return; 49 | } 50 | switch (error) { 51 | case 1: 52 | res.send('INPUT_INVALID'); 53 | break; 54 | case 2: 55 | res.send('SIGNATURE_NOT_MATCH'); 56 | break; 57 | default: 58 | res.send('UNKNOWN_ERROR'); 59 | break; 60 | } 61 | }); 62 | }); 63 | 64 | server.post('/weixinfail2', function (req, res) { 65 | var data = nodeWeixinAuth.extract(req.body); 66 | 67 | nodeWeixinAuth.ack('sdfsfdfds', data, function (error, data) { 68 | if (!error) { 69 | res.send(data); 70 | return; 71 | } 72 | switch (error) { 73 | case 1: 74 | res.send('INPUT_INVALID'); 75 | break; 76 | case 2: 77 | res.send('SIGNATURE_NOT_MATCH'); 78 | break; 79 | default: 80 | res.send('UNKNOWN_ERROR'); 81 | break; 82 | } 83 | }); 84 | }); 85 | 86 | nodeWeixinAuth.onAccessToken = function (eventApp, eventAuth) { 87 | settings.get(app.id, 'auth', function (auth) { 88 | assert(auth); 89 | // assert.deepEqual(eventApp, app); 90 | // assert.equal(true, eventAuth.accessToken === auth.accessToken); 91 | if (eventApp.id) { 92 | callbacks.push([eventApp, eventAuth]); 93 | } 94 | }); 95 | }; 96 | 97 | describe('node-weixin-auth node module', function () { 98 | it('should generate signature and check it', function () { 99 | var timestamp = 1439402998232; 100 | var nonce = 'wo1cn2NJPRnZWiTuQW8zQ6Mzn4qQ3kWi'; 101 | var token = 'sososso'; 102 | var sign = nodeWeixinAuth.generateSignature(token, timestamp, nonce); 103 | assert.equal(true, sign === 104 | '886a1db814d97a26c081a9814a47bf0b9ff1da9c'); 105 | }); 106 | 107 | it('should check failed', function () { 108 | var timestamp = 1439402998232; 109 | var nonce = 'wo1cn2NJPRnZWiTuQW8zQ6Mzn4qQ3kWi'; 110 | var token = 'sososso'; 111 | var result = nodeWeixinAuth.check(token, 'soso', timestamp, nonce); 112 | assert.equal(true, !result); 113 | }); 114 | 115 | it('should be able to get a token', function (done) { 116 | nodeWeixinAuth.tokenize(settings, app, function (error, json) { 117 | settings.get(app.id, 'auth', function (auth) { 118 | assert.equal(true, !error); 119 | assert.equal(true, json.access_token.length > 1); 120 | assert.equal(true, json.expires_in <= 7200); 121 | assert.equal(true, json.expires_in >= 7000); 122 | assert.equal(true, json.access_token === auth.accessToken); 123 | done(); 124 | }); 125 | }); 126 | }); 127 | it('should be able to determine to request within expiration', function (done) { 128 | nodeWeixinAuth.determine(settings, app, function (passed) { 129 | var timeOut = function () { 130 | nodeWeixinAuth.determine(settings, app, function (passed) { 131 | assert.equal(true, passed); 132 | done(); 133 | }); 134 | }; 135 | settings.get(app.id, 'auth', function (auth) { 136 | assert.equal(true, auth.lastTime !== null); 137 | assert.equal(true, !passed); 138 | setTimeout(timeOut, 1000); 139 | }); 140 | }); 141 | }); 142 | it('should be able to determine not to request within expiration', 143 | function (done) { 144 | // Change access token expiration to 7200 for testing purpose 145 | nodeWeixinAuth.ACCESS_TOKEN_EXP = 200; 146 | setTimeout(function () { 147 | nodeWeixinAuth.determine(settings, app, function (passed) { 148 | assert.equal(true, !passed); 149 | done(); 150 | }); 151 | }, 1000); 152 | }); 153 | 154 | it('should fail to get a token and checkit', function (done) { 155 | var onRequest = nodeWeixinAuth._onRequest(settings, app, function (err, json) { 156 | assert(err); 157 | assert.deepEqual(json, {}); 158 | done(); 159 | }); 160 | onRequest(true, { 161 | }); 162 | }); 163 | 164 | it('should be able to get a token and checkit', function (done) { 165 | nodeWeixinAuth.tokenize(settings, app, function (error, json) { 166 | assert.equal(true, !error); 167 | assert.equal(true, json.access_token.length > 1); 168 | assert.equal(true, json.expires_in <= 7200); 169 | assert.equal(true, json.expires_in >= 7000); 170 | done(); 171 | }); 172 | }); 173 | 174 | it('should be able to auth weixin signature', function (done) { 175 | var time = new Date().getTime(); 176 | var nonce = 'nonce'; 177 | var signature = nodeWeixinAuth.generateSignature(app.token, time, 178 | nonce); 179 | var echostr = 'Hello world!'; 180 | var data = { 181 | signature: signature, 182 | timestamp: time, 183 | nonce: nonce, 184 | echostr: echostr 185 | }; 186 | request(server).post('/weixin').send(data).expect(200).expect( 187 | echostr).end(done); 188 | }); 189 | 190 | it('should be failed to auth weixin signature', function (done) { 191 | var time = new Date().getTime(); 192 | var nonce = 'nonce'; 193 | var signature = nodeWeixinAuth.generateSignature(app.token, time, 194 | nonce); 195 | var data = { 196 | signature: signature, 197 | timestamp: time, 198 | nonce: nonce 199 | }; 200 | request(server).post('/weixin').send(data).expect(200).end(done); 201 | }); 202 | 203 | it('should be failed to auth weixin signature 2', function (done) { 204 | var data = { 205 | signature: '', 206 | timestamp: '', 207 | nonce: '' 208 | }; 209 | request(server).post('/weixin').send(data).expect(200).end(done); 210 | }); 211 | 212 | it('should be fail to auth weixin signature', function (done) { 213 | var time = new Date().getTime(); 214 | var nonce = 'nonce'; 215 | var signature = nodeWeixinAuth.generateSignature(app.token, time, 216 | nonce); 217 | var echostr = 'Hello world!'; 218 | var data = { 219 | signature: signature, 220 | timestamp: time, 221 | nonce: nonce, 222 | echostr: echostr 223 | }; 224 | request(server).post('/weixinfail').send(data).expect(200).expect( 225 | 'UNKNOWN_ERROR').end(done); 226 | }); 227 | 228 | it('should be fail to auth weixin signature 2', function (done) { 229 | var time = new Date().getTime(); 230 | var nonce = 'nonce'; 231 | var signature = nodeWeixinAuth.generateSignature(app.token, time, 232 | nonce); 233 | var echostr = 'Hello world!'; 234 | var data = { 235 | signature: signature, 236 | timestamp: time, 237 | nonce: nonce, 238 | echostr: echostr 239 | }; 240 | request(server).post('/weixinfail2').send(data).expect(200).expect( 241 | 'UNKNOWN_ERROR').end(done); 242 | }); 243 | 244 | it('should be able to get server ips', function (done) { 245 | nodeWeixinAuth.ips(settings, app, function (error, data) { 246 | assert.equal(true, !error); 247 | assert.equal(true, data.ip_list.length > 1); 248 | done(); 249 | }); 250 | }); 251 | 252 | it('should be able to get notified when access Token updated', function (done) { 253 | settings.get(app.id, 'auth', function (auth) { 254 | for (var i = 0; i < callbacks.length; i++) { 255 | var callback = callbacks[i]; 256 | var appInfo = callback[0]; 257 | var authInfo = callback[1]; 258 | assert.equal(true, appInfo.id === app.id); 259 | assert.equal(true, appInfo.token === app.token); 260 | assert.equal(true, appInfo.secret === app.secret); 261 | assert.equal(true, authInfo.accessToken === auth.accessToken); 262 | } 263 | assert.equal(true, callbacks.length >= 1); 264 | done(); 265 | }); 266 | }); 267 | }); 268 | --------------------------------------------------------------------------------