基于NodeJS开发钉钉回调接口实现AES-CBC加解密


Posted in NodeJs onAugust 20, 2020

钉钉小程序后台接收钉钉开放平台的回调比较重要,比如通讯录变动的回调,审批流程的回调都是在业务上十分需要的。回调接口时打通钉钉平台和内部系统的重要渠道。

但是给回调的接口增加了一些障碍,它需要支持回调的服务器的接口支持AES-CBC加解密。不然无法成功注册或解析内容。

钉钉官方文档中给出了JAVA,PHP,C#的后台SDK和demo,但是却没有Node服务器的代码支持,这让占有率很高的node服务器非常尴尬,难道node就不能作为钉钉平台的回调服务器么

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

好在钉钉已经开放了其加密算法,可以通过加密流程自己写一套JavaScript版的加解密程序,然后将node服务器注册为钉钉的回调接口。

首先,看一下钉钉回调接口的注册流程

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

首先,是由开发者主动发起一个POST请求到钉钉开放平台,传过去回调的URL,然后钉钉在这个请求中返回一个ok,如下图

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

在这里,我申请了通讯录加人或修改人事件的回调。

在这个接口请求完毕之后,钉钉会迅速的向你请求参数中写的url发送一个POST请求,如下

{"encrypt":"ihVRgn3eZZrCYHfAW4Lbh9eoOcpy1VddxGS9IIYsteFgAxpPN9ZaKKp4EH/7ArtmVEACxmyGCdUFtGuXxfNfcbXXXXXXXXXXXXXXXXXXXkGy+Oq/hIN"}

此时,钉钉要求我们“success”加密,然后在服务器中响应。

AES是一种对称性加密,即加密者通过一个密钥进行加密,将密文发送给接收人,接收人通过相同的密钥进行解密。但是CBC这种模式下,还需要一个偏移,或者说IV向量进行加解密。所以在加解密的时候实际上需要两个参数,密钥和IV。换句话说,钉钉回调接口使用的加密方式为AES-256-CBC模式

按照文档要求,我们返回的JSON中需要包含4个字段

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

其中,nonce是可以随便写的字符串,长度也没有限制,是用来增加msg_signature的变化度的。

timeStamp是10位数的时间戳,JavaScript默认时间戳是13位的,我们需要除以1000或者截取后3位。

encrypt是一段base64编码后的字符串,被编码的是“sucess”被加密后的密文

msg_signature是一段hash值,是将其余3个字符串,加上我们注册接口时设定的自定义token,4个字符串排序好,通过SHA1算法HASH后的值,用来验证完整性的。具体如下

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

最难以解决的就是encrypt字段了,还好在JS界谷歌已经给我们准备好了CryptoJS库,不用几行代码就可以解决问题。

首先观察下这个encrypt字段的形成逻辑:

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

需要被加密的明文由四个部分组成,分别是

16个字节的随机字符串:ASCII编码中,一个字符就占1个字节(8位),所以这里我们随便填16个字母组成的字符串就行

4个字节的msg长度:这里的长度不是文本格式的长度,而是4*8=32位二进制表示的长度,文档中没有明确指出是填msg的字节长度,还是比特位数,通过我个人验证,此处应该填msg的字节数。由于"success"由7个ASCII字符组成,所以长度为7,以4个字节的二进制表示就是

00000000 00000000 00000000 00000111

在JS中,要想把二进制数转化成字节,可以先换成十进制,然后使用String.fromCharCode(0)方法,转换为字节。所以此处要想用字符串表示,就是把0,0,0,7当作ASCII码转换为不可见字符

var lengthString = String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(7)

明文msg:就是字符串"success"

$key:我是企业内部开发,Corpid可以在钉钉开发者后台看到

有了明文,下一步就是进行加密

首先我们知道AES-CBC算法需要一个密钥KEY和一个偏移量IV,而钉钉说IV是密钥的前16位,如下

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

钉钉让我们提供32字节长的密钥,换句话说就是256比特,然后把密钥Base64进行编码,通过上面的注册接口发给钉钉。

由于一个ASCII字符就是一个字节,所以我们这里生成一个32字符长度的字符串,就由密钥了,我选择的密钥是"@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@",因为@字符的ASCII码值是64,容易记,同理,IV就是16个@组成的字符串。

注意32字节的长度的字符串base64编码后,长度肯定位44个字符,最后一位必然是=,去掉等号就是43个字符了

通过使用10进制的数字,转换为Byte字符串,也可以通过数组来解决,如上面这个,我就可以通过下面代码来生成密钥

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))

通过JS的btoa()函数,可以直接把密钥变成Base64格式

同理,生成IV之后,就可以开始进行加密操作了,这里直接放出代码

