附录
优质
小牛编辑
129浏览
2023-12-01
附录
签名signature校验
signature是签名,用于验证调用者的合法性
校验流程:
- 将rn、timestamp、Token三个参数字符串拼接成一个字符串后进行md5
- 加密后获得的字符串与请求中的signature对比,标识该请求来源于如流平台
同时还可通过记录signature、timestamp防止重放攻击。
参数说明:
参数 | 类型 | 参数位置 | 必须 | 说明 |
---|---|---|---|---|
rn | Integer | Query参数 | 是 | 请求中的rn |
timestamp | Integer | Query参数 | 是 | 请求中的timestamp |
Token | String | Query参数 | 是 | 管理后台应用详情-开发信息-回调配置获取的Token值 |
参考代码:
private static boolean checkSignature(HttpServletRequest request) throws Exception {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String rn = request.getParameter("rn");
String str = DigestUtils.md5Hex((rn + timestamp + Access_Token).getBytes("utf-8"));
return signature.equals(str);
}
消息/事件内容解密
如流企业平台将消息或者事件明文经过AES加密,然后对加密后内容进行Base64编码成密文,密文为Encrypt_Msg = Base64_Encode(AES_Encrypt(msg)),msg为消息或事件明文
加密算法:AES-128
加密模式:ECB
填充方式:PKCS5(PKCS7兼容)
编码方式:Base64URLSafe(它不在末尾填充=号,并将标准Base64中的+和/分别改成了-和_)
解密流程:
- Base64解码,Encrypt_Raw_Msg = Base64URLSafeDecode(Encrypt_Msg) 特别提醒:某些语言(比如Python)Base64URLSafeDecode方法不能处理尾部缺失=的情况,需要自行补齐=(len(Encrypt_Msg)% 4)
- AES解密,msg = AES_Decrypt(Encrypt_Raw_Msg, AESKey)
参数说明:
参数 | 说明 |
---|---|
EncodingAESKey | 企业平台生成,长度固定为22个字符 |
AESKey | AES算法的密钥 AESKey = Base64_Decode(EncodingAESKey + "=="),长度为16字节。 |
各语言示例: 主要包含signature校验和加解密示例,加密的消息或事件服务号消息和机器人消息本身内容的获取请查看接收消息和事件。
JAVA示例
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
String echostr = httpServletRequest.getParameter("echostr");
// 配置回调地址时应回调服务就绪,配置回调地址时会调用配置地址,需要回显才能校验通过
if (echostr != null && echostr.length() != 0) {
if (checkSignature(request)) {
httpServletResponse.getWriter().print(echostr);
}
} else {
byte[] msgBase64 = Base64.decodeBase64(IOUtils.toString(httpServletRequest.getInputStream(), "utf-8"));
Charset charset = Charset.forName("utf-8");
SecretKeySpec skeySpec = new SecretKeySpec(Base64.decodeBase64(encodingAESKey), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, skeySpec);
byte[] decrypted = cipher.doFinal(msgBase64);
// 通过AES解密后得到回调消息数据
String msgjsonStr = new String(decrypted, charset);
}
Python示例
#!/usr/bin/python
# -*- coding: utf-8 -*-
import base64
import binascii
import six
# pip install pycrypto
# Windows No module named Crypto.Cipher问题,https://www.jianshu.com/p/09a14a61b454
from Crypto.Cipher import AES
def base64_urlsafe_decode(s):
"""
base64 解码(urlsafe兼容模式)
:return:
"""
# 系统的urlsafe_b64decode方法不支持补'='
s = s.replace('-', '+').replace('_', '/') + '=' * (len(s) % 4)
return base64.b64decode(s)
class AESCipher(object):
"""
AES加解密类
"""
def __init__(self, key, mode=AES.MODE_ECB, padding='PKCS7', encode='base64', **kwargs):
"""
初始化
:param key:
:param mode:
:param padding: 数据填充方式 PKCS7、ZERO
:param encode: 数据编码方式 raw、base64、hex
"""
self.key = key
self.mode = mode
self.padding = padding
self.encode = encode
self.kwargs = kwargs
self.bs = AES.block_size
self.IV = self.kwargs.get('IV', None)
if self.IV and self.mode in (AES.MODE_ECB, AES.MODE_CTR):
raise TypeError("ECB and CTR mode does not use IV")
def _aes(self):
return AES.new(self.key, self.mode, **self.kwargs)
def encrypt(self, plaintext):
"""
加密
:param plaintext:
:return: py3返回 byte string, py2返回str
"""
# padding https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS#5_and_PKCS#7
if self.padding == 'PKCS7':
pad = lambda s: s + (self.bs - len(s) % self.bs) \
* chr(self.bs - len(s) % self.bs).encode('utf-8')
else:
pad = lambda s: s + (self.bs - len(s) % self.bs) \
* '\x00'
# 统一为字节类型
if isinstance(plaintext, six.text_type):
plaintext = plaintext.encode('utf-8')
# 注意:加密、解密需单独实例化
raw = self._aes().encrypt(pad(plaintext))
if self.encode == 'hex':
return binascii.hexlify(raw)
if self.encode == 'base64':
return base64.b64encode(raw)
return raw
def decrypt(self, ciphertext):
"""
解密
:param ciphertext:
:return: py3返回 byte string, py2返回str
"""
if not ciphertext:
return None
if self.padding == 'PKCS7':
if six.PY3:
unpad = lambda s: s[0:-s[-1]]
else:
unpad = lambda s: s[0:-ord(s[-1])]
else:
unpad = lambda s: s.rstrip('\x00')
# 统一为文本字符类型
if isinstance(ciphertext, six.binary_type) and self.encode != 'raw':
ciphertext = ciphertext.decode('utf-8')
if self.encode == 'hex':
ciphertext = binascii.unhexlify(ciphertext)
if self.encode == 'base64':
ciphertext = base64_urlsafe_decode(ciphertext)
return unpad(self._aes().decrypt(ciphertext))
if __name__ == '__main__':
# flask示例(推荐>=1.1.0),仅核心逻辑代码,其它web框架类比参考
from flask import request
# 获取POST参数echostr,Django可使用request.POST.get('echostr', None)
# 注意:仅回调配置的时候有此参数,需要指定缺省值
echostr = request.form.get('echostr', None)
# 配置回调地址时应回调服务就绪,配置回调地址时会调用配置地址,需要回显才能校验通过
if echostr:
if check_signature(signature, rn, timestamp, token):
return echostr
else:
return 'check signature fail'
else:
# 获取request raw body, Django可使用request.body
msg_base64 = request.get_data()
encrypter = AESCipher(base64_urlsafe_decode(encodingAESKey))
# 通过AES解密后得到回调消息数据
decrypted = encrypter.decrypt(msg_base64)
msg_data = json.loads(decrypted)
PHP示例
<?php
class Encrypter
{
private $method;
private $password;
private $padding;
private $iv;
private $lastErrorMessage; // 最后一次失败的信息
const AES_BLOCK_SIZE = 16;
/**
* Encrypter constructor.
*
* @param string $password aes_key
* @param string $method 加密的模式
* @param string $padding 填充数据模式, 支持ZERO、PKCS7(兼容PKCS5)
* @param string $iv 初始向量, 默认使用AES-128-ECB初始向量为空
*/
function __construct($password, $method = 'AES-128-ECB', $padding = 'PKCS7', $iv = '')
{
$this->password = $password;
$this->method = $method;
$this->padding = $padding;
$this->iv = $iv;
}
/**
* 加密
*
* @param string $data 需要加密的字符串
*
* @return false|string 失败时返回false
*/
public function encrypt($data)
{
/**
* openssl_encrypt ZERO_PADDING模式有个小问题
* 当设置为OPENSSL_ZERO_PADDING时其实是禁用padding
* EVP_CIPHER_CTX_set_padding(&cipher_ctx, 0);
* 因此需要构造合适长度的被加密串, 这样加密过程中就不需要padding了,不然会加密失败
* 不过需要注意的是因为padding了\0字符 解密完成后需要trim。显而易见,若是被加密串两端本身就包含\0等字符就会丢失信息
*
* 参考资料: http://php.net/manual/en/function.openssl-encrypt.php#117208
* http://php.net/manual/en/function.openssl-encrypt.php#117499
* http://www.cnblogs.com/solohac/p/4284424.html
* http://stackoverflow.com/questions/11838197/php-vs-java-aes-encryption-what-is-the-difference
*/
if ($this->padding === 'ZERO') {
$options = OPENSSL_ZERO_PADDING;
$padLen = self::AES_BLOCK_SIZE - (strlen($data) % self::AES_BLOCK_SIZE);
$data .= str_repeat("\0", $padLen); // zero padding
} else {
// 默认值, 使用PKCS7(兼容PKCS5) padding方式
$options = 0;
}
try {
$encryptedData = openssl_encrypt(
$data, $this->method, $this->password, $options, $this->iv
);
} catch (Exception $e) {
$this->lastErrorMessage = $e;
return false;
}
return $encryptedData;
}
/**
* 解密
*
* @param string $data 需要解密的字符串
*
* @return string|false 失败时返回false
*/
public function decrypt($data)
{
if (empty($data)) {
return null;
}
// 预先 decode
// base64 url safe decode
$data = base64_decode(str_pad(
strtr($data, '-_', '+/'),
strlen($data) % 4,
'=',
STR_PAD_RIGHT
));
if ($this->padding === 'ZERO') {
$options = OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING;
} else {
// PKCS7(兼容PKCS5)padding,openssl默认方式
$options = OPENSSL_RAW_DATA;
}
try {
$decryptedData = openssl_decrypt(
$data, $this->method, $this->password, $options, $this->iv
);
} catch (Exception $e) {
$this->lastErrorMessage = $e;
return false;
}
// 解密后的数据两端可能有padding的空字符, 需要调用方按需trim
return $decryptedData;
}
/**
* 获取最后一次报错的信息
*
* @return string
*/
public function getLastErrorMessage()
{
return sprintf(
"last error: %s, openssl error: %s",
$this->lastErrorMessage, openssl_error_string()
);
}
}
// 配置回调地址时应回调服务就绪,配置回调地址时会调用配置地址,需要回显才能校验通过
if (!empty($_POST['echostr'])) {
if (check_signature($signature, $rn, $timestamp, $token)) {
echo $_POST['echostr'];
}
else {
echo "check signature fail";
}
} else {
$msgBase64 = file_get_contents("php://input");
// 通过AES解密后得到回调消息数据
$encrypter = new Encrypter(base64_decode($encodingAESKey));
$decrypted = $encrypter->decrypt($msgBase64);
$msgData = json_decode(trim($decrypted), true);
}
Node.js示例
const Koa = require('koa');
const bodyParser = require('koa-bodyparser');
const CryptoJS = require('crypto-js');
// 需替换成自己机器人的密匙
const EncodingAESKey = '';
class AESCipher {
constructor(key) {
this.key = CryptoJS.enc.Base64.parse(key);
this.options = {
mode: CryptoJS.mode.ECB,
padding: CryptoJS.pad.Pkcs7
};
}
// 加密
encrypt(data) {
const cipher = CryptoJS.AES.encrypt(data, this.key, this.options);
const base64Cipher = cipher.ciphertext.toString(CryptoJS.enc.Base64);
const resultCipher = base64Cipher
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/g, '');
return resultCipher;
}
// 解密
decrypt(content) {
content = content
.replace(/-/g, '+')
.replace(/_/g, '/')
.padEnd(content.length + content.length % 4, '=')
const bytes = CryptoJS.AES.decrypt(content, this.key, this.options);
return bytes.toString(CryptoJS.enc.Utf8);
}
}
const cipher = new AESCipher(EncodingAESKey);
const app = new Koa();
app.use(bodyParser({enableTypes: ['json', 'form', 'text']}));
app.use(async function (ctx, next) {
if (ctx.method === 'POST') {
// 获取POST参数echostr
if (ctx.request.body.echostr) {
if (check_signature(signature, rn, timestamp, token)) {
// 配置回调地址时应回调服务就绪,配置回调地址时会调用配置地址,需要回显才能校验通过
ctx.body = ctx.request.body.echostr;
}
else {
ctx.body = 'check signaturefail';
}
} else {
// 获取request raw body,不同框架可能略有不同
let data = ctx.request.rawBody;
if (!data) {
console.error('未获取到数据对象');
}
// 解密
data = cipher.decrypt(data);
console.log('消息体内容', data);
// 转换为对象
data = JSON.parse(data);
}
} else {
ctx.body = 'GET request OK'
}
});
问题定位
企业平台开放接口(https://api.im.baidu.com
)会在response header中带上X-Logid,为方便定位,强烈建议业务方在打印调用错误日志时同时打印X-Logid。
全局错误码
返回码 | 说明 |
---|---|
0 | 请求成功 |
-1 | 系统错误 |
40000 | 请求参数错误 |
40001 | 无权限 |
40013 | 不合法的corpid |
40020 | 文件已存在 |
40021 | agentid失效 |
40022 | 上报超时 |
40023 | 上传成功 |
40025 | 文件不完整 |
40026 | fid sessionid 不匹配 |
40027 | 个人文件大小超出限制 |
40028 | 应用不支持上传 |
40029 | 会话id失效/不合法的oauth_code |
40030 | fid或md5 失效 |
40031 | status ,rate 失效 |
40032 | 有风险 |
40033 | 当前会话中超过10个文件处理对象 |
40034 | 群或讨论组文件过多 |
40035 | groupid失效 |
40036 | 非企业群 |
40037 | userid数量超过50 |
40038 | 所有userid均失效 |
40039 | 设备未找到 |
40040 | 参数错误 |
40041 | 用户不存在 |
40042 | 企业二次认证异常 |
40043 | 员工未认证 |
40044 | 群未关注 |
40056 | 不合法的agentid |
40080 | 不合法的suitesecret |
40082 | 不合法的suitetoken |
40083 | 不合法的suiteid |
40084 | 不合法的永久授权码 |
40085 | 不合法的suiteticket |
40086 | 重复开启 |
40087 | 重复关闭 |
60011 | 管理员权限不足,(user/department/agent)无权限 |
60012 | 非企业获取非群成员 |
60013 | 员工未认证 |
42003 | oauth_code超时或者失效 |
42009 | suitetoken失效 |
60003 | 部门不存在 |
60004 | 父部门不存在 |
60005 | 不允许删除有成员的部门 |
60006 | 不允许删除有子部门的部门 |
60007 | 不允许删除根部门 |
60008 | 部门名称已存在 |