diff --git a/index.js b/index.js index 661a48ab..ba85520e 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,5 @@ var wechat = require('./lib/wechat'); wechat.List = require('./lib/list'); wechat.API = require('./lib/common'); -module.exports = wechat; \ No newline at end of file +wechat.OAuth = require('./lib/oauth'); +module.exports = wechat; diff --git a/lib/common.js b/lib/common.js index 9eafdf9f..82b2bd32 100644 --- a/lib/common.js +++ b/lib/common.js @@ -2,39 +2,9 @@ var path = require('path'); var fs = require('fs'); var urllib = require('urllib'); var formstream = require('formstream'); - -/*! - * 对返回结果的一层封装,如果遇见微信返回的错误,将返回一个错误 - * 参见:http://mp.weixin.qq.com/wiki/index.php?title=返回码说明 - */ -var wrapper = function (callback) { - return function (err, data, res) { - if (err) { - err.name = 'WeChatAPI' + err.name; - return callback(err, data, res); - } - if (data.errcode) { - err = new Error(data.errmsg); - err.name = 'WeChatAPIError'; - return callback(err, data, res); - } - callback(null, data, res); - }; -}; - -/*! - * 对提交参数一层封装,当POST JSON,并且结果也为JSON时使用 - */ -var postJSON = function (data) { - return { - dataType: 'json', - type: 'POST', - data: data, - headers: { - 'Content-Type': 'application/json' - } - }; -}; +var util = require('./util'); +var wrapper = util.wrapper; +var postJSON = util.postJSON; /** * 根据appid和appsecret创建API的构造函数 diff --git a/lib/oauth.js b/lib/oauth.js new file mode 100644 index 00000000..0b0b8cc2 --- /dev/null +++ b/lib/oauth.js @@ -0,0 +1,197 @@ +var urllib = require('urllib'); +var wrapper = require('./util').wrapper; +var querystring = require('querystring'); + +/*! + * 处理token,更新过期时间 + */ +var processToken = function (that, callback) { + return function (err, data, res) { + if (err) { + return callback(err, data); + } + that.accessToken = data.access_token; + that.refreshToken = data.refresh_token; + that.expireTime = (new Date().getTime()) + (data.expires_in - 10) * 1000; + callback(err, data); + }; +}; + +/** + * 根据appid和appsecret创建OAuth接口的构造函数 + * + * Examples: + * ``` + * var OAuth = require('wechat').OAuth; + * var api = new OAuth('appid', 'secret'); + * ``` + * @param {String} appid 在公众平台上申请得到的appid + * @param {String} appsecret 在公众平台上申请得到的app secret + */ +var OAuth = function (appid, appsecret) { + this.appid = appid; + this.appsecret = appsecret; +}; + +/** + * 获取授权页面的URL地址 + * @param {String} redirect 授权后要跳转的地址 + * @param {String} state 开发者可提供的数据 + * @param {String} scope 作用范围,值为snsapi_userinfo和snsapi_base,前者用于弹出,后者用于跳转 + */ +OAuth.prototype.getAuthorizeURL = function (redirect, state, scope) { + var url = 'https://open.weixin.qq.com/connect/oauth2/authorize'; + var info = { + appid: this.appid, + redirect_uri: encodeURIComponent(redirect), + response_type: 'code', + scope: scope || 'snsapi_base', + state: state || '' + }; + + return url + '?' + querystring.stringify(info) + '#wechat_redirect'; +}; + +/** + * 返回当前的access token是否有效 + */ +OAuth.prototype.isAccessTokenValid = function () { + return this.expireTime && (new Date().getTime()) < this.expireTime; +}; + +/** + * 根据授权获取到的code,换取access token + * 获取openid之后,可以调用`wechat.API`来获取更多信息 + * Examples: + * ``` + * api.getAccessToken(code, callback); + * ``` + * Callback: + * + * - `err`, 获取access token出现异常时的异常对象 + * - `result`, 成功时得到的响应结果 + * + * Result: + * ``` + * { + * "access_token": "ACCESS_TOKEN", + * "expires_in": 7200, + * "refresh_token": "REFRESH_TOKEN", + * "openid": "OPENID", + * "scope": "SCOPE" + * } + * ``` + * @param {String} code 授权获取到的code + * @param {Function} callback 回调函数 + */ +OAuth.prototype.getAccessToken = function (code, callback) { + var url = 'https://api.weixin.qq.com/sns/oauth2/access_token'; + var info = { + appid: this.appid, + secret: this.appsecret, + code: code, + grant_type: 'authorization_code' + }; + var args = { + data: info, + dataType: 'json' + }; + urllib.request(url, args, wrapper(processToken(this, callback))); +}; + +/** + * 根据refresh token,刷新access token,调用getAccessToken后才有效 + * Examples: + * ``` + * api.refreshAccessToken(callback); + * ``` + * Callback: + * + * - `err`, 刷新access token出现异常时的异常对象 + * - `result`, 成功时得到的响应结果 + * + * Result: + * ``` + * { + * "access_token": "ACCESS_TOKEN", + * "expires_in": 7200, + * "refresh_token": "REFRESH_TOKEN", + * "openid": "OPENID", + * "scope": "SCOPE" + * } + * ``` + * @param {Function} callback 回调函数 + */ +OAuth.prototype.refreshAccessToken = function (callback) { + var that = this; + var url = 'https://api.weixin.qq.com/sns/oauth2/refresh_token'; + var info = { + appid: this.appid, + grant_type: 'refresh_token', + refresh_token: that.refreshToken + }; + var args = { + data: info, + dataType: 'json' + }; + urllib.request(url, args, wrapper(processToken(this, callback))); +}; + +OAuth.prototype._getUser = function (openid, callback) { + var url = 'https://api.weixin.qq.com/sns/userinfo'; + var info = { + access_token: this.appid, + openid: openid + }; + var args = { + data: info, + dataType: 'json' + }; + urllib.request(url, args, wrapper(callback)); +}; + +/** + * 根据openid,获取用户信息。 + * 当access token无效时,自动通过refresh token获取新的access token。然后再获取用户信息 + * Examples: + * ``` + * api.refreshAccessToken(callback); + * ``` + * Callback: + * + * - `err`, 获取用户信息出现异常时的异常对象 + * - `result`, 成功时得到的响应结果 + * + * Result: + * ``` + * { + * "openid": "OPENID", + * "nickname": "NICKNAME", + * "sex": "1", + * "province": "PROVINCE" + * "city": "CITY", + * "country": "COUNTRY", + * "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", + * "privilege": [ + * "PRIVILEGE1" + * "PRIVILEGE2" + * ] + * } + * ``` + * @param {Function} callback 回调函数 + */ +OAuth.prototype.getUser = function (openid, callback) { + var that = this; + if (this.isAccessTokenValid()) { + that._getUser(openid, callback); + } else { + that.refreshAccessToken(function (err, data) { + if (err) { + return callback(err); + } + that._getUser(openid, callback); + }); + } +}; + +module.exports = OAuth; diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 00000000..3af1d329 --- /dev/null +++ b/lib/util.js @@ -0,0 +1,32 @@ +/*! + * 对返回结果的一层封装,如果遇见微信返回的错误,将返回一个错误 + * 参见:http://mp.weixin.qq.com/wiki/index.php?title=返回码说明 + */ +exports.wrapper = function (callback) { + return function (err, data, res) { + if (err) { + err.name = 'WeChatAPI' + err.name; + return callback(err, data, res); + } + if (data.errcode) { + err = new Error(data.errmsg); + err.name = 'WeChatAPIError'; + return callback(err, data, res); + } + callback(null, data, res); + }; +}; + +/*! + * 对提交参数一层封装,当POST JSON,并且结果也为JSON时使用 + */ +exports.postJSON = function (data) { + return { + dataType: 'json', + type: 'POST', + data: data, + headers: { + 'Content-Type': 'application/json' + } + }; +}; diff --git a/test/oauth.test.js b/test/oauth.test.js new file mode 100644 index 00000000..92d511b1 --- /dev/null +++ b/test/oauth.test.js @@ -0,0 +1,182 @@ +var should = require('should'); +var urllib = require('urllib'); +var muk = require('muk'); +var OAuth = require('../').OAuth; + +describe('oauth.js', function () { + describe('getAuthorizeURL', function () { + var auth = new OAuth('appid', 'secret'); + + it('should ok', function () { + var url = auth.getAuthorizeURL('http://diveintonode.org/', 'hehe'); + url.should.be.equal('https://open.weixin.qq.com/connect/oauth2/authorize?appid=appid&redirect_uri=http%253A%252F%252Fdiveintonode.org%252F&response_type=code&scope=snsapi_base&state=hehe#wechat_redirect'); + }); + + it('should ok', function () { + var url = auth.getAuthorizeURL('http://diveintonode.org/', 'hehe', 'snsapi_userinfo'); + url.should.be.equal('https://open.weixin.qq.com/connect/oauth2/authorize?appid=appid&redirect_uri=http%253A%252F%252Fdiveintonode.org%252F&response_type=code&scope=snsapi_userinfo&state=hehe#wechat_redirect'); + }); + }); + + describe('getAccessToken', function () { + var api = new OAuth('appid', 'secret'); + it('should invalid', function (done) { + api.getAccessToken('code', function (err, data) { + should.exist(err); + err.name.should.be.equal('WeChatAPIError'); + err.message.should.be.equal('system error'); + done(); + }); + }); + + describe('should ok', function () { + before(function () { + muk(urllib, 'request', function (url, args, callback) { + var resp = { + "access_token":"ACCESS_TOKEN", + "expires_in":7200, + "refresh_token":"REFRESH_TOKEN", + "openid":"OPENID", + "scope":"SCOPE" + }; + process.nextTick(function () { + callback(null, resp); + }); + }); + }); + + after(function () { + muk.restore(); + }); + + it('should ok', function (done) { + api.getAccessToken('code', function (err, data, res) { + should.not.exist(err); + data.should.have.keys('access_token', 'expires_in', 'refresh_token', 'openid', 'scope'); + done(); + }); + }); + }); + }); + + describe('refreshAccessToken', function () { + var api = new OAuth('appid', 'secret'); + api.refreshToken = 'token'; + + it('should invalid', function (done) { + api.refreshAccessToken(function (err, data) { + should.exist(err); + err.name.should.be.equal('WeChatAPIError'); + err.message.should.be.equal('invalid appid'); + done(); + }); + }); + + describe('should ok', function () { + before(function () { + muk(urllib, 'request', function (url, args, callback) { + var resp = { + "access_token":"ACCESS_TOKEN", + "expires_in":7200, + "refresh_token":"REFRESH_TOKEN", + "openid":"OPENID", + "scope":"SCOPE" + }; + process.nextTick(function () { + callback(null, resp); + }); + }); + }); + + after(function () { + muk.restore(); + }); + + it('should ok', function (done) { + api.refreshAccessToken(function (err, data, res) { + should.not.exist(err); + data.should.have.keys('access_token', 'expires_in', 'refresh_token', 'openid', 'scope'); + done(); + }); + }); + }); + }); + + describe('getUser', function () { + var api = new OAuth('appid', 'secret'); + + it('should invalid', function (done) { + api._getUser('openid', function (err, data) { + should.exist(err); + err.name.should.be.equal('WeChatAPIError'); + err.message.should.be.equal('invalid credential'); + done(); + }); + }); + + it('should invalid with refresh_token missing', function (done) { + api.getUser('openid', function (err, data) { + should.exist(err); + err.name.should.be.equal('WeChatAPIError'); + err.message.should.be.equal('refresh_token missing'); + done(); + }); + }); + + describe('should ok', function () { + before(function () { + muk(urllib, 'request', function (url, args, callback) { + var resp = { + "openid": "OPENID", + "nickname": "NICKNAME", + "sex": "1", + "province": "PROVINCE", + "city": "CITY", + "country": "COUNTRY", + "headimgurl": "http://wx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46", + "privilege": [ + "PRIVILEGE1", + "PRIVILEGE2" + ] + }; + process.nextTick(function () { + callback(null, resp); + }); + }); + }); + + after(function () { + muk.restore(); + }); + + it('should ok', function (done) { + api.getUser('openid', function (err, data, res) { + should.not.exist(err); + data.should.have.keys('openid', 'nickname', 'sex', 'province', 'city', 'country', 'headimgurl', 'privilege'); + done(); + }); + }); + }); + + describe('should ok', function () { + before(function () { + muk(api, 'isAccessTokenValid', function () { + return true; + }); + }); + + after(function () { + muk.restore(); + }); + + it('should not ok', function (done) { + api.getUser('openid', function (err, data, res) { + should.exist(err); + err.should.have.property('name', 'WeChatAPIError'); + err.should.have.property('message', 'invalid credential'); + done(); + }); + }); + }); + }); +});