基于NodeJS開(kāi)發(fā)釘釘回調(diào)接口實(shí)現(xiàn)AES-CBC加解密
釘釘小程序后臺(tái)接收釘釘開(kāi)放平臺(tái)的回調(diào)比較重要,比如通訊錄變動(dòng)的回調(diào),審批流程的回調(diào)都是在業(yè)務(wù)上十分需要的?;卣{(diào)接口時(shí)打通釘釘平臺(tái)和內(nèi)部系統(tǒng)的重要渠道。
但是給回調(diào)的接口增加了一些障礙,它需要支持回調(diào)的服務(wù)器的接口支持AES-CBC加解密。不然無(wú)法成功注冊(cè)或解析內(nèi)容。
釘釘官方文檔中給出了JAVA,PHP,C#的后臺(tái)SDK和demo,但是卻沒(méi)有Node服務(wù)器的代碼支持,這讓占有率很高的node服務(wù)器非常尷尬,難道node就不能作為釘釘平臺(tái)的回調(diào)服務(wù)器么

好在釘釘已經(jīng)開(kāi)放了其加密算法,可以通過(guò)加密流程自己寫(xiě)一套JavaScript版的加解密程序,然后將node服務(wù)器注冊(cè)為釘釘?shù)幕卣{(diào)接口。
首先,看一下釘釘回調(diào)接口的注冊(cè)流程

首先,是由開(kāi)發(fā)者主動(dòng)發(fā)起一個(gè)POST請(qǐng)求到釘釘開(kāi)放平臺(tái),傳過(guò)去回調(diào)的URL,然后釘釘在這個(gè)請(qǐng)求中返回一個(gè)ok,如下圖

在這里,我申請(qǐng)了通訊錄加人或修改人事件的回調(diào)。
在這個(gè)接口請(qǐng)求完畢之后,釘釘會(huì)迅速的向你請(qǐng)求參數(shù)中寫(xiě)的url發(fā)送一個(gè)POST請(qǐng)求,如下
{"encrypt":"ihVRgn3eZZrCYHfAW4Lbh9eoOcpy1VddxGS9IIYsteFgAxpPN9ZaKKp4EH/7ArtmVEACxmyGCdUFtGuXxfNfcbXXXXXXXXXXXXXXXXXXXkGy+Oq/hIN"}
此時(shí),釘釘要求我們“success”加密,然后在服務(wù)器中響應(yīng)。
AES是一種對(duì)稱性加密,即加密者通過(guò)一個(gè)密鑰進(jìn)行加密,將密文發(fā)送給接收人,接收人通過(guò)相同的密鑰進(jìn)行解密。但是CBC這種模式下,還需要一個(gè)偏移,或者說(shuō)IV向量進(jìn)行加解密。所以在加解密的時(shí)候?qū)嶋H上需要兩個(gè)參數(shù),密鑰和IV。換句話說(shuō),釘釘回調(diào)接口使用的加密方式為AES-256-CBC模式
按照文檔要求,我們返回的JSON中需要包含4個(gè)字段

其中,nonce是可以隨便寫(xiě)的字符串,長(zhǎng)度也沒(méi)有限制,是用來(lái)增加msg_signature的變化度的。
timeStamp是10位數(shù)的時(shí)間戳,JavaScript默認(rèn)時(shí)間戳是13位的,我們需要除以1000或者截取后3位。
encrypt是一段base64編碼后的字符串,被編碼的是“sucess”被加密后的密文
msg_signature是一段hash值,是將其余3個(gè)字符串,加上我們注冊(cè)接口時(shí)設(shè)定的自定義token,4個(gè)字符串排序好,通過(guò)SHA1算法HASH后的值,用來(lái)驗(yàn)證完整性的。具體如下

最難以解決的就是encrypt字段了,還好在JS界谷歌已經(jīng)給我們準(zhǔn)備好了CryptoJS庫(kù),不用幾行代碼就可以解決問(wèn)題。
首先觀察下這個(gè)encrypt字段的形成邏輯:

