最近公司需要做一个腾讯会议API的对接,经过查看官方文档和腾讯相关技术人员周旋~~以及不断踩坑,终于在我的不懈努力下,成功将API接口调通了
腾讯会议API接口文档:https://cloud.tencent.com/document/product/1095/42407
公共参数是用于标识用户和接口鉴权目的的参数,如非必要,在每个接口单独的接口文档中不再对这些参数进行说明,但每次请求均需要携带这些参数,才能正常发起请求。
API 采用 TC3-HMAC-SHA256 签名方法,公共参数需要统一放到 HTTP Header 请求头部中。
参数名称 | 类型 | 必选 | 描述 |
---|---|---|---|
Content-Type | String | 是 | 内容类型,传入格式必须为 application/json。 |
X-TC-Action | String | 否 | 操作的接口名称。取值参考接口文档中输入参数公共参数 Action 的说明。例如云服务器的查询实例列表接口,取值为 DescribeInstances。 |
X-TC-Region | String | 否 | 地域参数,用来标识希望操作哪个地域的数据。接口接受的地域取值参考接口文档中输入参数公共参数 Region 的说明。注意:某些接口不需要传递该参数,接口文档中会对此特别说明,此时即使传递该参数也不会生效。 |
X-TC-Key | String | 是 | 此参数参与签名计算。腾讯云 API 接入,申请的安全凭证密钥对中的 SecretId,其 Secretkey 用于签名。企业管理员可以登录 腾讯会议官网,单击右上角用户中心,在左侧菜单栏中的企业管理 > 高级 > restApi中进行查看。 |
X-TC-Timestamp | String | 是 | 此参数参与签名计算。当前 UNIX 时间戳,可记录发起 API 请求的时间。例如1529223702,单位为秒。注意:如果与服务器时间相差超过5分钟,会引起签名过期错误。 |
X-TC-Nonce | String | 是 | 此参数参与签名计算。随机正整数。 |
X-TC-Version | String | 否 | 应用 App 的版本号,建议设置,以便灰度和查找问题。 |
X-TC-Signature | String | 是 | 放置由下面的签名方法产生的签名。 |
X-TC-Token | String | 否 | 临时证书所用的 Token ,需要结合临时密钥一起使用。临时密钥和 Token 需要到访问管理服务调用接口获取。长期密钥不需要 Token。 |
AppId | String | 是 | 腾讯会议分配给三方开发应用的 App ID。企业管理员可以登录 腾讯会议官网,单击右上角用户中心,在左侧菜单栏中的企业管理 > 高级 > restApi中进行查看。 |
SdkId | String | 否 | 用户子账号或开发的应用 ID,企业管理员可以登录 腾讯会议官网,单击右上角用户中心,在左侧菜单栏中的企业管理 > 高级 > restApi中进行查看(如存在 SdkId 则必须填写,早期申请 API 且未分配 SdkId 的客户可不填写)。 |
X-TC-Registered | String | 是 | 启用账户通讯录,传入值必须为1,创建的会议可出现在用户的会议列表中。 启用账户通讯录说明: 1. 通过 SSO 接入腾讯会议账号体系。 2. 通过调用接口创建企业用户。 3. 通过企业管理后台添加或批量导入企业用户。 |
可参考腾讯会议API-企业自建应用鉴权(JWT)
https://cloud.tencent.com/document/product/1095/42413
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* 生成签名,开发版本oracle jdk 1.8.0_221
*
* @param secretId 邮件下发的secret_id
* @param secretKey 邮件下发的secret_key
* @param httpMethod http请求方法 GET/POST/PUT等
* @param headerNonce X-TC-Nonce请求头,随机数
* @param headerTimestamp X-TC-Timestamp请求头,当前时间的秒级时间戳
* @param requestUri 请求uri,eg:/v1/meetings
* @param requestBody 请求体,没有的设为空串
* @return 签名,需要设置在请求头X-TC-Signature中
* @throws NoSuchAlgorithmException e
* @throws InvalidKeyException e
*/
static String sign(String secretId, String secretKey, String httpMethod, String headerNonce,
String headerTimestamp, String requestUri, String requestBody)
throws NoSuchAlgorithmException, InvalidKeyException {
String tobeSig =
httpMethod + "\nX-TC-Key=" + secretId + "&X-TC-Nonce=" + headerNonce + "&X-TC-Timestamp="
+ headerTimestamp + "\n" + requestUri + "\n" + requestBody;
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8),
mac.getAlgorithm());
mac.init(secretKeySpec);
byte[] hash = mac.doFinal(tobeSig.getBytes(StandardCharsets.UTF_8));
String hexHash = bytesToHex(hash);
return new String(Base64.getEncoder().encode(hexHash.getBytes(StandardCharsets.UTF_8)));
}
private static String HMAC_ALGORITHM = "HmacSHA256";
private static char[] HEX_CHAR = {
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c',
'd', 'e', 'f'};
static String bytesToHex(byte[] bytes) {
char[] buf = new char[bytes.length * 2];
int index = 0;
for (byte b : bytes) {
buf[index++] = HEX_CHAR[b >>> 4 & 0xf];
buf[index++] = HEX_CHAR[b & 0xf];
}
return new String(buf);
}
有了签名算法,我们可以生成公共请求头了
JWT鉴权方式需要在每次请求时加入公共请求头参数,以及通过签名算法生成的签名也需携带入请求头,借此,我封装了生成请求头的方法。
其中secretId
,secretKey
,appId
,sdkId
是创建企业自建应用生成的,参考腾讯会议API文档-创建企业自建应用
需要注意的地方:
1、Timestamp参数是Unix时间戳,单位是秒
2、GET方法请求体需传""
/**
* 获取公共请求头
*
* @param httpMethod 请求方式:POST|GET
* @param requestUri 请求uri
* @param requestBody 请求体 GET方法请求体需传""
* @return 拼接好的请求头
*/
public static Map<String, String> getHeader(String httpMethod, String requestUri,
String requestBody) {
HashMap<String, String> header = new HashMap<>(8);
// 请求随机数
String headerNonce = String.valueOf(new Random().nextInt(999999));
// 当前时间的UNIX时间戳
String headerTimestamp = String.valueOf(System.currentTimeMillis() / 1000);
String signature = null;
try {
signature = sign(secretId, secretKey, httpMethod, headerNonce, headerTimestamp, requestUri,
requestBody);
} catch (Exception e) {
log.error("签名生成异常", e);
}
header.put("Content-Type", "application/json");
header.put("X-TC-Key", secretId);
header.put("X-TC-Timestamp", headerTimestamp);
header.put("X-TC-Nonce", headerNonce);
header.put("AppId", appId);
header.put("X-TC-Version", "1.0");
header.put("X-TC-Signature", signature);
header.put("SdkId", sdkId);
header.put("X-TC-Registered", "1");
return header;
}
为了方便调用API,我封装了GET和POST方法
/**
* 发起get请求
* @param uri 请求uri 生成签名使用
* @param address 请求路径
* @return 请求结果的JsonStr
*/
public static String sendGet(String address,String uri) {
//get请求,请求体为""
Map<String, String> header = getHeader("GET",uri,"");
String result = "";
String logInfo = "";
GetMethod getMethod = null;
try {
// 创建httpClient实例对象
HttpClient httpClient = new HttpClient();
// 设置httpClient连接主机服务器超时时间:15000毫秒
httpClient.getHttpConnectionManager().getParams().setConnectionTimeout(15000);
// 创建GET请求方法实例对象
getMethod = new GetMethod(address);
// 设置post请求超时时间
getMethod.getParams().setParameter(HttpMethodParams.SO_TIMEOUT, 60000);
if (header != null) {
for (Map.Entry<String, String> entry : header.entrySet()) {
getMethod.addRequestHeader(entry.getKey(), entry.getValue());
}
}
logInfo = "HTTP调用接口:" + address;
httpClient.executeMethod(getMethod);
result = getMethod.getResponseBodyAsString();
} catch (Exception e) {
log.info("HTTP调用接口出错:" + logInfo + e, e);
} finally {
if (null != getMethod) {
getMethod.releaseConnection();
}
}
return result;
}
/**
* 腾讯会议发送post请求 携带生产签名和公共请求头参数
*
* @param address 请求地址
* @param uri 请求uri生产签名使用
* @param requestBody 请求参数
* @return 请求响应结果
*/
public static String sendPost(String address, String uri, String requestBody) {
//生成公共请求头参数和签名
HashMap<String, String> headerMap = getHeaderTest("POST", uri, requestBody);
CloseableHttpClient httpClient = HttpClients.createDefault();
HttpPost httpPost = new HttpPost(address);
String jsonStr = "";
try {
for (Entry<String, String> header : headerMap.entrySet()) {
httpPost.setHeader(header.getKey(), header.getValue());
}
httpPost.setEntity(new StringEntity(requestBody));
CloseableHttpResponse httpResponse = null;
httpResponse = httpClient.execute(httpPost);
HttpEntity httpEntity = httpResponse.getEntity();
if (httpEntity != null) {
jsonStr = EntityUtils.toString(httpEntity, "UTF-8");
}
log.info("腾讯会议httpPost请求相应信息{}", jsonStr);
} catch (IOException e) {
log.info("腾讯会议httpPost发送异常", e);
} finally {
httpPost.releaseConnection();
try {
httpClient.close();
} catch (IOException e) {
log.error("httpClient关闭异常", e);
}
}
return jsonStr;
}
有了公共请求头,我们就可以调用API了
REST APIs:https://cloud.tencent.com/document/product/1095/42414
需要注意的是会议主题 subject,直接传入中文,会报API签名验证失败,这个是一个坑点,当初搞了我好久,一直没查到原因。
解决办法是取中文的Unicode
/**
* 创建预约腾讯会议
*
* @param subject 主题
* @param startTime 开始时间戳
* @param duration 持续时间(单位分钟)
* @param userId 企业注册时的用户名
* @return 创建结果
*/
public static Map<String, Object> creatMeeting(String subject, Long startTime,
Integer duration, String userId) {
// Unix时间戳,单位为秒
startTime = startTime / 1000;
String endTime = String.valueOf(startTime + (duration * 60));
HashMap<String, Object> resultMap = new HashMap<>(8);
// 请求体,get方法请求体需传""
String resultBody = "{" +
//会议结束时间
"\"end_time\": \"" + endTime + "\"" + "," +
//会议开始时间戳(单位秒)。
"\"start_time\": \"" + startTime + "\"," +
//用户的终端设备类型 1:PC
"\"instanceid\": " + "1" + "," +
// 会议类型:0:预约会议 1:快速会议
"\"type\": " + "0" + "," +
//腾讯会议用户唯一标识 不能为1--9内的数字
"\"userid\": \"" + userId + "\"," +
//会议主题
"\"subject\": \"" + getUnicode(subject) + "\"" +
"}";
// 创建会议uri
String uri = "/v1/meetings";
String address = MEETING_DOMAIN_URL + uri;
try {
String jsonStr = requestPost(address, uri, resultBody);
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
if (null == jsonObject.getJSONObject("error_info")) {
JSONObject meetingInfo = jsonObject.getJSONArray("meeting_info_list").getJSONObject(0);
resultMap.put("success", "true");
resultMap.put("message", "创建预约会议成功");
resultMap.put("meetingId", meetingInfo.getString("meeting_id"));
resultMap.put("meetingCode", meetingInfo.getString("meeting_code"));
resultMap.put("joinUrl", meetingInfo.getString("join_url"));
} else {
resultMap.put("success", "false");
resultMap.put("message", "创建预约会议失败");
resultMap.put("errorInfo", jsonObject.getJSONObject("error_info"));
}
} catch (Exception e) {
resultMap.put("success", "false");
resultMap.put("message", e.getMessage());
log.error("创建预约会议发生异常", e);
}
return resultMap;
}
/**
* 防止api请求中传入中文导致的报错问题
* @param s 中文字符串
* @return Unicode码
*/
public static String getUnicode(String s) {
try {
StringBuffer out = new StringBuffer("");
byte[] bytes = s.getBytes("unicode");
for (int i = 0; i < bytes.length - 1; i += 2) {
out.append("\\u");
String str = Integer.toHexString(bytes[i + 1] & 0xff);
for (int j = str.length(); j < 2; j++) {
out.append("0");
}
String str1 = Integer.toHexString(bytes[i] & 0xff);
out.append(str1);
out.append(str);
}
return out.toString();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
/**
* 根据meetingId查询腾讯会议
*
* @param meetingId 创建腾讯会议生成的meetingId
* @param userId 创建人的id
* @return 查询结果
*/
public static Map<String, Object> queryMeetings(String meetingId, String userId) {
HashMap<String, Object> resultMap = new HashMap<>(8);
try {
String uri = "/v1/meetings/" + meetingId + "?userid=" + userId + "&instanceid=1";
// https://api.meeting.qq.com/v1/meetings/{meetingId}?userid={userid}&instanceid={instanceid}
// String address = url + "/" + meetingId;
String address = MEETING_DOMAIN_URL + uri;
String jsonStr = sendGet(address, uri);
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
resultMap.put("message", jsonObject.getString("error_info"));
} catch (Exception e) {
log.error("获取会议信息异常", e);
resultMap.put("success", "false");
resultMap.put("message", e.getMessage());
}
return resultMap;
}
/**
* 取消预约的会议
*
* @param userId 预定人id
* @param meetingId 会议id
* @return 取消结果
*/
public static Map<String, Object> cancelMeeting(String userId, String meetingId) {
HashMap<String, Object> resultMap = new HashMap<>(8);
// https://api.meeting.qq.com/v1/meetings/{meetingId}/cancel
String address;
// 创建会议uri
String uri = "/v1/meetings/" + meetingId + "/cancel";
address = MEETING_DOMAIN_URL + uri;
try {
// 请求体,get方法请求体需传""
String resultBody = "{\n"
+ " \"meetingId\" : \"" + meetingId + "\",\n"
+ " \"userid\" : \"" + userId + "\",\n"
+ " \"instanceid\" : 1,\n"
+ " \"reason_code\" : 1\n"
+ "}";
String jsonStr = requestPost(address, uri, resultBody);
JSONObject jsonObject = JSONObject.parseObject(jsonStr);
if (null == jsonObject.getJSONObject("error_info")) {
resultMap.put("message", "取消会议成功");
resultMap.put("success", "true");
} else {
resultMap.put("message", "取消会议失败");
resultMap.put("errorInfo", jsonObject.getJSONObject("error_info"));
resultMap.put("success", "false");
}
} catch (Exception e) {
resultMap.put("success", "false");
resultMap.put("message", e.getMessage());
log.error("取消会议异常:", e);
}
return resultMap;
}
到此我的文章就结束了,希望可以帮助到大家,少走一些弯路。
大家还有一些不懂的地方,可以到腾讯云提交工单(https://console.cloud.tencent.com/workorder/category),找腾讯售后寻求技术支持,客服还是很负责任的。
文章浏览阅读3.2w次,点赞16次,收藏90次。对于这个问题我也是从网上找了很久,终于解决了这个问题。首先遇到这个问题,应该确认虚拟机能不能正常的上网,就需要ping 网关,如果能ping通说明能正常上网,不过首先要用命令route -n来查看自己的网关,如下图:第一行就是默认网关。现在用命令ping 192.168.1.1来看一下结果:然后可以看一下电脑上面百度的ip是多少可以在linux里面ping 这个IP,结果如下:..._linux桥接ping不通baidu
文章浏览阅读512次。小妹在这里已经卡了2-3天了,研究了很多人的文章,除了低版本api 17有成功外,其他的不是channel null 就是没反应 (channel null已解决)拜托各位大大,帮小妹一下,以下是我的程式跟 gradle, 我在这里卡好久又没有人可问(哭)![image](/img/bVcL0Qo)public class MainActivity extends AppCompatActivit..._android 权限申请弹窗 横屏
文章浏览阅读1.4k次,点赞4次,收藏6次。valid padding(有效填充):完全不使用填充。half/same padding(半填充/相同填充):保证输入和输出的feature map尺寸相同。full padding(全填充):在卷积操作过程中,每个像素在每个方向上被访问的次数相同。arbitrary padding(任意填充):人为设定填充。..._cnn “相同填充”(same padding)
文章浏览阅读790次,点赞29次,收藏28次。手绘了下图所示的kafka知识大纲流程图(xmind文件不能上传,导出图片展现),但都可提供源文件给每位爱学习的朋友一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长![外链图片转存中…(img-Qpoc4gOu-1712656009273)][外链图片转存中…(img-bSWbNeGN-1712656009274)]
文章浏览阅读469次。Date对象取得年份有getYear和getFullYear两种方法经 测试var d=new Date;alert(d.getYear())在IE中返回 2009,在Firefox中会返回109。经查询手册,getYear在Firefox下返回的是距1900年1月1日的年份,这是一个过时而不被推荐的方法。而alert(d.getFullYear())在IE和FF中都会返回2009。因此,无论何时都应使用getFullYear来替代getYear方法。例如:2016年用 getFullYea_getyear和getfullyear
文章浏览阅读182次。Unix传奇(上篇) 陈皓 了解过去,我们才能知其然,更知所以然。总结过去,我们才会知道我们明天该如何去规划,该如何去走。在时间的滚轮中,许许多的东西就像流星一样一闪而逝,而有些东西却能经受着时间的考验散发着经久的魅力,让人津津乐道,流传至今。要知道明天怎么去选择,怎么去做,不是盲目地跟从今天各种各样琳琅满目前沿技术,而应该是去 —— 认认真真地了解和回顾历史。 Unix是目前还在存活的操作系_unix传奇pdf
文章浏览阅读308次。哈希算法:将字符串映射为数字形式,十分巧妙,一般运用为进制数,进制据前人经验,一般为131,1331时重复率很低,由于字符串的数字和会很大,所以一般为了方便,一般定义为unsigned long long,爆掉时,即为对 2^64 取模,可以对于任意子序列的值进行映射为数字进而进行判断入门题目链接:AC代码:#include<bits/stdc++.h>using na..._ac算法 哈希
文章浏览阅读952次,点赞13次,收藏27次。由于觉得Qt的编辑界面比较丑,所以想用vs2022的编辑器写Qt加MySQL的项目。_在vs中 如何装qt5sqlmysql模块
文章浏览阅读1k次。选择题题目:下面的哪个调研内容属于经济环境调研?()题目:()的目的就是加强与客户的沟通,它是是网络媒体也是网络营销的最重要特性。题目:4Ps策略中4P是指产品、价格、顾客和促销。题目:网络市场调研是目前最为先进的市场调研手段,没有任何的缺点或不足之处。题目:市场定位的基本参数有题目:市场需求调研可以掌握()等信息。题目:在开展企业网站建设时应做好以下哪几个工作。()题目:对企业网站首页的优化中,一定要注意下面哪几个方面的优化。()题目:()的主要作用是增进顾客关系,提供顾客服务,提升企业_画中画广告之所以能有较高的点击率,主要由于它具有以下特点
文章浏览阅读1k次,点赞2次,收藏5次。以爬取CSDN为例子:第一步:导入请求库第二步:打开请求网址第三步:打印源码import urllib.requestresponse=urllib.request.urlopen("https://www.csdn.net/?spm=1011.2124.3001.5359")print(response.read().decode('utf-8'))结果大概就是这个样子:好的,继续,看看打印的是什么类型的:import urllib.requestresponse=urllib.r_urlopen the read operation timed out
文章浏览阅读304次。修正sina.com/sina.cn邮箱获取不到联系人,并精简修改了其他邮箱代码,以下就是升级版版本的介绍:完整版本,整合了包括读取邮箱通讯录、MSN好友列表的的功能,目前读取邮箱通讯录支持如下邮箱:gmail(Y)、hotmail(Y)、 live(Y)、tom(Y)、yahoo(Y)(有点慢)、 sina(Y)、163(Y)、126(Y)、yeah(Y)、sohu(Y) 读取后可以发送邮件(完..._通讯录 应用读取 邮件 的相关
文章浏览阅读213次。云计算及虚拟化教程学习云计算、虚拟化和计算机网络的基本概念。此视频教程共2.0小时,中英双语字幕,画质清晰无水印,源码附件全课程英文名:Cloud Computing and Virtualization An Introduction百度网盘地址:https://pan.baidu.com/s/1lrak60XOGEqMOI6lXYf6TQ?pwd=ns0j课程介绍:https://www.aihorizon.cn/72云计算:概念、定义、云类型和服务部署模型。虚拟化的概念使用 Type-2 Hyperv_云计算与虚拟化技术 教改