基于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微信公众号支付开发
Sep 19 NodeJs
3分钟快速搭建nodejs本地服务器方法运行测试html/js
Apr 01 NodeJs
详解nodejs微信公众号开发——3.封装消息响应模块
Apr 10 NodeJs
详解Windows下安装Nodejs步骤
May 18 NodeJs
nodejs模块学习之connect解析
Jul 05 NodeJs
nodejs微信扫码支付功能实现
Feb 17 NodeJs
详解redis在nodejs中的应用
May 02 NodeJs
对mac下nodejs 更新到最新版本的最新方法(推荐)
May 17 NodeJs
nodejs 使用http进行post或get请求的实例(携带cookie)
Jan 03 NodeJs
nodejs二进制与Buffer的介绍与使用
Jul 11 NodeJs
nodejs制作小爬虫功能示例
Feb 24 NodeJs
nodejs各种姿势断点调试的方法
Jun 18 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 Parse Error: syntax error, unexpected $end 错误的解决办法
2012/06/05 PHP
深入php socket的讲解与实例分析
2013/06/13 PHP
php定时计划任务与fsockopen持续进程实例
2014/05/23 PHP
PHP下的Oracle客户端扩展(OCI8)安装教程
2014/09/10 PHP
浅谈PHP Cookie处理函数
2016/06/10 PHP
PHP获取当前文件的父目录方法汇总
2016/07/21 PHP
PHP Ajax实现无刷新附件上传
2016/08/17 PHP
laravel 实现根据字段不同值做不同查询
2019/10/23 PHP
通过PHP实现获取访问用户IP
2020/05/09 PHP
简单的php购物车代码
2020/06/05 PHP
Jquery常用技巧收集整理篇
2010/11/14 Javascript
jquery实现右键菜单插件
2015/03/29 Javascript
7个jQuery最佳实践
2016/01/12 Javascript
Extjs让combobox写起来简洁又漂亮
2017/01/05 Javascript
12个非常有用的JavaScript技巧
2017/05/17 Javascript
详解在Vue中如何使用axios跨域访问数据
2017/07/07 Javascript
vue-cli V3.0版本的使用详解
2018/10/24 Javascript
详解超简单的react服务器渲染(ssr)入坑指南
2019/02/28 Javascript
基于leaflet.js实现修改地图主题样式的流程分析
2020/05/15 Javascript
Python中easy_install 和 pip 的安装及使用
2017/06/05 Python
selenium在执行phantomjs的API并获取执行结果的方法
2018/12/17 Python
python实现的登录与提交表单数据功能示例
2019/09/25 Python
Python3实现英文字母转换哥特式字体实例代码
2020/09/01 Python
css3中单位px,em,rem,vh,vw,vmin,vmax的区别及浏览器支持情况
2016/12/06 HTML / CSS
如何使用PHP session
2015/04/21 面试题
财务经理岗位职责
2013/11/09 职场文书
求职简历中个人的自我评价
2013/12/01 职场文书
复核员上岗演讲稿
2014/01/05 职场文书
运动会跳远加油稿
2014/02/20 职场文书
学生自我评语大全
2014/04/18 职场文书
一分钟演讲稿
2014/04/30 职场文书
人力资源管理毕业生自荐信
2014/06/26 职场文书
学校运动会报道稿
2014/09/23 职场文书
个人剖析材料及整改措施
2014/10/07 职场文书
个人求职意向书
2015/05/11 职场文书
大学三好学生主要事迹范文
2015/11/03 职场文书