基于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 26 NodeJs
nodejs开发环境配置与使用
Nov 17 NodeJs
nodejs 提示‘xxx’ 不是内部或外部命令解决方法
Nov 20 NodeJs
Nodejs学习笔记之测试驱动
Apr 16 NodeJs
详解nodejs express下使用redis管理session
Apr 24 NodeJs
NodeJs的fs读写删除移动监听
Apr 28 NodeJs
NodeJS自定义模块写法(详解)
Jun 27 NodeJs
windows系统下更新nodejs版本的方案
Nov 24 NodeJs
nodejs实现大文件(在线视频)的读取
Oct 16 NodeJs
nodeJs实现基于连接池连接mysql的方法示例
Feb 10 NodeJs
nodejs 简单实现动态html的方法
May 12 NodeJs
浅谈vue websocket nodeJS 进行实时通信踩到的坑
Sep 22 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中开启gzip压缩的2种方法
2015/01/31 PHP
Zend Framework教程之路由功能Zend_Controller_Router详解
2016/03/07 PHP
浅谈PHP值mysql操作类
2016/06/29 PHP
thinkphp 字母函数详解T/I/N/D/M/A/R/U
2017/04/03 PHP
Laravel 5.4因特殊字段太长导致migrations报错的解决
2017/10/22 PHP
Yii2框架配置文件(Application属性)与调试技巧实例分析
2019/05/27 PHP
jquery 弹出层注册页面等(asp.net后台)
2010/06/17 Javascript
javaScript 动态访问JSon元素示例代码
2013/08/30 Javascript
jQuery学习笔记之jQuery动画效果
2013/09/09 Javascript
jQuery处理xml格式的返回数据(实例解析)
2013/11/28 Javascript
jQuery实现加入购物车飞入动画效果
2015/03/14 Javascript
jQuery插件slicebox实现3D动画图片轮播切换特效
2015/04/12 Javascript
jQuery固定元素插件scrolltofixed使用指南
2015/04/21 Javascript
js+css实现select的美化效果
2016/03/24 Javascript
javascript 判断页面访问方式电脑或者移动端
2016/09/19 Javascript
bootstrap网格系统使用方法解析
2017/01/13 Javascript
原生js仿淘宝网商品放大镜效果
2017/02/28 Javascript
使用AngularJS2中的指令实现按钮的切换效果
2017/03/27 Javascript
Angular.js去除页面中显示的空行方法示例
2017/03/30 Javascript
Ext JS 实现建议词模糊动态搜索功能
2017/05/13 Javascript
js实现移动端导航点击自动滑动效果
2017/07/18 Javascript
解决vue中post方式提交数据后台无法接收的问题
2018/08/11 Javascript
JavaScript递归函数定义与用法实例分析
2019/01/24 Javascript
jQuery列表动态增加和删除的实现方法
2020/11/05 jQuery
跟老齐学Python之Python安装
2014/09/12 Python
Python实现字典的key和values的交换
2015/08/04 Python
Python编程入门之Hello World的三种实现方式
2015/11/13 Python
Python实现迭代时使用索引的方法示例
2018/06/05 Python
联想韩国官网:Lenovo Korea
2018/05/10 全球购物
大学生应聘推荐信范文
2013/11/19 职场文书
酒会开场白大全
2015/06/01 职场文书
聘任合同书
2015/09/21 职场文书
英语版自我评价,35句话轻松搞定
2019/10/08 职场文书
idea以任意顺序debug多线程程序的具体用法
2021/08/30 Java/Android
错误码NET::ERR_CERT_DATE_INVALID证书已过期解决方法?
2022/07/07 数码科技
windows10 家庭版下FTP服务器搭建教程
2022/08/05 Servers