需要被加密的明文由四個(gè)部分組成,分別是
16個(gè)字節(jié)的隨機(jī)字符串:ASCII編碼中,一個(gè)字符就占1個(gè)字節(jié)(8位),所以這里我們隨便填16個(gè)字母組成的字符串就行
4個(gè)字節(jié)的msg長(zhǎng)度:這里的長(zhǎng)度不是文本格式的長(zhǎng)度,而是4*8=32位二進(jìn)制表示的長(zhǎng)度,文檔中沒(méi)有明確指出是填msg的字節(jié)長(zhǎng)度,還是比特位數(shù),通過(guò)我個(gè)人驗(yàn)證,此處應(yīng)該填msg的字節(jié)數(shù)。由于"success"由7個(gè)ASCII字符組成,所以長(zhǎng)度為7,以4個(gè)字節(jié)的二進(jìn)制表示就是
00000000 00000000 00000000 00000111
在JS中,要想把二進(jìn)制數(shù)轉(zhuǎn)化成字節(jié),可以先換成十進(jìn)制,然后使用String.fromCharCode(0)方法,轉(zhuǎn)換為字節(jié)。所以此處要想用字符串表示,就是把0,0,0,7當(dāng)作ASCII碼轉(zhuǎn)換為不可見(jiàn)字符
var lengthString = String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(7)
明文msg:就是字符串"success"
$key:我是企業(yè)內(nèi)部開(kāi)發(fā),Corpid可以在釘釘開(kāi)發(fā)者后臺(tái)看到
有了明文,下一步就是進(jìn)行加密
首先我們知道AES-CBC算法需要一個(gè)密鑰KEY和一個(gè)偏移量IV,而釘釘說(shuō)IV是密鑰的前16位,如下

釘釘讓我們提供32字節(jié)長(zhǎng)的密鑰,換句話說(shuō)就是256比特,然后把密鑰Base64進(jìn)行編碼,通過(guò)上面的注冊(cè)接口發(fā)給釘釘。
由于一個(gè)ASCII字符就是一個(gè)字節(jié),所以我們這里生成一個(gè)32字符長(zhǎng)度的字符串,就由密鑰了,我選擇的密鑰是"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@",因?yàn)锧字符的ASCII碼值是64,容易記,同理,IV就是16個(gè)@組成的字符串。
注意32字節(jié)的長(zhǎng)度的字符串base64編碼后,長(zhǎng)度肯定位44個(gè)字符,最后一位必然是=,去掉等號(hào)就是43個(gè)字符了
通過(guò)使用10進(jìn)制的數(shù)字,轉(zhuǎn)換為Byte字符串,也可以通過(guò)數(shù)組來(lái)解決,如上面這個(gè),我就可以通過(guò)下面代碼來(lái)生成密鑰
var key_256 = [64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64,
64, 64, 64, 64, 64, 64, 64, 64];
var key_text = '';
for(let i=0;i<32;i++){
key_text += String.fromCharCode(key_256[i]);
}
console.log(btoa(key_text))
通過(guò)JS的btoa()函數(shù),可以直接把密鑰變成Base64格式
同理,生成IV之后,就可以開(kāi)始進(jìn)行加密操作了,這里直接放出代碼
CryptoJS庫(kù)既可以在HTML中使用,也可以require到node中使用
在HTML中使用時(shí),先到https://code.google.com/archive/p/crypto-js/downloads下載最新壓縮包,然后解壓到項(xiàng)目目錄即可,如下

