功能描述
腾讯会议支持企业内管理员和成员行为记录的保存,您可以通过企业管理后台或 API 对管理员和成员行为进行审计,其中管理员日志包括管理员在企业管理 Web 端的操作行为日志,包括:录制管理、账户管理、用户管理、会议管理、会议室管理、会议室连接器等。
接入步骤
步骤1:创建企业级应用
企业自建应用的应用类型分为企业级和应用级类型。目前查询管理员操作日志仅支持企业级应用获取。
企业级类型:企业级可以获取到您企业账户下的所有数据,该类型的应用需要由管理员创建。
步骤2:生成公私钥对
通过 openssl 命令生成私钥,私钥需要企业自己维护。
生成私钥:
openssl genrsa -out rsa_private_key.pem 1024
该指令支持1024和2048两种密钥长度。
生成公钥:
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
通过此种方式生成的私钥是 pkcs1 格式,公钥是 pkcs8 格式。
私钥转 pkcs8 格式:
openssl pkcs8 -topk8 -in rsa_private_key.pem -nocrypt -out pkcs8.pem
步骤3:上传公钥
接口描述:账户级接口,仅企业超级管理员可以上传公钥,通过接口将公钥上传给腾讯会议,用于对指定日志的加密。覆盖式接口。
请求方式:PUT
接口请求域名:
https://api.meeting.qq.com/v1/encryption/public-key
鉴权方式:JWT
限频:100次/min
输入参数
参数名称 | 参数类型 | 是否必须 | 参数描述 |
userid | String | 是 | 操作人 ID。 |
enc_type | Integer | 否 | 算法类型(目前仅支持 RSA 算法,填充模式为 RSA_PKCS1_PADDING)。 0:RSA;默认值为0。 |
public_key | String | 是 | 公钥内容。 |
key_len | Integer | 是 | 密钥长度,单位bit (对于 RSA 算法目前仅支持1024、2048)。 |
scene_type | Integer | 否 | 加密场景(不同场景可以上传不同的公钥,来加密不同场景下的数据)。 0:管理员操作日志。 |
输出参数
{ }
步骤4:获取管理员日志
接口描述:账户级接口,可以查询加密后的企业管理后台管理员操作日志信息。
请求方式:GET
接口操作者权限点:需具备管理员记录列表的查看权限,可登录 管理后台 通过如下路径查看:用户管理 > 角色管理员记录 > 管理员记录 > 记录列表查看权限。
接口请求域名:
https://api.meeting.qq.com/v1/log/admin-log?operator_id={operator_id}&operator_id_type={operator_id_type}
鉴权方式:JWT
限频:100次/min
输入参数
参数名称 | 参数类型 | 是否必须 | 参数描述 |
start_time | String | 否 | 开始时间戳。 |
end_time | String | 否 | 结束时间戳。 |
userid | String | 否 | 被查询的企业成员 ID。 |
event_code | String | 否 | 事件 code。 |
page | Integer | 否 | 页码,1-2000。默认为1。 |
page_size | Integer | 否 | 分页大小,50-1000。默认为50。 |
operator_id | String | 是 | 操作者 ID。 |
operator_id_type | Integer | 是 | 操作者 ID 类型。 1:userid |
输出参数
参数名称 | 参数类型 | 参数描述 |
current_page | Integer | 当前页码。 |
current_size | Integer | 当前页条数。 |
total_page | Integer | 总页数。 |
total_count | Integer | 总条数。 |
log_list | Log 对象数组 | 日志列表。 |
enc_key | String | 加密后的日志密钥。 |
Log 对象数组
参数名称 | 参数类型 | 参数描述 |
event_code | String | 事件 code。 |
operator_id | String | 操作人 ID。 |
operator_id_type | Integer | 操作人类型。 |
event_time | String | 事件时间戳。 |
event_details | Object | |
operator_name | String | 操作者名称。 |
event_status | String | 事件状态:success,fail。 |
步骤5:使用私钥解密日志
通过接口获取到的 log_list 数据为通过对称加密后得到的数据。enc_key 为通过非对称加密后的对称密钥数据。您需要通过私钥解密出对称密钥的明文,再通过对称密钥解密日志列表拿到最终的日志明文。下面为解密数据的流程及代码 demo。
import base64from Crypto.Cipher import AES, PKCS1_v1_5from Crypto.PublicKey import RSAdef decrypt_data(rsa_pri_key: str, enc_data: str, enc_key: str) -> str:"""解密数据:param rsa_pri_key: PKCS#8格式的RSA私钥文件内容:param enc_data: 加密数据:param enc_key: 加密对称密钥:return: 解密后数据"""# 解密对称密钥aes_key = dec_enc_key(rsa_pri_key, enc_key)# 解密数据dec_data = dec_enc_data(aes_key, enc_data)return dec_datadef dec_enc_key(rsa_pri_key: str, enc_key: str) -> str:"""解密对称密钥"""# base64解码对称密钥decode_key = base64.b64decode(enc_key)# 私钥解密对称密钥rsa_key = RSA.import_key(rsa_pri_key)cipher = PKCS1_v1_5.new(rsa_key)aes_key = cipher.decrypt(decode_key, None)if len(aes_key) != 32:raise ValueError("非256位对称密钥")return aes_key.decode()def pkcs7_unpadding(orig_data: str) -> str:"""去除填充"""length = len(orig_data)unpadding = orig_data[-1]index = length - unpaddingif index < 0 or index >= length:raise ValueError("index out of range")return orig_data[:index]def dec_enc_data(aes_key: str, enc_data: str) -> str:"""解密数据"""# 获取初始向量IViv = aes_key[:16]# base64解码数据decode_data = base64.b64decode(enc_data)# 解密数据cipher = AES.new(aes_key.encode(), AES.MODE_CBC, iv.encode())orig_data = cipher.decrypt(decode_data)# 去除填充byte_dec_data = pkcs7_unpadding(orig_data)dec_data = byte_dec_data.decode('utf-8')return dec_data
import javax.crypto.Cipher;import javax.crypto.spec.IvParameterSpec;import javax.crypto.spec.SecretKeySpec;import java.io.BufferedReader;import java.io.IOException;import java.io.StringReader;import java.security.*;import java.security.spec.PKCS8EncodedKeySpec;import java.security.spec.X509EncodedKeySpec;import java.util.Base64;public class DecryptDemo {/*** decryptData 解密数据** @param rsaPriKey PKCS#8格式的RSA私钥文件内容* @param encData 加密数据* @param encKey 加密对称密钥* @return decData 解密后数据*/public static String decryptData(String rsaPriKey, String encData, String encKey) throws Exception {// 解密对称密钥String aesKey = decEncKey(rsaPriKey, encKey);// 解密数据return decEncData(aesKey, encData);}/*** decEncKey 解密对称密钥* @param rsaPriKey* @param encKey* @return 对称密钥*/public static String decEncKey(String rsaPriKey, String encKey) throws Exception {// base64解码对称密钥byte[] decodeKey = Base64.getDecoder().decode(encKey);// 私钥解密对称密钥Cipher cipher = Cipher.getInstance("RSA");PrivateKey pk = ReadPemKeyPair.loadPrivateKey(rsaPriKey);cipher.init(Cipher.DECRYPT_MODE, pk);String aesKey = new String(cipher.doFinal(decodeKey));if (aesKey.length() != 32) {throw new Exception("非256位对称密钥");}return aesKey;}/*** decEncData 解密数据* @param aesKey* @param encData* @return 解密后的明文数据*/public static String decEncData(String aesKey, String encData) throws Exception {// 获取初始向量IVString iv = aesKey.substring(0, 16);// base64解码数据byte[] decodeData = Base64.getDecoder().decode(encData);// 解密数据Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");IvParameterSpec params = new IvParameterSpec(iv.getBytes());cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(aesKey.getBytes(), "AES"), params);// byte[] originData = pkcs7UnPadding(cipher.doFinal(decodeData));// return new String(originData, StandardCharsets.UTF_8);return new String(cipher.doFinal(decodeData));}private static byte[] pkcs7UnPadding(byte[] originData) throws Exception {int length = originData.length;int unpadding = originData[length - 1];int index = length - unpadding;if (index < 0 || index > length) {throw new Exception("index out of range");}byte[] dst = new byte[index];System.arraycopy(originData, 0, dst, 0, index);return dst;}}class ReadPemKeyPair {private static String loadKey(String keyPem) throws IOException {BufferedReader brKey = new BufferedReader(new StringReader(keyPem));StringBuilder sb = new StringBuilder();String line;while ((line = brKey.readLine())!= null) {if (!line.startsWith("-")) {sb.append(line);}}return sb.toString();}public static PrivateKey loadPrivateKey(String privateKeyPem) throws GeneralSecurityException, IOException {// 读取 pem 中的 keyString privateKeyStr = loadKey(privateKeyPem);byte [] pkcs8EncodedKeySpec = Base64.getDecoder().decode(privateKeyStr);PKCS8EncodedKeySpec privSpec = new PKCS8EncodedKeySpec(pkcs8EncodedKeySpec);KeyFactory kf = KeyFactory.getInstance("RSA");return kf.generatePrivate(privSpec);}public static PublicKey loadPublicKey(String publicKeyPem) throws GeneralSecurityException, IOException {// 读取 pem 中的 keyString publicKeyStr = loadKey(publicKeyPem);X509EncodedKeySpec x509PubKeySpec = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKeyStr));KeyFactory fact = KeyFactory.getInstance("RSA");return fact.generatePublic(x509PubKeySpec);}}
#include <iostream>#include <string>#include <vector>#include <openssl/rsa.h>#include <openssl/pem.h>#include <openssl/evp.h>#include <openssl/aes.h>#include <openssl/err.h>#include <openssl/bio.h>#include <openssl/buffer.h>// Base64解码std::string base64Decode(const std::string &input) {BIO* bio_mem = BIO_new_mem_buf(input.data(), input.length());BIO* bio64 = BIO_new(BIO_f_base64());BIO_set_flags(bio64, BIO_FLAGS_BASE64_NO_NL);BIO_push(bio64, bio_mem);char buffer[1024];std::string result;int decoded_size;while ((decoded_size = BIO_read(bio64, buffer, 1024)) > 0) {result.append(buffer, decoded_size);}BIO_free_all(bio64);return result;}// 解密对称密钥std::string decEncKey(const std::string &rsa_pri_key, const std::string &enc_key) {BIO *bio_mem = BIO_new_mem_buf(rsa_pri_key.data(), rsa_pri_key.size());RSA *rsa = RSA_new();rsa = PEM_read_bio_RSAPrivateKey(bio_mem, &rsa, nullptr, nullptr);if (rsa == nullptr) {return "";}// base64解码对称密钥std::string decoded_key = base64Decode(enc_key);// 私钥解密对称密钥std::vector<unsigned char> decrypted_key(RSA_size(rsa));int key_size = RSA_private_decrypt(decoded_key.size(), (const unsigned char *)decoded_key.data(), &decrypted_key[0], rsa, RSA_PKCS1_PADDING);RSA_free(rsa);BIO_free(bio_mem);return std::string(decrypted_key.begin(), decrypted_key.begin() + key_size);}// 去除填充std::string pkcs7UnPadding(const std::string& orig_data) {size_t length = orig_data.size();unsigned char unpadding = orig_data[length - 1];size_t index = length - unpadding;if (index < 0 || index >= length) {throw std::runtime_error("index out of range");}return orig_data.substr(0, index);}// 解密数据std::string decEncData(const std::string &aes_key, const std::string &enc_data) {// base64解码数据std::string decoded_data = base64Decode(enc_data);// 获取初始向量IVstd::string iv = aes_key.substr(0, 16);// 解密数据AES_KEY key;if (AES_set_decrypt_key(reinterpret_cast<const unsigned char*>(aes_key.data()), 256, &key) < 0) {throw std::runtime_error("failed to set AES decryption key");}std::string orig_data(decoded_data.size(), '\\0');AES_cbc_encrypt(reinterpret_cast<const unsigned char*>(decoded_data.data()),reinterpret_cast<unsigned char*>(&orig_data[0]),decoded_data.size(),&key,reinterpret_cast<unsigned char*>(&iv[0]),AES_DECRYPT);std::string dec_data = pkcs7UnPadding(orig_data);return dec_data;}/** DecryptData 解密数据** @param rsaPriKey PKCS#8格式的RSA私钥文件内容* @param encData 加密数据* @param encKey 加密对称密钥* @return decData 解密后数据*/std::string DecryptData(const std::string &rsaPriKey, const std::string &encData, const std::string &encKey) {// 解密对称密钥std::string aesKey = decEncKey(rsaPriKey, encKey);if (aesKey == "") {return "";}// 解密数据std::string decData = decEncData(aesKey, encData);return decData;}
package utilimport ("crypto/aes""crypto/cipher""crypto/rand""crypto/rsa""crypto/x509""encoding/base64""encoding/pem""errors")/** DecryptData 解密数据** @param rsaPriKey PKCS#8格式的RSA私钥文件内容* @param encData 加密数据* @param encKey 加密对称密钥* @return decData 解密后数据* @return err 错误信息*/func DecryptData(rsaPriKey, encData, encKey string) (decData string, err error) {// 解密对称密钥aesKey, err := decEncKey(rsaPriKey, encKey)if err != nil {return "", err}// 解密数据decData, err = decEncData(aesKey, encData)if err != nil {return "", err}return decData, nil}// decEncKey 解密对称密钥func decEncKey(rsaPriKey, encKey string) (string, error) {// base64解码对称密decodeKey, err := base64.StdEncoding.DecodeString(string(encKey))if err != nil {return "", err}// 私钥解密对称密钥pemBlk, _ := pem.Decode([]byte(rsaPriKey))if pemBlk == nil {return "", errors.New("非法PEM格式的RSA私钥文件")}pkInf, err := x509.ParsePKCS8PrivateKey(pemBlk.Bytes)if err != nil {return "", err}pk, ok := pkInf.(*rsa.PrivateKey)if !ok {return "", errors.New("非PKCS#8格式的RSA私钥文件")}byteAESKey, err := rsa.DecryptPKCS1v15(rand.Reader, pk, decodeKey)if err != nil {return "", err}aesKey := string(byteAESKey)if len(aesKey) != 32 {return "", errors.New("非256位对称密钥")}return aesKey, nil}// pkcs7UnPadding 去除填充func pkcs7UnPadding(origData []byte) ([]byte, error) {length := len(origData)unpadding := int(origData[length-1])index := length - unpaddingif index < 0 || index >= len(origData) {return nil, errors.New("index out of range")}return origData[:index], nil}// decEncData 解密数据func decEncData(aesKey, encData string) (string, error) {// 获取初始向量IViv := aesKey[:16]// base64解码数据decodeData, err := base64.StdEncoding.DecodeString(string(encData))if err != nil {return "", err}// 解密数据cipherBlk, err := aes.NewCipher([]byte(aesKey))if err != nil {return "", err}blockMode := cipher.NewCBCDecrypter(cipherBlk, []byte(iv))origData := make([]byte, len(decodeData))blockMode.CryptBlocks(origData, decodeData)byteDecData, err := pkcs7UnPadding(origData)if err != nil {return "", err}decData := string(byteDecData)return decData, nil}
拿到日志明文为 json 格式,公参为每条日志都会返回的数据包括事件 code、事件时间、事件操作者等信息,私参 event_details 根据不同的事件返回的内容不同,您可以根据业务需要处理 event_details 对象。
附录
event_details 定义。
会议管理
事件名称 | 事件 code | event_detials 对象定义 |
搜索会议 | search_meeting |
|
导出会议列表 | download_meeting_list |
|
导出会议成员列表 | download_meeting_participants |
|
查看会议质量 | query_meeting_quality |
|
用户管理
事件名称 | 事件 code | event_details 对象 |
搜索用户 | search_user |
|
邀请用户 | invite_user_activate |
|
用户操作 | modify_user |
|
批量用户操作 | batch_update_user |
|
修改管理范围 | modify_user_manage_range |
|
修改部门设置 | modify_department |
|
修改角色 | modify_role |
|
敏感权限隔离 | modify_super_admin_permission |
|
修改用户角色 | modify_role_user |
|
修改通讯录规则 | modify_directory_rule |
|
账户管理
事件名称 | 事件 code | event_details 定义 |
修改账户信息 | modify_corp_info |
|
修改会前设置 | modify_corp_before-meeting_setting |
|
修改会中设置 | modify_corp_in-meeting_setting |
|
修改安全设置 | modify_corp_security_setting |
|
修改账号设置 | modify_corp_setting |
|
修改通讯录设置 | modify_corp_directory_setting |
|
录制管理
事件名称 | 事件 code | event_details 定义 |
分享录制文件 | share_record |
|
下载录制文件 | download_record |
|
删除录制文件 | delete_record |
|
修改录制文件 | modify_record |
|
查看录制文件 | view_record |
|
搜索录制文件 | search_record |
|
修改录制设置 | modify_corp_record_setting |
|
会议室管理
事件名称 | 事件 code | event_details 对象定义 |
查询会议室信息 | search_meeting_room |
|
添加会议室 | create_meeting_room |
|
修改会议室信息 | modify_meeting_room |
|
删除会议室 | delete_meeting_room |
|
修改会议室标签 | modify_meeting_room_label |
|
会议室操作 | operate_meeting_room_account |
|
批量会议室操作 | batch_operate_meeting_room |
|
下载会议室参会信息 | download_meeting_room_meetinginfo |
|
管理会议室设备 | modify_meeting_room_device_list |
|
修改会议室设置 | modify_meeting_room_settings |
注意:一次行为涉及哪部分修改就返回哪部分内容。 |
会议室连接器
事件名称 | 事件 code | event_details 对象定义 |
本地部署设置 | mra_deploy_management |
|
服务配置 | mra_service_configration |
说明:根据修改内容,动态包含修改项。 |
注册账号管理 | mra_registration_management |
说明:根据修改内容,动态包含修改项。 |