Skip to content

Commit

Permalink
Mini-Program support. (#44)
Browse files Browse the repository at this point in the history
* Mini-Program support.

* Unit test on wechat decryption class.

* Unit tests on mini program getUserByCode

* Cover more branches on wechat decryptor and OAuth.

* Fix bad smell code.
  • Loading branch information
aojiaotage authored and JacksonTian committed Apr 24, 2018
1 parent 0cd122a commit 3a9c210
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 10 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,23 @@ var client = new Oauth(appid, secret, function (openid, callback) {
});
```

### 小程序初始化
使用小程序时,需要在初始化OAuth时指定`isMiniProgram`参数为`true`

单进程
```
var OAuth = require('wechat-oauth');
var client = new OAuth('your appid', 'your secret', null, null, true); // 最后一个参数即isMiniProgram
```

多进程
```
var oauthApi = new OAuth('appid', 'secret', getToken, saveToken, true);
```

注意:微信不会将用户的sessionKey过期时间告知开发者,该时间会根据用户与小程序互动频繁程度等因素发生变化,建议根据小程序客户端`wx.checkSession()`方法检验凭证是否依旧有效,若失效应该再次使用code换取新的sessionKey。故而此例中的`getToken``saveToken`方法过期机制须有不同。
[官方文档](https://developers.weixin.qq.com/miniprogram/dev/api/signature.html)

### 引导用户
生成引导用户点击的URL。

Expand Down
118 changes: 109 additions & 9 deletions lib/oauth.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var urllib = require('urllib');
var wrapper = require('./util').wrapper;
var extend = require('util')._extend;
var querystring = require('querystring');
var WxBizDataCrypt = require('./wx_biz_data_crypt');

var AccessToken = function (data) {
if (!(this instanceof AccessToken)) {
Expand Down Expand Up @@ -60,9 +61,10 @@ var processToken = function (that, callback) {
* @param {Function} getToken 用于获取token的方法
* @param {Function} saveToken 用于保存token的方法
*/
var OAuth = function (appid, appsecret, getToken, saveToken) {
var OAuth = function (appid, appsecret, getToken, saveToken, isMiniProgram) {
this.appid = appid;
this.appsecret = appsecret;
this.isMiniProgram = isMiniProgram;
// token的获取和存储
this.store = {};
this.getToken = getToken || function (openid, callback) {
Expand Down Expand Up @@ -198,6 +200,46 @@ OAuth.prototype.getAccessToken = function (code, callback) {
this.request(url, args, wrapper(processToken(this, callback)));
};

/**
* 根据授权获取到的code,换取小程序的session key和openid(以及有条件下的unionid)
* 获取openid之后,可以调用`wechat.API`来获取更多信息
* Examples:
* ```
* api.getSessionKey(code, callback);
* ```
* Callback:
*
* - `err`, 获取session key出现异常时的异常对象
* - `result`, 成功时得到的响应结果
*
* Result:
* ```
* {
* data: {
* "session_key": "SESSION_KEY",
* "openid": "OPENID",
* "unionid": "UNIONID"
* }
* }
* ```
* @param {String} code 授权获取到的code
* @param {Function} callback 回调函数
*/
OAuth.prototype.getSessionKey = function(code, callback) {
var url = 'https://api.weixin.qq.com/sns/jscode2session';
var info = {
appid: this.appid,
secret: this.appsecret,
code: code,
grant_type: 'authorization_code',
};
var args = {
data: info,
dataType: 'json'
};
this.request(url, args, wrapper(processToken(this, callback)));
}

/**
* 根据refresh token,刷新access token,调用getAccessToken后才有效
* Examples:
Expand Down Expand Up @@ -252,6 +294,50 @@ OAuth.prototype._getUser = function (options, accessToken, callback) {
this.request(url, args, wrapper(callback));
};

/**
* 根据服务器保存的sessionKey对从小程序客户端获取的加密用户数据进行解密
* Examples:
* ```
* api.decryptMiniProgramUser({encryptedData, iv}, callback);
* ```
* Callback:
*
* - `err`, 解密用户信息出现异常时的异常对象
* - `result`, 成功时得到的响应结果
*
* Result:
* ```
*{
* "openId": "OPENID",
* "nickName": "NICKNAME",
* "gender": "GENDER",
* "city": "CITY",
* "province": "PROVINCE",
* "country": "COUNTRY",
* "avatarUrl": "AVATARURL",
* "unionId": "UNIONID",
* "watermark":
* {
* "appid":"APPID",
* "timestamp":TIMESTAMP
* }
*}
* ```
* @param {Object} options 需要解密的对象
* @param {String} options.encryptedData 从小程序中获得的加密过的字符串
* @param {String} options.iv 从小程序中获得的加密算法初始向量
* @param {Function} callback 回调函数
*/
OAuth.prototype.decryptMiniProgramUser = function (options, callback) {
try {
var decrypter = new WxBizDataCrypt(this.appId, options.sessionKey);
var user = decrypter.decryptData(options.encryptedData, options.iv);
}catch (e) {
return callback(new Error('error occurred when trying to decrypt mini-program user data'));
}
return callback(null, user);
}

/**
* 根据openid,获取用户信息。
* 当access token无效时,自动通过refresh token获取新的access token。然后再获取用户信息
Expand Down Expand Up @@ -348,7 +434,7 @@ OAuth.prototype.verifyToken = function (openid, accessToken, callback) {
};

/**
* 根据code,获取用户信息。
* 根据code,获取用户信息。注意,当OAuth为MiniProgram类型时,返回的用户对象会有所不同,请查看官方文档确定数据结构以便解析。
* Examples:
* ```
* api.getUserByCode(code, callback);
Expand Down Expand Up @@ -388,13 +474,27 @@ OAuth.prototype.getUserByCode = function (options, callback) {
code = options.code;
}

this.getAccessToken(code, function (err, result) {
if (err) {
return callback(err);
}
var openid = result.data.openid;
that.getUser({openid: openid, lang: lang}, callback);
});
if (this.isMiniProgram) {
this.getSessionKey(code, function (err, result) {
if (err) {
return callback(err);
}
var openid = result.data.openid;
that.decryptMiniProgramUser({
openid: openid,
encryptedData: options.encryptedData,
iv: options.iv,
}, callback);
});
} else {
this.getAccessToken(code, function (err, result) {
if (err) {
return callback(err);
}
var openid = result.data.openid;
that.getUser({openid: openid, lang: lang}, callback);
});
}
};