CryptoJS库既可以在HTML中使用,也可以require到node中使用

在HTML中使用时,先到https://code.google.com/archive/p/crypto-js/downloads下载最新压缩包,然后解压到项目目录即可,如下

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

然后再HTML中进行引用

<script src = "crypto-js-4.0.0/crypto-js.js"></script>

这样我们就可以直接通过浏览器本地调试,生成我们想要的字符串,让node服务器直接原文返回就可以了

<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个字节的随机字符串
var randomString = '1234567890123456';
//明文msg
var msg = 'success';
//$key,对于企业内部开发来说,$key填写企业的Corpid。
var corpid = 'ding00000035b90000000005d6980864d335'

function len_msg(msg){//该函数返回的是字符串,无文本意义
	result = String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(0)+String.fromCharCode(7);
	return result;
}
//msg_len(4B),此处为ASCII编码的二进制字符串,无文本意义
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));
 
 
// 加密选项
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位时间戳:'+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('实际内容是'+realMessage);
</script>
</head>
<body>
</body>
</html>

注意,加密选项中CryptoJS.enc.Latin1.parse(AesKey);是将字符串表示的密钥通过ASCII码转换为字节,在加密时也可以使用CryptoJS.enc.Utf8,因为utf8编码再ASCII字符中编码没有区别。

但是反过来,加密中用Latin1和Utf8都没有问题,但是在解密时,钉钉那边是使用ASCII编码的,如果使用CryptoJS.enc.Utf8就会发生错误。因为钉钉返回内容应该全是普通英文字符,没有中文或其他特殊字符

对于消息体签名,我们只需使用JS的arr.sort(),把四个字段组成的数组通过首字母进行排序,然后首尾相连变为一个字符串,再使用CryptoJS.SHA1(msg_signature).toString()的SHA1算法取HASH值即可,注意这里的HASH值是HEX格式表示的(文档没有写,但是通过实验得出的),不要用Base64了,代码上等价于

CryptoJS.SHA1(msg_signature).toString(CryptoJS.enc.Hex);

还有encrypted.toString()方法,默认返回的就是Base64编码格式,无需转换,这一点和上面SHA1方法的默认值不同,还有,CryptoJS.AES.decrypt()方法,传入的待解码的密文,也可以直接把钉钉给的Base64格式密文传入的,无需提前解码Base64

注意,排序四元素之一的token,既不是AES的密钥,也不是IV,也不是钉钉平台的access_token,而是我们在前面https://oapi.dingtalk.com/call_back/register_call_back接口中上传的token字段,是个纯自定义的的字段

我们通过在浏览器中执行上面的代码,就可以把注册回调需要返回的JSON值都获取到,然后我们直接在node里写死这几个值用来返回就可以了,同时,我们还需要在nodejs中引入CryptoJS,用来对钉钉发来的回调信息进行解密

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('钉钉回调接口收到请求了:'+JSON.stringify(req.body));//获取钉钉的回调参数
 
 var timeStamp = ""+parseInt(new Date()/1000);//动态项
 var nonce = "aaaaaa";//随便写
 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('解密内容是:'+decryptMsg(req.body.encrypt));//获取钉钉传过来的参数,并解密处json信息
 res.send(JSON.stringify(resp));
 
});

// AES 秘钥
var AesKey = "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@";

// AES-128-CBC偏移量
var CBCIV = "@@@@@@@@@@@@@@@@";

// 加密选项
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字节,后面掐掉Corpid
	if(!realMessage || realMessage.length < 20 || endPosition==0){
		console.log('解密失败')
		return;
	}
	var jsonData = realMessage.slice(20,endPosition);
	return jsonData;
}

钉钉用于验证你服务器的POST请求,与给你发信息的回调参数,格式是一样的,POST收到的明文为:

{"encrypt":"ihVRgn3eZZrCYHfAW4Lbh9eoOcpy1VddxGS9IIYsteFgAxpPN9ZaKKp4EH/7ArtmV"}

 解密之后,密文部分为一个JSON字符串,里面包含着我们想要的东西,如,用于验证url的参数解密后为,这个和我们设置的响应加密字符串一样,是16字节的随机字符串,4个字节的二进制长度,正文+Corpid。

AzW30dHltl1iocOd{"EventType":"check_url"}dingxxxxxxxxxxxxxxxxxxxxxxx

要判断钉钉回调我们的接口是否成功,或者说我们有没有返回正确的加密报文,只需调用钉钉的查看回调接口列表就行了,方法是使用POST请求调用https://oapi.dingtalk.com/call_back/get_call_back?access_token=,然后观察回调接口中是否包含你刚注册的url即可

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

