公眾號第三方平臺開發流程詳解
準備工作
1. 註冊申請
2. 建立第三方平臺
進入‘管理中心->建立第三方平臺’
2.1. 輸入基本資訊
!基本資訊將顯示在授權頁被使用者看到,請認真填寫
修改平臺稽核成功後,僅對測試公眾號生效,屆時再提交“覆蓋現網全網釋出狀態”並稽核成功後,才可影響現網
2.2. 選擇許可權
根據第三方平臺需要的介面許可權進行選擇,需要注意:
- 修改平臺稽核成功後,僅對測試公眾號生效,屆時再提交“覆蓋現網全網釋出狀態”並稽核成功後,才可影響現網;
- 在修改平臺時,若有許可權的增加,則視為一次平臺升級,已授權使用者使用老許可權不受任何影響,但使用新許可權需要使用者再次進行授權頁進行升級授權,此時注意引導使用者重新授權
2.3. 填寫開發資料
授權流程相關
授權發起頁
:即引導使用者進入授權頁面的入口頁面的域名(後面開發部分會詳細介紹)
授權測試公眾號列表
:要填寫公眾號的原始ID,‘微信公眾平臺->公眾號設定->原始ID’
授權事件接收URL
:用於接收微信的各種通知,包括授權成功、取消授權、授權更新以及每十分鐘推送的component_verify_ticket(後面開發部分會詳細介紹)
授權後代公眾號實現業務
如果有過公眾號開發經驗這部分就相對比較容易理解了,基本上與公眾號開發的配置相同(‘微信公眾平臺->基本配置->伺服器配置’),可以參考
公眾號開發準備這篇文章。
公眾號訊息與事件接收URL
這一項要配置一個萬用字元$APPID$
,不同的公眾號事件推送時,這個萬用字元會自動替換為對應的公眾號的AppID
其它
這個IP列表要填寫公網IP,我使用的是阿里雲ESC雲伺服器
開發
使用 Node.js
進行的服務端開發,資料庫使用 Mysql
,ES7
語法,使用Koa2
框架。具體框架結構參考使用Koa2搭建web專案這篇文章(一定要看,不然後面的程式碼會看不懂)。下面會按流程逐步完成第三方平臺後臺的開發
為了篇幅工整,所有的工具類(XXXUtil.js)均放在最後列舉
1. 授權事件接收
<完成本文2.3中的 授權事件接收URL
的開發>
在公眾號第三方平臺建立稽核通過後,微信伺服器會向其“授權事件接收URL”每隔10分鐘定時推送component_verify_ticket(重要欄位類似於密碼)。將欄位存入資料庫定期重新整理,component_verify_ticket、component_access_token以及component_auth_code儲存在同一個表中,資料表component
結構如下:
id | component_ticket | component_access_token | component_auth_code |
---|
【component.js】
const xmlUtil = require("./../utils/xmlUtil.js");
const componentService = require('./../service/componentService.js');
//Be called every 10 minutes to refresh component_verify_ticket by wechat
//Be called when authorized
//Be called when unauthorized
//Be called when refresh
var handleComponentMessage = async (ctx, next) => {
let requestString = ctx.data;
let requestMessage = xmlUtil.formatMessage(requestString.xml);
let query = ctx.query;
/**
* Checking whether access_token and pre_auth_code
* need to be refreshed according to component_verify_ticket
* access_token need to be refreshed every 2 hours
* pre_auth_code need to be refreshed every 30 minutes
*/
let result = await componentService.handleComponentMessage(requestMessage, query);
ctx.response.body = 'success';
}
... ...
module.exports = {
'POST /handleComponentMessage' :handleComponentMessage
};
【componentService.js】
const xml2js = require('xml2js');
const xmlUtil = require("./../utils/xmlUtil.js");
const wechatCrypto = require('./../utils/cryptoUtil.js');
const http = require('./../utils/httpUtils.js');
//analyze the decrypted message (xml => json)
var resolveMessage = messageXML => {
var message = new Promise((resolve, reject) => {
xml2js.parseString(messageXML, {trim: true}, (err, result) => {
var message = xmlUtil.formatMessage(result.xml);
resolve(message);
});
}).then(function(message) {
return message;
});
return message;
};
var handleComponentMessage = async (requestMessage, query) => {
let signature = query.msg_signature;
let timestamp = query.timestamp;
let nonce = query.nonce;
logger.debug("Receive messasge from weixin \nsignature: " + signature + "\ntimestamp: " + timestamp + "\nnonce: " + nonce);
//Create cryptor object for decrypt message
let cryptor = new wechatCrypto(真實的token, 真實的encodingAESKey, 真實的第三方平臺appID);
let encryptMessage = requestMessage.Encrypt;
let decryptMessage = cryptor.decrypt(encryptMessage);
logger.debug('Receive messasge from weixin decrypted :' + JSON.stringify(decryptMessage));
var message = await resolveMessage(decryptMessage.message);
let infoType = message.InfoType;
if(infoType == 'component_verify_ticket') {
let ticket = message.ComponentVerifyTicket;
//query the component_verify_ticket, component_access_token and component_auth_code
... ...
//TODO 將拿到的ticket更新儲存到資料表component中
} else if(infoType == 'authorized') {
//TODO authorized
} else if(infoType == 'unauthorized') {
//TODO unauthorized
} else if(infoType == 'refresh') {
//TODO refresh
}
return '';
}
2. 獲取component_access_token和pre_auth_code
由於兩個屬性都需要定期重新整理,所以我做了一個定時任務,定時重新整理這兩個欄位並存入資料表component
中
【refresh.js】
const schedule = require('node-schedule');
const http = require('./../utils/httpUtils.js');
//refresh component_access_token every 1 hour
var refreshComponentAccessToken = async function() {
var component = 讀取表component;
var ticket = component.component_ticket;
if(ticket == null || ticket == undefined || ticket == '') {
return;
}
var access_token = component.component_access_token;
var auth_code = component.component_auth_code;
var componentTokenPostData = {
component_appid : 第三方appID,
component_appsecret : 第三方appSecret,
component_verify_ticket : ticket
};
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/component/api_component_token',
method : 'post'
};
var component_access_token_result = await http.doHttps_withdata(https_options, componentTokenPostData);
var access_token_json = JSON.parse(component_access_token_result);
logger.debug('Refresh component_access_token result: ' + component_access_token_result);
if(access_token_json.errcode != undefined) {
return;
}
access_token = access_token_json.component_access_token;
//TODO 將拿到的access_token 更新儲存到資料表component中
}
//refresh pre_auth_code every 20 minutes
var refreshComponentAuthCode = async function() {
var component = 讀取表component;
var ticket = component.component_ticket;
var access_token = component.component_access_token;
if(access_token == null || access_token == undefined || access_token == '') {
return;
}
var access_token_times = component.component_access_token_times;
var auth_code = component.component_auth_code;
var auth_code_times = component.component_auth_code_times;
var componentAuthCodePostData = {
component_appid : config.component.appID
};
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/component/api_create_preauthcode?component_access_token=%ACCESS_TOKEN%',
method : 'post'
};
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', access_token);
var component_preauthcode_result = await http.doHttps_withdata(https_options, componentAuthCodePostData);
var preauthcode_json = JSON.parse(component_preauthcode_result);
logger.debug('Refresh pre_auth_code result: ' + component_preauthcode_result);
if(preauthcode_json.errcode != undefined) {
return;
}
auth_code = preauthcode_json.pre_auth_code;
//TODO 將拿到的pre_auth_code 更新儲存到資料表component中
}
//refresh component_access_token every hour
var refreshComponentAccessTokenJob = schedule.scheduleJob('0 0 */1 * * *', refreshComponentAccessToken);
//refresh pre_auth_code every 20 minutes
var refreshComponentAuthCodeJob = schedule.scheduleJob('10 */20 * * * *', refreshComponentAuthCode);
3. 獲取授權
引導使用者進入授權頁,拿到授權碼,再通過授權碼獲取授權令牌(authorizer_access_token)
【component.js】
const xmlUtil = require("./../utils/xmlUtil.js");
const componentService = require('./../service/componentService.js');
var componentAuthorize = async (ctx, next) => {
let url = await componentService.getAuthorizeUrl();
ctx.redirect(url);
}
//授權後跳轉到的頁面
var queryAuthorizeInfo = async (ctx, next) => {
let query = ctx.query;
let auth_code = query.auth_code;
let expires_in = query.expires_in;
let authorization_info = await componentService.queryAuthorizeInfo(auth_code);
let html = '<html><body>'
+ '<p>auth_code = ' + query.auth_code + '</p>'
+ '<p>authorizer_appid = ' + authorization_info.authorizer_appid + '</p>'
+ '<p>access_token = ' + authorization_info.authorizer_access_token + '</p>'
+ '<p>refresh_token = ' + authorization_info.authorizer_refresh_token + '</p>'
+ '<p>func_info = ' + JSON.stringify(authorization_info.func_info) + '</p>'
+ '<p>expires_in = ' + query.expires_in + '</p>'
+ '</body></html>';
ctx.response.type ='text/html';
ctx.response.body = html;
};
module.exports = {
'GET /componentAuthorize' :componentAuthorize,
'GET /queryAuthorizeInfo' :queryAuthorizeInfo
};
【componentService.js】
const xml2js = require('xml2js');
const xmlUtil = require("./../utils/xmlUtil.js");
const wechatCrypto = require('./../utils/cryptoUtil.js');
const http = require('./../utils/httpUtils.js');
let getAuthorizeUrl = async function() {
let component = 讀取表component;
let url = 'https://mp.weixin.qq.com/cgi-bin/componentloginpage?component_appid=%APPID%&pre_auth_code=%AUTH_CODE%&redirect_uri=%REDIRECT_URI%'
.replace('%APPID%', 第三方平臺appID)
.replace('%AUTH_CODE%', component.component_auth_code)
.replace('%REDIRECT_URI%', /*要跳轉到的頁面*/'http://www.xxxxxxx.com/queryAuthorizeInfo');
return url;
}
let queryAuthorizeInfo = async (auth_code) => {
var component = 讀取表component;
var access_token = component.component_access_token;
let queryAuthorizePostData = {
component_appid : 第三方平臺appID,
authorization_code : auth_code
};
let https_options = {
hostname : 'api.weixin.qq.com',
path : config.'/cgi-bin/component/api_query_auth?component_access_token=%ACCESS_TOKEN%',
method : 'POST'
};
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', access_token);
let queryAuthorizeResult = await http.doHttps_withdata(https_options, queryAuthorizePostData);
let queryAuthorize_json = JSON.parse(queryAuthorizeResult);
console.log(queryAuthorize_json);
let authorization_info = queryAuthorize_json.authorization_info;
let appid = authorization_info.authorizer_appid;
access_token = authorization_info.authorizer_access_token;
let expires_in = authorization_info.expires_in;
let refresh_token = authorization_info.authorizer_refresh_token;
let func_info = JSON.stringify(authorization_info.func_info);
let authorizers = await 授權資料表操作類.queryAuthorizerByAppID(appid);
//存在則更新,不存在則新建表項
if(authorizers.length == 0) {
let authorizerResult = await 授權資料表操作類.saveAuthorizer(appid, access_token, refresh_token, expires_in, func_info);
} else {
let authorizerResult = await 授權資料表操作類.updateAuthorizer(appid, access_token, refresh_token, expires_in, func_info);
}
return authorization_info;
}
module.exports = {
getAuthorizeUrl : getAuthorizeUrl,
queryAuthorizeInfo : queryAuthorizeInfo
}
同component_access_token和pre_auth_code一樣,authorizer_access_token也需要定時重新整理,所以在refresh.js中新加一個重新整理任務
【refresh.js】
const schedule = require('node-schedule');
const http = require('./../utils/httpUtils.js');
//refresh authorizer_access_token
var refreshAuthorizerAccessToken = async (component_access_token, authorizer) => {
let appid = authorizer.authorizer_appid;
let authorizer_refresh_token = authorizer.authorizer_refresh_token;
let func_info = authorizer.authorizer_func_info;
var authorizerAccessTokenPostData = {
component_appid : 第三方平臺appID,
authorizer_appid : appid,
authorizer_refresh_token : authorizer_refresh_token
};
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/component/api_authorizer_token?component_access_token=%ACCESS_TOKEN%',
method : 'post'
};
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', component_access_token);
var authorizer_token_result = await http.doHttps_withdata(https_options, authorizerAccessTokenPostData);
var authorizer_token_json = JSON.parse(authorizer_token_result);
logger.debug('Refresh authorizer_access_token result: ' + authorizer_token_result);
if(authorizer_token_json.errcode != undefined) {
return;
}
let authorizer_access_token = authorizer_token_json.authorizer_access_token;
let expires_in = authorizer_token_json.expires_in;
authorizer_refresh_token = authorizer_token_json.authorizer_refresh_token;
var authorizerResult = await 資料表authorizer操作類.updateAuthorizer(appid, authorizer_access_token, authorizer_refresh_token, expires_in, func_info);
return authorizerResult;
}
//refresh authorizer_access_tokens
var refreshAuthorizerAccessTokens = async function() {
var component = await 資料表component操作類.queryComponent();
var component_access_token = component.component_access_token;
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
for(let authorizer of authorizers) {
let authorizerResult = await refreshAuthorizerAccessToken(component_access_token, authorizer);
}
}
//refresh authorizer_access_token(s) every hour
var refreshAuthorizerAccessTokensJob = schedule.scheduleJob('0 0 */1 * * *', refreshAuthorizerAccessTokens);
5. 介面測試
使用獲取到的authorizer_access_token
測試各個授權介面呼叫情況
【unitTest.js】
//本部分內容用於測試使用
const mysql = require('../utils/mysqlUtil.js');
const config = require('./../../config/config.local.js');
const 資料表authorizer操作類 = require('./../dao/資料表authorizer操作類.js');
const http = require('./../utils/httpUtils.js');
const wechatCrypto = require('./../utils/cryptoUtil.js');
var testMenuCreate = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/menu/create?access_token=%ACCESS_TOKEN%',
method : 'POST'
};
var button = [{
type:'view',
name:'百度',
url:'http://www.baidu.com/'
}];
var postData = {
button : button
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
var menuResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = menuResult;
}
var testMenuGet = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/menu/get?access_token=%ACCESS_TOKEN%',
method : 'GET'
};
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
var menuResult = await http.doHttps(https_options);
ctx.response.body = menuResult;
}
var testMenuDelete = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/menu/delete?access_token=%ACCESS_TOKEN%',
method : 'GET'
};
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
var menuResult = await http.doHttps(https_options);
ctx.response.body = menuResult;
}
var testSelfMenuGet = async (ctx, next) => {
var http_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/get_current_selfmenu_info?access_token=%ACCESS_TOKEN%',
method : 'GET'
};
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
var menuResult = await http.doHttp(http_options);
ctx.response.body = menuResult;
}
var testConditionalMenuCreate = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/menu/addconditional?access_token=%ACCESS_TOKEN%',
method : 'POST'
};
var button = [{
type:'view',
name:'soso',
url:'http://www.soso.com/'
}];
var postData = {
button : button,
matchrule:{
tag_id : '130'
}
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
var menuResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = menuResult;
}
var testConditionalMenuDelete = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/menu/delconditional?access_token=%ACCESS_TOKEN%',
method : 'POST'
};
var postData = {
menuid : '417320051'
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
var menuResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = menuResult;
}
var testMaterialGetApi = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/material/batchget_material?access_token=%ACCESS_TOKEN%',
method : 'POST'
};
var postData = {
type : 'image',
offset : 0,
count : 30
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(https_options.path);
var materialResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = materialResult;
}
var testMaterialCount = async (ctx, next) => {
var http_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/material/get_materialcount?access_token=%ACCESS_TOKEN%',
method : 'GET'
};
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(http_options.path);
var materialResult = await http.doHttps(http_options);
ctx.response.body = materialResult;
}
var testCurrentAutoReplyInfo = async (ctx, next) => {
var http_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/get_current_autoreply_info?access_token=%ACCESS_TOKEN%',
method : 'GET'
};
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(http_options.path);
var currentAutoReplyInfoResult = await http.doHttps(http_options);
ctx.response.body = currentAutoReplyInfoResult;
}
var testTagGet = async (ctx, next) => {
var http_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/tags/get?access_token=%ACCESS_TOKEN%',
method : 'GET'
};
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
http_options.path = http_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(http_options.path);
var tagResult = await http.doHttps(http_options);
ctx.response.body = tagResult;
}
var testUserTagGet = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/user/tag/get?access_token=%ACCESS_TOKEN%',
method : 'POST'
};
var postData = {
tagid : 130
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(https_options.path);
var tagResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = tagResult;
}
var testTagCreate = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/tags/create?access_token=%ACCESS_TOKEN%',
method : 'POST'
}
var postData = {
tag : {
name : 'china'
}
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(https_options.path);
var tagResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = tagResult;
}
var testTagUpdate = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/tags/update?access_token=%ACCESS_TOKEN%',
method : 'POST'
}
var postData = {
tag : {
id : 131,
name : 'English'
}
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(https_options.path);
var tagResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = tagResult;
}
var testTagDelete = async (ctx, next) => {
var https_options = {
hostname : 'api.weixin.qq.com',
path : '/cgi-bin/tags/delete?access_token=%ACCESS_TOKEN%',
method : 'POST'
}
var postData = {
tag : {
id : 131
}
}
var authorizers = await 資料表authorizer操作類.queryAuthorizer();
https_options.path = https_options.path.replace('%ACCESS_TOKEN%', authorizers[0].authorizer_access_token);
console.log(https_options.path);
var tagResult = await http.doHttps_withdata(https_options, postData);
ctx.response.body = tagResult;
}
var testMsgDecrypt = async (ctx, next) => {
let encryptMessage = '0yGuDILyxl2GQeyIyGwDyN0JR5CBEctVkZAMyxbsCy' +
'+ZM9eVEZCf+erMzZhh0tC8Ucc4KcUtzXye4DeJ6vLgCX/gUEKWsmGOMXqO5k7kYYNpFpEzAtXWxFjqtr' +
'TbYs27gkoUefUoRvEOke5aTHxQeYwvQoPg8GLKudFehlhsSvii0J90C2cgq4HW5tYTfSAxEIEK8wV37s' +
'tf13JRfCTQWsutpKg1G3Z/Rg1dad/8bLgB+jOeY3NDSPgfkfjn5e7oq7vz/WqQqpmLIOi3zyvahsO6Od' +
'v0VGPo0eaYSpy0Rz1Kh94sunav+XxRAdrAPe1YrGpOX9XTMtBH36bFPrTX82kmjT6/AOYUNUIISVxYaq' +
'ESiIMdB/Fx+3Fn0vX9jp6GIQaRh4mWcvhZFr1aO6E/xNXWsxWN/CjB43ST3FuEk6R9f5+RaOJ8lamhw6' +
'bGTuN7BaTU0qtWgHovtgEEvSZMfw==';
let cryptor = new wechatCrypto(config.validate.token, config.validate.encodingAESKey, config.system.appID);
let decryptMessage = cryptor.decrypt(encryptMessage);
console.log(decryptMessage);
ctx.response.body = decryptMessage;
}
module.exports = {
'GET /testMenuCreate' : testMenuCreate,
'GET /testMenuGet' : testMenuGet,
'GET /testMenuDelete' : testMenuDelete,
'GET /testSelfMenuGet' : testSelfMenuGet,
'GET /testConditionalMenuCreate' : testConditionalMenuCreate,
'GET /testConditionalMenuDelete' : testConditionalMenuDelete,
'GET /testMaterialGetApi' : testMaterialGetApi,
'GET /testMaterialCount' : testMaterialCount,
'GET /testCurrentAutoReplyInfo' : testCurrentAutoReplyInfo,
'GET /testTagCreate' : testTagCreate,
'GET /testTagUpdate' : testTagUpdate,
'GET /testTagDelete' : testTagDelete,
'GET /testTagGet' : testTagGet,
'GET /testUserTagGet' : testUserTagGet,
'GET /testMsgDecrypt' : testMsgDecrypt
};
6. 工具類
加解密工具【cryptoUtil.js】
var crypto = require('crypto');
/**
* 提供基於PKCS7演算法的加解密介面
*
*/
var PKCS7Encoder = {};
/**
* 刪除解密後明文的補位字元
*
* @param {String} text 解密後的明文
*/
PKCS7Encoder.decode = function (text) {
var pad = text[text.length - 1];
if (pad < 1 || pad > 32) {
pad = 0;
}
return text.slice(0, text.length - pad);
};
/**
* 對需要加密的明文進行填充補位
*
* @param {String} text 需要進行填充補位操作的明文
*/
PKCS7Encoder.encode = function (text) {
var blockSize = 32;
var textLength = text.length;
//計算需要填充的位數
var amountToPad = blockSize - (textLength % blockSize);
var result = new Buffer(amountToPad);
result.fill(amountToPad);
return Buffer.concat([text, result]);
};
/**
* 微信企業平臺加解密資訊建構函式
*
* @param {String} token 公眾平臺上,開發者設定的Token
* @param {String} encodingAESKey 公眾平臺上,開發者設定的EncodingAESKey
* @param {String} id 企業號的CorpId或者AppId
*/
var wechatCrypto = function (token, encodingAESKey, id) {
if (!token || !encodingAESKey || !id) {
throw new Error('please check arguments');
}
this.token = token;
this.id = id;
var AESKey = new Buffer(encodingAESKey + '=', 'base64');
if (AESKey.length !== 32) {
throw new Error('encodingAESKey invalid');
}
this.key = AESKey;
this.iv = AESKey.slice(0, 16);
};
/**
* 獲取簽名
*
* @param {String} timestamp 時間戳
* @param {String} nonce 隨機數
* @param {String} encrypt 加密後的文字
*/
wechatCrypto.prototype.getSignature = function(timestamp, nonce, encrypt) {
var shasum = crypto.createHash('sha1');
var arr = [this.token, timestamp, nonce, encrypt].sort();
shasum.update(arr.join(''));
return shasum.digest('hex');
};
/**
* 對密文進行解密
*
* @param {String} text 待解密的密文
*/
wechatCrypto.prototype.decrypt = function(text) {
// 建立解密物件,AES採用CBC模式,資料採用PKCS#7填充;IV初始向量大小為16位元組,取AESKey前16位元組
var decipher = crypto.createDecipheriv('aes-256-cbc', this.key, this.iv);
decipher.setAutoPadding(false);
var deciphered = Buffer.concat([decipher.update(text, 'base64'), decipher.final()]);
deciphered = PKCS7Encoder.decode(deciphered);
// 演算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID]
// 去除16位隨機數
var content = deciphered.slice(16);
var length = content.slice(0, 4).readUInt32BE(0);
return {
message: content.slice(4, length + 4).toString(),
id: content.slice(length + 4).toString()
};
};
/**
* 對明文進行加密
*
* @param {String} text 待加密的明文
*/
wechatCrypto.prototype.encrypt = function (text) {
// 演算法:AES_Encrypt[random(16B) + msg_len(4B) + msg + $CorpID]
// 獲取16B的隨機字串
var randomString = crypto.pseudoRandomBytes(16);
var msg = new Buffer(text);
// 獲取4B的內容長度的網路位元組序
var msgLength = new Buffer(4);
msgLength.writeUInt32BE(msg.length, 0);
var id = new Buffer(this.id);
var bufMsg = Buffer.concat([randomString, msgLength, msg, id]);
// 對明文進行補位操作
var encoded = PKCS7Encoder.encode(bufMsg);
// 建立加密物件,AES採用CBC模式,資料採用PKCS#7填充;IV初始向量大小為16位元組,取AESKey前16位元組
var cipher = crypto.createCipheriv('aes-256-cbc', this.key, this.iv);
cipher.setAutoPadding(false);
var cipheredMsg = Buffer.concat([cipher.update(encoded), cipher.final()]);
// 返回加密資料的base64編碼
return cipheredMsg.toString('base64');
};
module.exports = wechatCrypto;
xml解析工具【xmlUtil.js】
var formatMessage = result => {
var message = {};
if (typeof result === 'object') {
for (var key in result) {
if (!(result[key] instanceof Array) || result[key].length === 0) {
continue;
}
if (result[key].length === 1) {
var val = result[key][0];
if (typeof val === 'object') {
message[key] = formatMessage(val);
} else {
message[key] = (val || '').trim();
}
} else {
message[key] = [];
result[key].forEach(function (item) {
message[key].push(formatMessage(item));
});
}
}
return message;
} else {
return result;
}
};
module.exports = {
formatMessage : formatMessage
};
資料庫工具【mysqlUtil.js】
const mysql = require('mysql');
const config = require('./../../config/config.local.js');
var connectionPool = mysql.createPool({
'host' : config.database.host,
'port':config.database.port,
'user' : config.database.user,
'password' : config.database.password,
'database' : config.database.database,
'charset': config.database.charset,
'connectionLimit': config.database.connectionLimit,
'supportBigNumbers': true,
'bigNumberStrings': true
});
var release = connection => {
connection.end(function(error) {
if(error) {
console.log('Connection closed failed.');
} else {
console.log('Connection closed succeeded.');
}
});
};
var execQuery = sqlOptions => {
var results = new Promise((resolve, reject) => {
connectionPool.getConnection((error,connection) => {
if(error) {
console.log("Get connection from mysql pool failed !");
throw error;
}
var sql = sqlOptions['sql'];
var args = sqlOptions['args'];
if(!args) {
var query = connection.query(sql, (error, results) => {
if(error) {
console.log('Execute query error !');
throw error;
}
resolve(results);
});
} else {
var query = connection.query(sql, args, function(error, results) {
if(error) {
console.log('Execute query error !');
throw error;
}
resolve(results);
});
}
connection.release(function(error) {
if(error) {
console.log('Mysql connection close failed !');
throw error;
}
});
});
}).then(function (chunk) {
return chunk;
});
return results;
};
module.exports = {
release : release,
execQuery : execQuery
}
http工具【httpUtils.js】
const http = require('http');
const https = require('https');
var querystring=require('querystring');
//傳送HTTP請求獲取資料方法
var doHttp_withdata = (http_options, data) => {
var message = new Promise(function(resolve, reject) {
var postData = JSON.stringify(data);
http_options.headers = {
"Content-Type": 'application/json, charset=UTF-8',
"Content-Length": new Buffer(postData).length
};
var req = http.request(http_options, function(res) {
var data = '';
res.setEncoding('utf8');
res.on('data', function (chunk) {
data += chunk;
});
res.on('end', function () {
resolve(data);
});
});
req.on('error', function(e) {
console.log('problem with request: ' + e.message);
});
req.write(postData + "\n");
req.end();
}).then(function (data) {
return data;
});
return message;
};
//傳送HTTP請求獲取資料方法
var doHttp = http_options => {
var message = new Promise(function(resolve, reject) {
var req = http.request(http_options, function(res) {
var data = '';
res.setEncoding('utf8');
res.on('data', function (chunk) {
data += chunk;
});
res.on('end', function () {
resolve(data);
});
});
req.on('error'