module.exports = OAuth;
54 changes: 54 additions & 0 deletions lib/wx_biz_data_crypt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'use strict';

var crypto = require('crypto');

/**
* 根据appId和小程序的sessionKey对小程序解密器的构造函数
* 该代码来自官方示例:https://developers.weixin.qq.com/miniprogram/dev/api/signature.html
* Examples:
* ```
* var WXBizDataCrypt = require('./wx_biz_data_crypt');
* var decrypter = new WXBizDataCrypt('appid', 'sessionKey');
* ```
* @param {String} appid 在公众平台上申请得到的appid
* @param {String} session_key 根据appid和小程序auth code获得的对应用户sessionKey
*/
function WXBizDataCrypt(appId, sessionKey) {
this.appId = appId;
this.sessionKey = sessionKey;
}

/**
* 通过已有的解密器对小程序加密数据进行解密
*
* @param {String} encryptedData 从小程序中获得的加密数据,格式应该为base64
* @param {String} iv 从小程序中获得加密算法初始向量initial-vector,格式应当为base64
*/
WXBizDataCrypt.prototype.decryptData = function (encryptedData, iv) {
// base64 decode
var sessionKey = new Buffer(this.sessionKey, 'base64');
var encryptedBuffer = new Buffer(encryptedData, 'base64');
var ivBuffer = new Buffer(iv, 'base64');

try {
// 解密
var decipher = crypto.createDecipheriv('aes-128-cbc', sessionKey, ivBuffer);
// 设置自动 padding 为 true,删除填充补位
decipher.setAutoPadding(true);
var decoded = decipher.update(encryptedBuffer, 'binary', 'utf8');
decoded += decipher.final('utf8');

decoded = JSON.parse(decoded);

} catch (err) {
throw new Error('Illegal Buffer, Is Your Data Correct?');
}

if (decoded.watermark.appid !== this.appId) {
throw new Error('Invalid Watermark, Be Sure to Check Again');
}

return decoded;
};