另外推荐一个网站,可以将base64后的待加密字符串,使用AES-256-CBC算法进行加解密

https://the-x.cn/cryptography/Aes.aspx

基于NodeJS开发钉钉回调接口实现AES-CBC加解密

参考:https://blog.csdn.net/myzksky/article/details/82052920

到此这篇关于基于NodeJS开发钉钉回调接口实现AES-CBC加解密的文章就介绍到这了,更多相关NodeJS AES-CBC加解密内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

NodeJs 相关文章推荐
用nodejs写的一个简单项目打包工具
May 11 NodeJs
nodejs教程之环境安装及运行
Nov 21 NodeJs
nodejs事件的监听与触发的理解分析
Feb 12 NodeJs
浅谈Nodejs应用主文件index.js
Aug 28 NodeJs
NodeJS整合银联网关支付(DEMO)
Nov 09 NodeJs
nodejs基础知识
Feb 03 NodeJs
nodejs搭建本地服务器并访问文件的方法
Mar 03 NodeJs
详解使用nodeJs安装Vue-cli
May 17 NodeJs
nodejs实现爬取网站图片功能
Dec 14 NodeJs
NodeJS读取分析Nginx错误日志的方法
May 14 NodeJs
详解nodejs内置模块
May 06 NodeJs
浅谈使用nodejs搭建web服务器的过程
Jul 20 #NodeJs
通过实例了解Nodejs模块系统及require机制
Jul 16 #NodeJs
Nodejs环境实现socket通信过程解析
Jul 03 #NodeJs
使用nodejs实现JSON文件自动转Excel的工具(推荐)
Jun 24 #NodeJs
nodejs各种姿势断点调试的方法
Jun 18 #NodeJs
在NodeJs中使用node-schedule增加定时器任务的方法
Jun 08 #NodeJs
nodeJS与MySQL实现分页数据以及倒序数据
Jun 05 #NodeJs
You might like
php中的动态调用实例分析
2015/01/07 PHP
php实现window平台的checkdnsrr函数
2015/05/27 PHP
Yii2基于Ajax自动获取表单数据的方法
2016/08/10 PHP
PHP去除字符串最后一个字符的三种方法实例
2017/03/01 PHP
js正则表达式的使用详解
2013/07/09 Javascript
js实现有时间限制消失的图片方法
2015/02/27 Javascript
使用Bootstrap Tabs选项卡Ajax加载数据实现
2016/12/23 Javascript
JS实现浏览器打印、打印预览示例
2017/02/28 Javascript
Bootstrap表单制作代码
2017/03/17 Javascript
深入理解vue-loader如何使用
2017/06/06 Javascript
vue+vux实现移动端文件上传样式
2017/07/28 Javascript
从0到1搭建Element的后台框架的方法步骤
2019/04/10 Javascript
[42:06]2019国际邀请赛全明星赛 8.23
2019/09/05 DOTA
[54:43]DOTA2-DPC中国联赛 正赛 CDEC vs Dynasty BO3 第一场 2月22日
2021/03/11 DOTA
wxpython 学习笔记 第一天
2009/03/16 Python
让python的Cookie.py模块支持冒号做key的方法
2010/12/28 Python
Python基础教程之正则表达式基本语法以及re模块
2016/03/25 Python
python实现按任意键继续执行程序
2016/12/30 Python
windows下cx_Freeze生成Python可执行程序的详细步骤
2018/10/09 Python
对Python中class和instance以及self的用法详解
2019/06/26 Python
Python3 pandas 操作列表实例详解
2019/09/23 Python
python GUI库图形界面开发之PyQt5控件QTableWidget详细使用方法与属性
2020/02/25 Python
Python图像处理库PIL的ImageDraw模块介绍详解
2020/02/26 Python
python列表的逆序遍历实现
2020/04/20 Python
HTML5和CSS3让网页设计提升到下一个高度
2009/08/14 HTML / CSS
法国美发器材和产品购物网站:Beauty Coiffure
2016/12/05 全球购物
巴西最大的在线约会网站:ParPerfeito
2018/07/11 全球购物
英语专业毕业生自荐信
2013/10/28 职场文书
打架检讨书400字
2014/01/17 职场文书
《胡杨》教学反思
2014/02/16 职场文书
《画》教学反思
2014/04/14 职场文书
学雷锋演讲稿汇总
2014/05/10 职场文书
2014年信访维稳工作总结
2014/12/08 职场文书
2015年副班长工作总结
2015/05/15 职场文书
2016年中秋节晚会领导致辞
2015/11/26 职场文书
PHP面试题 wakeup魔法 Ezpop pop序列化与反序列化
2022/04/11 PHP