最近公司需要做一个腾讯会议API的对接,经过查看官方文档和腾讯相关技术人员周旋~~以及不断踩坑,终于在我的不懈努力下,成功将API接口调通了
腾讯会议API接口文档:https://cloud.tencent.com/document/product/1095/42407
文章目录
一、腾讯会议JWT鉴权
- 腾讯会议API接口文档中详细写了使用API需要做的准备: 包括-创建应用,以及获取相应应用的SecretId,SecretKey,AppId等
- 根据不同的应用创建方式,选择的鉴权方式也不同。
- 腾讯会议API提供了两套鉴权方式,在这里我只介绍企业自建应用鉴权(JWT)
1、公共参数
公共参数是用于标识用户和接口鉴权目的的参数,如非必要,在每个接口单独的接口文档中不再对这些参数进行说明,但每次请求均需要携带这些参数,才能正常发起请求。
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. 通过企业管理后台添加或批量导入企业用户。 |
2、签名算法
可参考腾讯会议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;
}
三、封装HTTP请求方法
为了方便调用API,我封装了GET和POST方法
1、GET方法
/**
* 发起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;
}
2、POST方法
/**
* 腾讯会议发送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
REST APIs:https://cloud.tencent.com/document/product/1095/42414
1、创建会议
- 传参参考官方文档(https://cloud.tencent.com/document/product/1095/42417),我这里只传了必要参数
需要注意的是会议主题 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;
}
}
2、查询会议
/**
* 根据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;
}
3、取消会议
/**
* 取消预约的会议
*
* @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),找腾讯售后寻求技术支持,客服还是很负责任的。