module.exports = WXBizDataCrypt;
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "wechat-oauth",
"version": "1.2.1",
"version": "1.3.0",
"description": "微信公共平台OAuth",
"main": "lib/oauth.js",
"scripts": {
Expand Down
119 changes: 119 additions & 0 deletions test/oauth.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,72 @@ describe('oauth.js', function () {
});
});

describe('mock getUserByCode mini program', function () {
describe('should ok', function () {
var api = new OAuth('appid', 'secret', null, null, true);
before(function () {
muk(api, 'getSessionKey', function (code, callback) {
var resp = {
data: {
session_key: 'SESSION_KEY',
expires_in:7200,
openid: 'OPENID',
unionid: 'UNIONID'
}
};
process.nextTick(function () {
callback(null, resp);
});
});

muk(api, 'decryptMiniProgramUser', function (code, callback) {
process.nextTick(function () {
callback(null, {
openId: 'OPENID',
nickName: 'NICKNAME',
gender: 0,
city: 'CITY',
province: 'PROVINCE',
country: 'COUNTRY',
avatarUrl: 'AVATARURL',
unionId: 'UNIONID',
});
});
});
});

it('should ok with getUserByCode', function (done) {
api.getUserByCode('code', function (err, data) {
expect(err).not.to.be.ok();
expect(data).to.have.keys('openId', 'nickName', 'gender', 'province', 'city',
'country', 'avatarUrl');
done();
});
});

after(function () {
muk.restore();
});
});

describe('should not ok', function () {
it('should not ok if get session key throws an error', function (done) {
var api = new OAuth('appid', 'secret', null, null, true);

muk(api, 'getSessionKey', function (code, callback) {
callback(new Error('mock error'));
});

api.getUserByCode('code', function (err, data) {
expect(err).to.be.a(Error);
done();
});

muk.restore();
});
});
});

describe('verifyToken', function () {
var api = new OAuth('appid', 'secret');
it('should ok with verifyToken', function (done) {
Expand All @@ -483,4 +549,57 @@ describe('oauth.js', function () {
});
});
});

describe('getSessionKey', function () {
var api = new OAuth('appid', 'secret', null, null, true);
it('should invalid', function (done) {
api.getSessionKey('code', function (err, result) {
expect(err).to.be.ok();
expect(err.name).to.be.equal('WeChatAPIError');
expect(err.message).to.contain('invalid appid');
done();
});
});

describe('should ok', function () {
before(function () {
muk(urllib, 'request', function (url, args, callback) {
var resp = {
session_key: 'SESSION_KEY',
expires_in:7200,
openid: 'OPENID',
unionid: 'UNIONID'
};
process.nextTick(function () {
callback(null, resp);
});
});
});

after(function () {
muk.restore();
});

it('should ok', function (done) {
api.getSessionKey('code', function (err, token) {
expect(err).not.to.be.ok();
expect(token).to.have.property('data');
expect(token.data).to.have.keys('session_key', 'openid', 'create_at');
done();
});
});
});
});

describe('decryptMiniProgramUser', function () {
describe('should not ok', function () {
var api = new OAuth('appid', 'secret', null, null, true);
it('should not ok with invalid data', function (done) {
api.decryptMiniProgramUser({}, function (err, result) {
expect(err).to.be.a(Error);
done();
});
});
});
});
});
Loading

0 comments on commit 3a9c210

Please sign in to comment.