然后再HTML中進(jìn)行引用
<script src = "crypto-js-4.0.0/crypto-js.js"></script>
這樣我們就可以直接通過(guò)瀏覽器本地調(diào)試,生成我們想要的字符串,讓node服務(wù)器直接原文返回就可以了
<html>
<head>
<script src = "crypto-js-4.0.0/crypto-js.js"></script>
<script>
// AES 秘鑰
var AesKey = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@";
console.log(btoa(AesKey))
// AES-128-CBC偏移量
var CBCIV = "@@@@@@@@@@@@@@@@";
//16個(gè)字節(jié)的隨機(jī)字符串
var randomString = '1234567890123456';
//明文msg
var msg = 'success';
//$key,對(duì)于企業(yè)內(nèi)部開(kāi)發(fā)來(lái)說(shuō),$key填寫(xiě)企業(yè)的Corpid。
var corpid = 'ding00000035b90000000005d6980864d335'
function len_msg(msg){//該函數(shù)返回的是字符串,無(wú)文本意義
result = String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(7);
return result;
}
//msg_len(4B),此處為ASCII編碼的二進(jìn)制字符串,無(wú)文本意義
var msg_len = len_msg(msg);
//要加密的明文是[random(16B) + msg_len(4B) + msg + $key]
var codeString = randomString + msg_len + msg + corpid;
console.log('要加密的明文字符串為:'+codeString);
console.log('要加密的字符串Base64為:'+btoa(codeString));
// 加密選項(xiàng)
var CBCOptions = {
iv: CryptoJS.enc.Latin1.parse(CBCIV),
mode:CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
/**
* AES加密(CBC模式,需要偏移量)
* @param data
* @returns {*}
*/
function encrypt(data){
var key = CryptoJS.enc.Latin1.parse(AesKey);
var secretData = CryptoJS.enc.Latin1.parse(data);
var encrypted = CryptoJS.AES.encrypt(
secretData,
key,
CBCOptions
);
return encrypted.toString();
}
/**
* AES解密(CBC模式,需要偏移量)
* @param data
* @returns {*}
*/
function decrypt(data){
var key = CryptoJS.enc.Latin1.parse(AesKey);
var decrypt = CryptoJS.AES.decrypt(
data,
key,
CBCOptions
);
return CryptoJS.enc.Latin1.stringify(decrypt).toString();
}
//encrypt = Base64_Encode(AES_Encrypt[random(16B) + msg_len(4B) + msg + $key])
var encodeData=encrypt(codeString);
console.log('加密后密文為:'+encodeData);
console.log('10位時(shí)間戳:'+parseInt(new Date()/1000));
var timeStamp = ""+parseInt(new Date()/1000);
var nonce = "aaaaaa";
var encrypt = "LwJ0000000000000000000000000000000000000YYQIBxRvsQ=="
var token = '@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@';
//dev_msg_signature=sha1(sort(token、timestamp、nonce、msg_encrypt))
var sortList = [timeStamp,nonce,encrypt,token];
sortList.sort();
console.log(sortList);
var msg_signature = '';
for (i in sortList){
msg_signature += i;
}
console.log(msg_signature);
console.log(CryptoJS.SHA1(msg_signature).toString())
var secretTxt = 'Fuqa0wgIvMtUgFBnyZkCb1z3tpSYJ0000000000000000000000p64KnDZkGsjP3y5AIGnryUjkMi16Lz5C/ZzkMRbaipIgz60U5gELKSblZ3MnTf1CVbPMvyjoYbyenjbKCDmQpdgdA4Ejh8Cnlil1laZ8wQSUSD0ju8a9pFIx9Rh6HwNfh0FenpnX22HpfU000007ZjNM5PeK5DeCbmCrqnrq1zwjqomeXSw8mw9g0i83DQKYMXuU3KsO000cHPLdfbWIKUyTcw=='
var realMessage = decrypt(secretTxt);
console.log('實(shí)際內(nèi)容是'+realMessage);
</script>
</head>
<body>
</body>
</html>
注意,加密選項(xiàng)中CryptoJS.enc.Latin1.parse(AesKey);是將字符串表示的密鑰通過(guò)ASCII碼轉(zhuǎn)換為字節(jié),在加密時(shí)也可以使用CryptoJS.enc.Utf8,因?yàn)閡tf8編碼再ASCII字符中編碼沒(méi)有區(qū)別。
但是反過(guò)來(lái),加密中用Latin1和Utf8都沒(méi)有問(wèn)題,但是在解密時(shí),釘釘那邊是使用ASCII編碼的,如果使用CryptoJS.enc.Utf8就會(huì)發(fā)生錯(cuò)誤。因?yàn)獒斸敺祷貎?nèi)容應(yīng)該全是普通英文字符,沒(méi)有中文或其他特殊字符
對(duì)于消息體簽名,我們只需使用JS的arr.sort(),把四個(gè)字段組成的數(shù)組通過(guò)首字母進(jìn)行排序,然后首尾相連變?yōu)橐粋€(gè)字符串,再使用CryptoJS.SHA1(msg_signature).toString()的SHA1算法取HASH值即可,注意這里的HASH值是HEX格式表示的(文檔沒(méi)有寫(xiě),但是通過(guò)實(shí)驗(yàn)得出的),不要用Base64了,代碼上等價(jià)于
CryptoJS.SHA1(msg_signature).toString(CryptoJS.enc.Hex);
還有encrypted.toString()方法,默認(rèn)返回的就是Base64編碼格式,無(wú)需轉(zhuǎn)換,這一點(diǎn)和上面SHA1方法的默認(rèn)值不同,還有,CryptoJS.AES.decrypt()方法,傳入的待解碼的密文,也可以直接把釘釘給的Base64格式密文傳入的,無(wú)需提前解碼Base64
注意,排序四元素之一的token,既不是AES的密鑰,也不是IV,也不是釘釘平臺(tái)的access_token,而是我們?cè)谇懊?a rel="external nofollow" target="_blank" >https://oapi.dingtalk.com/call_back/register_call_back接口中上傳的token字段,是個(gè)純自定義的的字段
我們通過(guò)在瀏覽器中執(zhí)行上面的代碼,就可以把注冊(cè)回調(diào)需要返回的JSON值都獲取到,然后我們直接在node里寫(xiě)死這幾個(gè)值用來(lái)返回就可以了,同時(shí),我們還需要在nodejs中引入CryptoJS,用來(lái)對(duì)釘釘發(fā)來(lái)的回調(diào)信息進(jìn)行解密
const express = require('express')
const bodyParser = require('body-parser');
const CryptoJS = require("crypto-js");
const app = express()
const port = 8080
const appkey = 'dingxxxx';
const appsecret = 'xxxxxx';
const agentId = 'xxxxxx';
var dingToken = '';
app.get('/', (req, res) => res.send('Hello World!'))
app.listen(port, function(){
console.log(`Example app listening on port ${port}!`);
getToken();
})
app.use(bodyParser.json())
app.post('/dingCallback', function (req, res) {
console.log('釘釘回調(diào)接口收到請(qǐng)求了:'+JSON.stringify(req.body));//獲取釘釘?shù)幕卣{(diào)參數(shù)
var timeStamp = ""+parseInt(new Date()/1000);//動(dòng)態(tài)項(xiàng)
var nonce = "aaaaaa";//隨便寫(xiě)
var encrypt = "LwXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXxRvsQ=="
var token = '666666';
//dev_msg_signature=sha1(sort(token、timestamp、nonce、msg_encrypt))
var sortList = [timeStamp,nonce,encrypt,token];
sortList.sort();
console.log(sortList);
var msg_signature = '';
for (let text of sortList){
msg_signature += text;
}
console.log('msg_signature明文='+msg_signature)
msg_signature = CryptoJS.SHA1(msg_signature).toString()
var resp = {
msg_signature:msg_signature,
timeStamp:timeStamp,
nonce:nonce,
encrypt:encrypt
}
console.log(''+JSON.stringify(resp))
console.log('解密內(nèi)容是:'+decryptMsg(req.body.encrypt));//獲取釘釘傳過(guò)來(lái)的參數(shù),并解密處json信息
res.send(JSON.stringify(resp));
});
// AES 秘鑰
var AesKey = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@";
// AES-128-CBC偏移量
var CBCIV = "@@@@@@@@@@@@@@@@";
// 加密選項(xiàng)
var CBCOptions = {
iv: CryptoJS.enc.Latin1.parse(CBCIV),
mode:CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7
}
/**
* AES解密(CBC模式,需要偏移量)
* @param data Base64格式
* @returns {*}
*/
function decrypt(data){
var key = CryptoJS.enc.Latin1.parse(AesKey);
var decrypt = CryptoJS.AES.decrypt(
data,
key,
CBCOptions
);
return CryptoJS.enc.Latin1.stringify(decrypt).toString();
}
function decryptMsg(base64_crypt_msg){
var realMessage = decrypt(base64_crypt_msg);
var endPosition = realMessage.lastIndexOf('dingXXXXXXXX');//掐頭去尾,前面掐掉20字節(jié),后面掐掉Corpid
if(!realMessage || realMessage.length < 20 || endPosition==0){
console.log('解密失敗')
return;
}
var jsonData = realMessage.slice(20,endPosition);
return jsonData;
}
釘釘用于驗(yàn)證你服務(wù)器的POST請(qǐng)求,與給你發(fā)信息的回調(diào)參數(shù),格式是一樣的,POST收到的明文為:
{"encrypt":"ihVRgn3eZZrCYHfAW4Lbh9eoOcpy1VddxGS9IIYsteFgAxpPN9ZaKKp4EH/7ArtmV"}
解密之后,密文部分為一個(gè)JSON字符串,里面包含著我們想要的東西,如,用于驗(yàn)證url的參數(shù)解密后為,這個(gè)和我們?cè)O(shè)置的響應(yīng)加密字符串一樣,是16字節(jié)的隨機(jī)字符串,4個(gè)字節(jié)的二進(jìn)制長(zhǎng)度,正文+Corpid。
AzW30dHltl1iocOd{"EventType":"check_url"}dingxxxxxxxxxxxxxxxxxxxxxxx
要判斷釘釘回調(diào)我們的接口是否成功,或者說(shuō)我們有沒(méi)有返回正確的加密報(bào)文,只需調(diào)用釘釘?shù)牟榭椿卣{(diào)接口列表就行了,方法是使用POST請(qǐng)求調(diào)用https://oapi.dingtalk.com/call_back/get_call_back?access_token=,然后觀察回調(diào)接口中是否包含你剛注冊(cè)的url即可

另外推薦一個(gè)網(wǎng)站,可以將base64后的待加密字符串,使用AES-256-CBC算法進(jìn)行加解密
https://the-x.cn/cryptography/Aes.aspx

參考:https://blog.csdn.net/myzksky/article/details/82052920
到此這篇關(guān)于基于NodeJS開(kāi)發(fā)釘釘回調(diào)接口實(shí)現(xiàn)AES-CBC加解密的文章就介紹到這了,更多相關(guān)NodeJS AES-CBC加解密內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
NodeJS http模塊用法示例【創(chuàng)建web服務(wù)器/客戶端】
這篇文章主要介紹了NodeJS http模塊用法,結(jié)合實(shí)例形式分析了node.js創(chuàng)建web服務(wù)器與客戶端,進(jìn)行HTTP通信的相關(guān)操作技巧,需要的朋友可以參考下2019-11-11
基于Node-red的在線評(píng)語(yǔ)系統(tǒng)(可視化編程,公網(wǎng)訪問(wèn))
Node-Red是IBM公司開(kāi)發(fā)的一個(gè)可視化的編程工具,在網(wǎng)頁(yè)內(nèi)編程,主要是拖拽控件,代碼量很小,這篇文章主要介紹了基于Node-red的在線評(píng)語(yǔ)系統(tǒng)(可視化編程,公網(wǎng)訪問(wèn)),需要的朋友可以參考下2022-01-01
Node.js讀寫(xiě)文件之批量替換圖片的實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇Node.js讀寫(xiě)文件之批量替換圖片的實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09
Nodejs極簡(jiǎn)入門(mén)教程(二):定時(shí)器
這篇文章主要介紹了Nodejs極簡(jiǎn)入門(mén)教程(二):定時(shí)器,本文講解了setTimeout、setInterval、setImmediate及process.nextTick等內(nèi)容,需要的朋友可以參考下2014-10-10
windows系統(tǒng)中如何更新npm及node.js到最新版本
這篇文章主要介紹了windows系統(tǒng)中如何更新npm及node.js到最新版本的相關(guān)資料,文中介紹了兩種方法,還提到了一些常見(jiàn)問(wèn)題的解決方法,如權(quán)限錯(cuò)誤和使用nvm-windows管理Node.js版本,需要的朋友可以參考下2025-05-05

