功能描述
腾讯会议支持企业内管理员和成员行为记录的保存,您可以通过企业管理后台或 API 对管理员和成员行为进行审计,其中成员行为日志包括成员登录登出、会议管理、会议控制等行为的记录。
接入步骤
步骤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:管理员操作日志 1:企业成员行为日志 |
输出参数
{ }
步骤4:获取企业成员行为日志
接口描述:账户级接口,可以查询加密后的企业成员在腾会的行为日志信息。
请求方式:GET
接口请求域名:
https://api.meeting.qq.com/v1/log/user-log
鉴权方式:JWT
限频:100次/min
输入参数
参数名称 | 参数类型 | 是否必须 | 参数描述 |
start_time | QUERY | 否 | 开始时间戳(秒级)。接口返回该时间戳所在当天的日志数据。 |
userid | QUERY | 否 | 企业成员 ID。 |
event_type | QUERY | 是 | 1:成员行为日志 2:用户登录登出日志 |
event_code | QUERY | 否 | 事件 code。 |
meeting_id | QUERY | 否 | 会议 ID。 |
operator_role | QUERY | 否 | 操作者类型: 1:主持人 2:联席主持人 3:普通参会者 4:创建者 5:管理员 |
page | QUERY | 否 | 页码,1-2000。默认为1。 |
page_size | QUERY | 否 | 分页大小,50-200。默认为50。 |
输出参数
参数名称 | 参数类型 | 参数描述 |
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 | 操作人类型: 1:userid 3:rooms_id 6:MRA 账号或 ip |
operator_name | String | 操作者名称。 |
operator_role | Integer | 操作者角色: 1:主持人 2:联席主持人 3:普通参会者 4:创建者 5:管理员 |
instanceid | Integer | 设备类型: 0:PSTN 1:PC 2:Mac 3:Android 4:iOS 5:Web 6:iPad 7:Android Pad 8:小程序 9:voip、sip 设备 10:linux 20:Rooms for Touch Windows 21:Rooms for Touch MacOS 22:Rooms for Touch Android 30:Controller for Touch Windows 32:Controller for Touch Android 33:Controller for Touch iOS |
source_type | Integer | 操作来源。 1:restapi |
event_time | String | 事件时间戳(秒级)。 |
event_details | Object | |
meeting_id | String | 会议 ID。 |
步骤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_details对象定义 |
用户通过授权登录 | user_login_by_authorize |
|
用户通过手机号登录 | user_login_by_phone |
|
个人用户退出登录 | user_logout |
|
其他
事件名称 | 事件code | event_details对象定义 |
关闭麦克风 | mute |
|
| 开启麦克风 | unmute |
开启共享音频 | open_share_audio |
|
| 关闭共享音频 | close_share_audio |
| 打开共享屏幕或白板 | open_share_screen |
| 关闭共享屏幕或白板 | close_share_screen |
| 暂停共享屏幕或白板 | pause_share_screen |
开启摄像头 | open_video |
|
| 关闭摄像头 | close_video |
举手 | raise_hand |
|
关闭字幕 | close_subtitle |
|
| 开启字幕 | open_subtitle |
关闭云录制 | close_cloud_record |
|
| 开启云录制 | open_cloud_record |
关闭本地录制 | close_local_record |
|
| 开启本地录制 | open_local_record |
创建会议 | create_meeting |
|
取消会议 | cancel_meeting |
|
修改会议 | edit_meeting |
上报修改的字段及其内容。 |
删除会议 | delete_meeting |
|
将成员移出会议 | kick_out_of_meeting |
|
加入会议 | join_meeting_by_media_backend |
|
离开会议 | leave_meeting_by_media_backend_filter |
|
打开远程控制 | open_remote_control |
|
关闭远程控制 | close_remote_control |
|
设置主持人 | set_host |
|
| 设置联席主持人 | set_joint_host |
| 取消联席主持人角色 | cancel_joint_host |
回收主持人角色 | cancel_set_host |
|
全体静音 | all_mute |
|
| 全体静音且允许自己解除静音 | all_mute_allow_unmute |
| 解除全体静音 | all_unmute |
设置成员静音 | mute_user |
|
| 解除成员静音 | unmute_user |
开始分组讨论 | begin_group_discussion |
|
| 结束分组讨论 | end_group_discussion |
打开互动批注 | open_interactive_annotations |
|
| 关闭互动批注 | close_interactive_annotations |
将成员移至等候室 | move_to_waiting_room |
|
开启等候室 | open_waiting_room |
|
打开水印 | open_watermark |
|
关闭水印 | close_watermark |
|
修改会中名称 | change_name |
|
锁定会议 | lock_meeting |
|
解除锁定会议 | unlock_meeting |
|
结束会议 | dismiss_meeting |
|