需要解决的问题
微信的第三方开发者平台采用的是OAuth的认证流程。因此,需要解决的问题是:
按照OAuth的流程,调用微信第三方平台的api来获取pre_authenticatioin_code 等参数,提供授权页面让服务号管理者授权;
这里面,有7、8个参数需要通过微信API一步一步的获取。而且当中的某些参数是有过期时间的,因此,需要后端服务能够在过期前自动调用微信API重新获取,并保存这些参数;
考虑到要能够支持多家不同的维修服务号业务应用,因此需要schema设计上支持多个tenant;
需要考虑冷启动的问题。因为,这些参数缓存在memory里面(持久化存储是没有意义的,因为大部分参数过期失效),所以,需要考虑如何从DB 中load 部分参数(如何component access token)以及 快速重新发起微信参数请求;
这7、8个参数是有依赖关系的,用前一个获取后一个。因此,一旦网络连接失效,在获取前面的参数不能成功的情况下,需要重试,成功后,再接着下面的参数请求。
具体步骤
(因为这些代码还没有来得及放到 npm上,所以有需要代码的mail to fxfx_001@163.com. )
(一)获得微信开发者资质及部署公众号应用的步骤
这一步,网上已经有很多可以参考的资料。 无非就是去提供一个企业工商名称,提供一个银行卡号,然后花点钱,让微信给你开个账户。 之后,进入到微信第三方开发者平台,选择公众号第三方平台。
注释 : 只有处在白名单列表中的微信后台服务才能够接调用wechat 平台的api。因此,一般情况下,可以通过nginx的forward proxy 做后台微信的前置机,每次调用api的时候,传出的是这个nginx所在机器的ip地址。
另外,还需要注意的是,这里面所有涉及域名的部分最好保持一致。
(二)存储在wechat 通的数据schema
这个schema 中定义了wechat 通需要存储的服务号具体配置信息。
var wxaccountSchema = new Schema({
mdt_app_id:{ type: String, unique:true},
wx_authorizer_apps : [{
wx_authorizer_app_id:{ type:String,required:true},
wx_authorizer_refresh_token:{ type:String,required:true},
wx_origin_menu_config: { type:String},
wx_mapped_menu_config: { type:String},
wx_mapped_menu_click : { type:String},
wx_authorizer_info:{ type:String}
}]
});
mdt_app_id - 是指一个门店通应用的app_id ,这个app_id 是在应用管理平台上创建时,系统分配给这个品牌商企业的一个唯一标识;
一个app_id 其实可以指定多个应用服务号 (当然,还有一个大前提是,门店通应用需要在wechat 第三方开发者平台通过全网测试)
wx_authorizer_app_id - 是指授权给门店通(wechat 通)管理的服务号应用的id ,这个id 是wechat 平台为每个服务号应用指定的;
wx_authorizer_refresh_token - wechat 通第三方授权的流程最终目的就是要获得这个 refresh_token 以及 访问/替代服务号接受消息的access_token。这里的两个token 一个是存储在db 中(因为,只有通过这个token 才能获得 access-token) , 一个是存储在redis 缓存中;
wx_*_menu_config - 等参数是用于当wechat 通接管微信服务号时,获取现有服务号中的menu 配置信息,为了不造成对终端用户的影响,当你被授权接管服务号时(具体是因为,wechat 通需要获取消息管理权限,而这个权限间接需要自定义menu的权限),需要能够重新设定菜单的配置,这些配置需要存储下来;
wx-authorizer_info - 是一个string 类型,这里存储的是wechat 服务号授予的具体权限集等信息。
(三)wechat 通中各个token及授权码的自动刷新获取
每个微信服务号第三方应用在创建时需要获取 verify-ticket 以及 component_access_token 以及pre_auth_code , 这三个参数的获取顺序依次递进的,而且,每个code 都有过期时间,因此wechat 通需要make sure 这些参数在它过期前重新刷新并替换掉现有的。
而对于,wechat 通自己的应用配置信息,可以存储到db 中或者在应用启动时pass in 到配置文件里,
wechat_platform_config : {
appid: 'wx4ce1df4ef19c19ac',
appsecret: 'ee57d830c7885b90eaba9be18b30f9fa',
msg_token: '123456789mdt',
msg_aeskey: 'p0o9i8u7y6t5r4e3w2q1azsxdcfvgbhnjmkl0987654',
oauth_callback_url: 'http://wechat.mdt.goelia.com.cn/callback'
},
注: msg_token 及aeskey 等参数用于在收发wechat 平台的加解密信息时用于签名的验证。
1) 在wechat 通启动时,尝试获取verify-ticket 等参数 (verify-ticket ,每隔10分钟,wechat 平台会自动同步给应用端,获取后,再去获取component_access_token 及 pre_auth_code ,也可以看出这几个参数间的获取顺序)
function loadWxPlatformParams(){
logger.info('Load mdt-wechat platform params');
searchDependedParam(PLATFORM_TICKET_KEY)
.then(function(){
return redisClient.getAsync(PLATFORM_TICKET_KEY)
})
.then(function(param){
return wxplatform.getComponentAccessToken(param);
})
.then(function(rs){
return wxplatform.createPreAutCode(rs);
})
.then(function(){
return wxservice.findWxAccountsBymdtAppId('')
})
.then(function(rs){
if (rs instanceof Array && rs.length == 1){
rs[0].wx_authorizer_apps.forEach(function(o){
redisClient.getAsync(PLATFORM_TOKEN_KEY)
.then(function(token){
return wxplatform.refreshAuthorizerAccessToken(token, o.wx_authorizer_app_id, o.wx_authorizer_refresh_token);
})
.then(function(rs){
logger.info('After refresh AuthorizerAccessToken, returned authorizer_app_id is '+rs+'. Going to set WechatApi Instance');
//set wechat api instance
return wxplatform.setWechatApiInstance(rs);
2)存储在redis 中各个参数被设置了expire 时间,当expire 时间到了的时候,wechat 通会获取到对应的事件来触发重新刷新的过程
redisPubSub.on(REDIS_EXPIRE_EVENT, function(data, channel){
if (PLATFORM_TOKEN_KEY_SHADOW === data){
searchDependedParam(PLATFORM_TICKET_KEY).then(
function(param){
return wxplatform.getComponentAccessToken(param);
}
).then(function(rs){
logger.info('Trying to refresh component token - '+rs);
}).catch(function(err){
//retry....
});
} else if (PLATFORM_PRE_AUTH_CODE_KEY_SHADOW === data){
searchDependedParam(PLATFORM_TOKEN_KEY).then(function(param){
return wxplatform.createPreAutCode(param);
}).then(function(rs){
logger.info('Trying to refresh '+PLATFORM_PRE_AUTH_CODE_KEY+' - '+rs);
}).catch(function(err){
//retry....
});
} else if (_.endsWith(data,'authorizerAccessToken-shadow')){
var ds = _.split(data,':');
var authorizer_app_id = ds[1];
//TODO - PLATFORM_AUTHORIZER_ACCESS_REFRESH_TOKEN & PLATFORM_AUTHORIZER_APPID should be tried reading from DB firstly.
//TODO - If not there, check if the authorization_code expires. If not, go getting it. Otherwise, ask customer to re-do OAuth.
Q.all([searchDependedParam(PLATFORM_TOKEN_KEY),wxservice.findRefreshTokenByAuthAppId('',authorizer_app_id)])
.spread(function(token,refresh_token){
if (token && refresh_token && authorizer_app_id){
return wxplatform.refreshAuthorizerAccessToken(token,authorizer_app_id,refresh_token);
} else {
logger.info('refresh_token is '+refresh_token);
if (!refresh_token){
logger.info('refresh_token for authorizer_app_id '+authorizer_app_id+' is null!!');
}
}
}).then(function(rs){
logger.info('Trying to refresh Authorizer Access Token - '+rs);
return wxplatform.setWechatApiInstance(rs);
}).catch(function(err){
//retry....
logger.error(data+' is expired from redis. But error happened during refreshing authorizer access token.'+ u.inspect(err,false,null));
});
}
});
3) wechat 通生成让服务号管理员登陆授权的页面。其中的点击登陆授权link 是wechat 平台要求提供的。
其中可以看见,redirect_url 是wechat 通自己制定的auth_call_back url 链接。
exports.getOAuthPage = function(req, res) {
//Obtain the pre_code only exists
redisClient.getAsync(PLATFORM_PRE_AUTH_CODE_KEY)
.then(function(value) {
if (value) {
var strUrl = 'https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=' + config.wechat_platform_config.appid + '&pre_auth_code=' + value + '&redirect_uri=' + encodeURIComponent(config.wechat_platform_config.oauth_callback_url);
//var header = '';
//var body = '<a href=\"' + url+ '\" id=\"authurl\" style=\"display: inline;\">' + 'click here to authorize!!!</a>';
//var html = '<!DOCTYPE html>'
// + '<html><header>' + header + '</header><body>' + body + '</body></html>';
res.render('oauth', { oauth_url: strUrl });
} else {
res.status(404).end();
}
});
};
3) 服务号授权后,wechat 通需要保存各项menu config 以及获取access_token 以及refresh-token.
注: 这里的authCallback 是wechat 开放平台的OAuth2.0 协议的一个实现,即,你传入一个auth_callback 的url, 一旦服务号的管理员授权后,wechat 将authorization code 通过这个call back url 回传给wechat 通。 wechat 通,接下来可以用这个authorization-code 接着获取access_token.
exports.authCallback = function(req, res) {
var authorization_code = req.query.auth_code;
var expire = req.query.expires_in;
logger.debug('Authorization code is ' + authorization_code);
//redisClient.set(PLATFORM_AUTH_CODE, authorization_code);
//If the auth_code expires, it can only be obtained by asking cutomer re-do oauth.
//redisClient.expire(PLATFORM_AUTH_CODE,expire);
res.status(200).send("success");
//Authorization code will expire. So, it is possible to get the access_token & refresh_token asap.
//And by wechat specification, authorization code can only be obtained when doing the OAuth.
//So before expire, the authorizer_access_token can be obtained for multi-times.
_getAuthorizerAccessToken(authorization_code)
.then(function(rs) {
//TODO - Set the mdtAppId formally
//set wx_app_id and wx_refresh_token to database.
return wxservice.createWxAccount('', { "wx_authorizer_app_id": rs.authorizer_appid, "wx_authorizer_refresh_token": rs.authorizer_refresh_token });
})
.then(function(rs) {
//Note- Then save it to redis as well.
redisClient.set(u.format(PLATFORM_AUTHORIZER_ACCESS_REFRESH_TOKEN, rs.wx_authorizer_app_id), rs.wx_authorizer_refresh_token);
return _obtainWxAccountDetailInfoAndSave(rs.wx_authorizer_app_id);
}).then(function(rs) {
return _setWechatApiInstance(rs.authorizer_app_id);
}).then(function(rs) {
//generate menu config
return _convertMenus(rs.authorizerAppId);
}).then(function(rs) {
//save to db for menu config
//Hardcode the mdtAppId
return wxservice.updateWxAccountMenuConfig('', rs);
}).then(function(rs) {
//create menu
return _createWxAccountMenu(rs);
4)获取refresh_token 及access-token 后创建应用(app_id)对于的wechat api instance。 这个instance主要用于操作服务号中的各种api (如,模板消息api,客服接口消息api,素材管理api 等)
function _setWechatApiInstance(authorizer_app_id) {
var deferred = Q.defer();
redisClient.getAsync(u.format(PLATFORM_AUTHORIZER_ACCESS_TOKEN, authorizer_app_id))
.then(function(authorizer_access_token) {
if (authorizer_app_id && authorizer_access_token) {
var api = new WechatAPI(authorizer_app_id, "", function(callback) {
//Make sure the token is refreshed before the expire time. So, for wechat-api, just set the expire time for a
//far time
var ts = new Date('2016-12-30 23:59').getTime();
callback(null, { "accessToken": authorizer_access_token, "expireTime": ts });
});
apiWechatMap.set(authorizer_app_id, api);
deferred.resolve({ 'authorizerAppId': authorizer_app_id });
} else {
deferred.reject(new Error('No token value'), {});
}
});
return deferred.promise;