wechat-oauth
Version:
503 lines (476 loc) • 13.1 kB
JavaScript
'use strict';
var urllib = require('urllib');
var extend = require('util')._extend;
var querystring = require('querystring');
var wrapper = require('./util').wrapper;
var WxBizDataCrypt = require('./wx_biz_data_crypt');
var AccessToken = function (data) {
if (!(this instanceof AccessToken)) {
return new AccessToken(data);
}
this.data = data;
};
/*!
* 检查AccessToken是否有效,检查规则为当前时间和过期时间进行对比
*
* Examples:
* ```
* token.isValid();
* ```
*/
AccessToken.prototype.isValid = function () {
return !!this.data.access_token && (new Date().getTime()) < (this.data.create_at + this.data.expires_in * 1000);
};
/*!
* 处理token,更新过期时间
*/
var processToken = function (that, callback) {
var create_at = new Date().getTime();
return function (err, data, res) {
if (err) {
return callback(err, data);
}
data.create_at = create_at;
// 存储token
that.saveToken(data.openid, data, function (err) {
callback(err, new AccessToken(data));
});
};
};
/**
* 根据appid和appsecret创建OAuth接口的构造函数
* 如需跨进程跨机器进行操作,access token需要进行全局维护
* 使用使用token的优先级是:
*
* 1. 使用当前缓存的token对象
* 2. 调用开发传入的获取token的异步方法,获得token之后使用(并缓存它)。
* Examples:
* ```
* var OAuth = require('wechat-oauth');
* var api = new OAuth('appid', 'secret');
* ```
* @param {String} appid 在公众平台上申请得到的appid
* @param {String} appsecret 在公众平台上申请得到的app secret
* @param {Function} getToken 用于获取token的方法
* @param {Function} saveToken 用于保存token的方法
*/
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) {
callback(null, this.store[openid]);
};
if (!saveToken && process.env.NODE_ENV === 'production') {
console.warn('Please dont save oauth token into memory under production');
}
this.saveToken = saveToken || function (openid, token, callback) {
this.store[openid] = token;
callback(null);
};
this.defaults = {};
};
/**
* 用于设置urllib的默认options
*
* Examples:
* ```
* oauth.setOpts({timeout: 15000});
* ```
* @param {Object} opts 默认选项
*/
OAuth.prototype.setOpts = function (opts) {
this.defaults = opts;
};
/*!
* urllib的封装
*
* @param {String} url 路径
* @param {Object} opts urllib选项
* @param {Function} callback 回调函数
*/
OAuth.prototype.request = function (url, opts, callback) {
var options = {};
extend(options, this.defaults);
if (typeof opts === 'function') {
callback = opts;
opts = {};
}
for (var key in opts) {
if (key !== 'headers') {
options[key] = opts[key];
} else {
if (opts.headers) {
options.headers = options.headers || {};
extend(options.headers, opts.headers);
}
}
}
urllib.request(url, options, callback);
};
/**
* 获取授权页面的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: redirect,
response_type: 'code',
scope: scope || 'snsapi_base',
state: state || ''
};
return url + '?' + querystring.stringify(info) + '#wechat_redirect';
};
/**
* 获取授权页面的URL地址
* @param {String} redirect 授权后要跳转的地址
* @param {String} state 开发者可提供的数据
* @param {String} scope 作用范围,值为snsapi_login,前者用于弹出,后者用于跳转
*/
OAuth.prototype.getAuthorizeURLForWebsite = function (redirect, state, scope) {
var url = 'https://open.weixin.qq.com/connect/qrconnect';
var info = {
appid: this.appid,
redirect_uri: redirect,
response_type: 'code',
scope: scope || 'snsapi_login',
state: state || ''
};
return url + '?' + querystring.stringify(info) + '#wechat_redirect';
};
/**
* 根据授权获取到的code,换取access token和openid
* 获取openid之后,可以调用`wechat.API`来获取更多信息
* Examples:
* ```
* api.getAccessToken(code, callback);
* ```
* Callback:
*
* - `err`, 获取access token出现异常时的异常对象
* - `result`, 成功时得到的响应结果
*
* Result:
* ```
* {
* data: {
* "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'
};
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,
js_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:
* ```
* api.refreshAccessToken(refreshToken, callback);
* ```
* Callback:
*
* - `err`, 刷新access token出现异常时的异常对象
* - `result`, 成功时得到的响应结果
*
* Result:
* ```
* {
* data: {
* "access_token": "ACCESS_TOKEN",
* "expires_in": 7200,
* "refresh_token": "REFRESH_TOKEN",
* "openid": "OPENID",
* "scope": "SCOPE"
* }
* }
* ```
* @param {String} refreshToken refreshToken
* @param {Function} callback 回调函数
*/
OAuth.prototype.refreshAccessToken = function (refreshToken, callback) {
var url = 'https://api.weixin.qq.com/sns/oauth2/refresh_token';
var info = {
appid: this.appid,
grant_type: 'refresh_token',
refresh_token: refreshToken
};
var args = {
data: info,
dataType: 'json'
};
this.request(url, args, wrapper(processToken(this, callback)));
};
OAuth.prototype._getUser = function (options, accessToken, callback) {
var url = 'https://api.weixin.qq.com/sns/userinfo';
var info = {
access_token: accessToken,
openid: options.openid,
lang: options.lang || 'en'
};
var args = {
data: info,
dataType: 'json'
};
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 从小程序中获得的加密算法初始向量
*/
OAuth.prototype.decryptMiniProgramUser = function (options) {
var decrypter = new WxBizDataCrypt(this.appid, options.sessionKey);
return decrypter.decryptData(options.encryptedData, options.iv);
};
/**
* 根据openid,获取用户信息。
* 当access token无效时,自动通过refresh token获取新的access token。然后再获取用户信息
* Examples:
* ```
* api.getUser(openid, callback);
* api.getUser(options, callback);
* ```
*
* Options:
* ```
* // 或
* {
* "openid": "the open Id", // 必须
* "lang": "the lang code" // zh_CN 简体,zh_TW 繁体,en 英语
* }
* ```
* 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 {Object|String} options 传入openid或者参见Options
* @param {Function} callback 回调函数
*/
OAuth.prototype.getUser = function (options, callback) {
if (typeof options !== 'object') {
options = {
openid: options
};
}
var that = this;
this.getToken(options.openid, function (err, data) {
if (err) {
return callback(err);
}
// 没有token数据
if (!data) {
var error = new Error('No token for ' + options.openid + ', please authorize first.');
error.name = 'NoOAuthTokenError';
return callback(error);
}
var token = new AccessToken(data);
if (token.isValid()) {
that._getUser(options, token.data.access_token, callback);
} else {
that.refreshAccessToken(token.data.refresh_token, function (err, token) {
if (err) {
return callback(err);
}
that._getUser(options, token.data.access_token, callback);
});
}
});
};
/**
* 检验授权凭证(access_token)是否有效。
* Examples:
* ```
* api.verifyToken(openid, accessToken, callback);
* ```
* @param {String} openid 传入openid
* @param {String} accessToken 待校验的access token
* @param {Function} callback 回调函数
*/
OAuth.prototype.verifyToken = function (openid, accessToken, callback) {
var url = 'https://api.weixin.qq.com/sns/auth';
var info = {
access_token: accessToken,
openid: openid
};
var args = {
data: info,
dataType: 'json'
};
this.request(url, args, wrapper(callback));
};
/**
* 根据code,获取用户信息。注意,当OAuth为MiniProgram类型时,返回的用户对象会有所不同,请查看官方文档确定数据结构以便解析。
* Examples:
* ```
* api.getUserByCode(code, 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 {Object|String} options 授权获取到的code
* @param {Function} callback 回调函数
*/
OAuth.prototype.getUserByCode = function (options, callback) {
var that = this;
var lang, code;
if (typeof options === 'string') {
code = options;
} else {
lang = options.lang;
code = options.code;
}
if (this.isMiniProgram) {
this.getSessionKey(code, function (err, result) {
if (err) {
return callback(err);
}
var sessionKey = result.data.session_key;
var user;
try {
user = that.decryptMiniProgramUser({
sessionKey,
encryptedData: options.encryptedData,
iv: options.iv,
});
} catch (ex) {
return callback(ex);
}
callback(null, user);
});
} 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;