下面是关于基于NodeJS开发钉钉回调接口实现AES-CBC加解密的完整攻略。
简介
钉钉回调接口是钉钉提供的一种主动通知机制,允许开发者注册特定类型的事件(比如用户离职、群组变化等),当事件发生时,钉钉会向开发者指定的服务器推送消息,以便开发者及时获取钉钉中发生的各种变化情况。
为保证安全性,钉钉回调接口推送的消息采用了AES-CBC加密方式,需要在服务器端对消息进行解密才能得到其真正内容。
此文档将讲述如何基于NodeJS开发钉钉回调接口实现消息的AES-CBC加解密。
开发环境
- Node.js 10.x
- npm 6.x
步骤
步骤1:安装必要的依赖
使用Node.js开发钉钉回调接口需要使用到一些第三方库来辅助开发,这些库可以通过npm包管理器进行安装。在命令行中执行以下命令:
npm install express body-parser crypto --save
其中:
- express:一个流行的Node.js Web框架,用于搭建服务器端的Web应用程序;
- body-parser:一个Node.js中间件,用于解析HTTP请求体中的数据;
- crypto:Node.js支持的加密解密库。
步骤2:创建服务器
创建一个express实例:
const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
const port = process.env.PORT || 3000
app.listen(port, () => {
console.log(`Server is listening on port ${port}...`)
})
在此示例中,我们使用express以及body-parser来解析HTTP请求,在端口3000上启动HTTP服务器。
步骤3:接收推送消息并解密
当钉钉推送消息到服务器时,我们需要对消息进行解密以获取其真实内容。下面是解密的具体步骤:
3.1:获取消息体和签名
钉钉推送的消息体保存在HTTP请求体的encrypt
参数中,签名则在HTTP请求头的x-ns-signature
参数中,因此,我们需要从HTTP请求中提取这两个参数。
app.post('/', (req, res) => {
const encrypt = req.body.encrypt
const signature = req.headers['x-ns-signature']
...
})
3.2:验证签名
我们需要将消息体和回调url上的token按照顺序拼接,接着使用签名算法(HMAC_SHA256)对拼接后的字符串进行HMAC_SHA256计算,最后将计算结果转为16进制表示的字符串。最终的签名结果应该是和HTTP请求头中的x-ns-signature
参数中的值相同。
app.post('/', (req, res) => {
const encrypt = req.body.encrypt
const signature = req.headers['x-ns-signature']
// 验证签名是否正确
const token = 'your_token'
const timestamp = req.headers['x-ns-timestamp']
const plainText = token + timestamp + encrypt
const hmac = crypto.createHmac('sha256', token)
hmac.update(plainText)
if (signature !== hmac.digest('hex')) {
res.status(401).send('signature invalid')
return
}
...
})
3.3:解密消息体
我们需要使用AES-CBC算法解密消息体,首先需要解密Base64编码后的AES密钥(HTTP请求头的x-ns-ekey参数),得到真正的AES密钥;接着使用AES密钥和IV(初始向量,HTTP请求头的x-ns-iv参数),对消息体进行解密,得到消息的原始内容。
app.post('/', (req, res) => {
const encrypt = req.body.encrypt
const signature = req.headers['x-ns-signature']
// 验证签名是否正确
const token = 'your_token'
const timestamp = req.headers['x-ns-timestamp']
const plainText = token + timestamp + encrypt
const hmac = crypto.createHmac('sha256', token)
hmac.update(plainText)
if (signature !== hmac.digest('hex')) {
res.status(401).send('signature invalid')
return
}
// 解密消息体
const ekey = Buffer.from(req.headers['x-ns-ekey'], 'base64')
const iv = Buffer.from(req.headers['x-ns-iv'], 'base64')
const decipher = crypto.createDecipheriv('aes-256-cbc', ekey, iv)
let decrypted = decipher.update(encrypt, 'base64', 'utf8')
decrypted += decipher.final('utf8')
console.log(decrypted)
...
})
最终的解密结果即为变量decrypted
中保存的内容。
步骤4:构造回复消息并加密
通常情况下,我们需要在接收到消息之后向钉钉服务端回复一条确认消息。回复消息也需要进行加密处理,加密时需要按照以下步骤进行:
4.1:构造回复明文
回复消息的明文格式必须与接收到的消息格式相同,所以我们需要将解密后的消息体中的msgtype
值复制到回复消息中,同时指定一个响应指令(HTTP响应头的x-ns-responsekey参数)。
app.post('/', (req, res) => {
const encrypt = req.body.encrypt
const signature = req.headers['x-ns-signature']
// 验证签名是否正确
const token = 'your_token'
const timestamp = req.headers['x-ns-timestamp']
const plainText = token + timestamp + encrypt
const hmac = crypto.createHmac('sha256', token)
hmac.update(plainText)
if (signature !== hmac.digest('hex')) {
res.status(401).send('signature invalid')
return
}
// 解密消息体
const ekey = Buffer.from(req.headers['x-ns-ekey'], 'base64')
const iv = Buffer.from(req.headers['x-ns-iv'], 'base64')
const decipher = crypto.createDecipheriv('aes-256-cbc', ekey, iv)
let decrypted = decipher.update(encrypt, 'base64', 'utf8')
decrypted += decipher.final('utf8')
console.log(decrypted)
// 构造回复消息
const response = {
msgtype: JSON.parse(decrypted).msgtype,
... // 其他自定义属性
}
const responseText = JSON.stringify(response)
const responseKey = 'your_response_key'
...
})
4.2:加密回复密文
使用步骤3.3中获取到的AES-CBC密钥和IV,对回复消息进行加密处理,得到回复密文。
app.post('/', (req, res) => {
const encrypt = req.body.encrypt
const signature = req.headers['x-ns-signature']
// 验证签名是否正确
const token = 'your_token'
const timestamp = req.headers['x-ns-timestamp']
const plainText = token + timestamp + encrypt
const hmac = crypto.createHmac('sha256', token)
hmac.update(plainText)
if (signature !== hmac.digest('hex')) {
res.status(401).send('signature invalid')
return
}
// 解密消息体
const ekey = Buffer.from(req.headers['x-ns-ekey'], 'base64')
const iv = Buffer.from(req.headers['x-ns-iv'], 'base64')
const decipher = crypto.createDecipheriv('aes-256-cbc', ekey, iv)
let decrypted = decipher.update(encrypt, 'base64', 'utf8')
decrypted += decipher.final('utf8')
console.log(decrypted)
// 构造回复消息
const response = {
msgtype: JSON.parse(decrypted).msgtype,
... // 其他自定义属性
}
const responseText = JSON.stringify(response)
const responseKey = 'your_response_key'
// 加密回复消息
const responseIv = crypto.randomBytes(16)
const ekey2 = crypto.createHash('sha256').update(responseKey).digest().slice(0, 32)
const cipher = crypto.createCipheriv('aes-256-cbc', ekey2, responseIv)
let encrypted = cipher.update(responseText, 'utf8', 'base64')
encrypted += cipher.final('base64')
res.header('Content-Type', 'application/json')
res.header('x-ns-ekey', Buffer.from(ekey2).toString('base64'))
res.header('x-ns-iv', responseIv.toString('base64'))
res.header('x-ns-responsekey', 'your_response_key')
res.send({ encrypt: encrypted })
})
最终得到的回复消息密文保存在变量encrypted
中,可以将其直接返回到钉钉服务器。
示例
下面是两个钉钉回调接口的示例,可供参考:
示例1:获取用户信息
请求
POST /user/get HTTP/1.1
Host: your_callback_url
x-ns-signature: 1ca36f3b164901f28a5e83aa4fced54acc73e2beabcedf9ded100bf682588bbc
x-ns-timestamp: 1580469492913
Content-Type: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 264
{
"encrypt": "rkoZgNlKv...TBlxdFxmlF/S452J1Y4LoJQb24="
}
其中encrypt
参数为加密后的消息体。
响应
HTTP/1.1 200 OK
Content-Type: application/json
x-ns-ekey: RoTyMNawUTwWHGQBNSz1GfWb+3qGOU6hJo57eOhy/vA=
x-ns-iv: OQtLErqQyKK+bl4b+1b1Tg==
x-ns-responsekey: your_response_key
{
"encrypt": "lpikvT7NVJYW+...5U+IKTn5UPNsDD04g1qXlw=="
}
其中encrypt
参数为加密后的响应消息体。
示例2:更新部门信息
请求
POST /department/update HTTP/1.1
Host: your_callback_url
x-ns-signature: 1ca36f3b164901f28a5e83aa4fced54acc73e2beabcedf9ded100bf682588bbc
x-ns-timestamp: 1580469492913
Content-Type: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Length: 264
{
"encrypt": "rkoZgNlKv...TBlxdFxmlF/S452J1Y4LoJQb24="
}
其中encrypt
参数为加密后的消息体。
响应
HTTP/1.1 200 OK
Content-Type: application/json
x-ns-ekey: RoTyMNawUTwWHGQBNSz1GfWb+3qGOU6hJo57eOhy/vA=
x-ns-iv: OQtLErqQyKK+bl4b+1b1Tg==
x-ns-responsekey: your_response_key
{
"encrypt": "lpikvT7NVJYW+...5U+IKTn5UPNsDD04g1qXlw=="
}
其中encrypt
参数为加密后的响应消息体。
总结
本文介绍了如何使用Node.js开发钉钉回调接口的AES-CBC加解密功能,主要包括解析HTTP请求、检测签名正确性、解密回调消息体、加密回复消息体等步骤。
在实际开发中,需要注意检测签名的正确性,以保证消息能够安全可靠地在客户端和服务端之间传递。另外,需要注意加解密所使用的算法和密钥,这些信息需要保密存储,避免泄漏给攻击者。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:基于NodeJS开发钉钉回调接口实现AES-CBC加解密 - Python技术站