需求说明
由于腾讯云历史消息存储时长为7天,所以在结束群聊后,需要将群聊消息拉取到本地,方面后续随时查看。
这里调用腾讯云 拉取群历史消息接口,这个接口只提供最多20条最近消息记录,所以需要使用递归不断查询,最终得到所有消息记录。
服务端代码
接口工具类
package com.cxbdapp.hbhtyyBackground.utils;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONArray;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
/**
* 群组管理相关接口
*/
@Slf4j
public class GroupSystemUtils {
/**
* 拉取群历史全量消息
*
* @param groupId 拉取消息的群 ID
* @return 群历史全量消息
*/
public static JSONObject groupMsgGetSimple(String groupId) {
try {
JSONObject groupMsgJsonData = groupMsgGetSimplePage(groupId, null);
if (groupMsgJsonData == null) {
return null;
}
Integer code = (Integer) groupMsgJsonData.get("ErrorCode");
if (code != 0) {
return null;
}
JSONArray rspMsgList = groupMsgJsonData.getJSONArray("RspMsgList");
groupMsgRecursive(rspMsgList, groupId);
groupMsgJsonData.put("RspMsgList", rspMsgList);
return groupMsgJsonData;
} catch (Exception e) {
log.error("拉取群历史消息异常", e);
return null;
}
}
/**
* 递归算法,查询所有消息记录
*
* @param rspMsgList 消息记录集合
* @param groupId 群ID
*/
public static void groupMsgRecursive(JSONArray rspMsgList, String groupId) {
JSONObject leastMsgSeq = (JSONObject) rspMsgList.get(rspMsgList.size() - 1);
Integer msgSeq = leastMsgSeq.getInt("MsgSeq");
if (msgSeq > 1) {
JSONObject groupMsgJsonData = groupMsgGetSimplePage(groupId, msgSeq - 1);
if (groupMsgJsonData == null) {
return;
}
Integer code = (Integer) groupMsgJsonData.get("ErrorCode");
if (code != 0) {
return;
}
rspMsgList.addAll(groupMsgJsonData.getJSONArray("RspMsgList"));
groupMsgRecursive(rspMsgList, groupId);
}
}
/**
* 拉取群历史消息
*
* @param groupId 拉取消息的群 ID
* @param reqMsgSeq 请求的消息最大 seq,返回 <=ReqMsgSeq 的消息
* @return 群历史消息
*/
public static JSONObject groupMsgGetSimplePage(String groupId, Integer reqMsgSeq) {
try {
String userSig = GenerateUserSigUtil.getUserSig(AppletUtils.identifier);
String random = GenerateUserSigUtil.get32Random();
String url = String.format(AppletUtils.GROUP_MSG_GET_SIMPLE, AppletUtils.SDKAPPID, AppletUtils.identifier, userSig, random);
JSONObject requestPackage = new JSONObject();
requestPackage.put("GroupId", groupId); //拉取消息的群 ID
if (reqMsgSeq != null) {
requestPackage.put("ReqMsgSeq", reqMsgSeq); //请求的消息最大 seq,返回 <=ReqMsgSeq 的消息
}
requestPackage.put("ReqMsgNumber", 20); //需要拉取的消息条数
String body = HttpUtil.post(url, requestPackage.toString());
log.info("拉取群历史消息应答包体报文:{}", body);
if (StrUtil.isEmpty(body)) {
return null;
}
return JSONUtil.parseObj(body);
} catch (Exception e) {
log.error("拉取群历史消息异常", e);
return null;
}
}
}
即时通信IM辅助工具类(腾讯云SDK拷贝的)
package com.cxbdapp.hbhtyyBackground.utils;
import android.util.Base64;
import cn.hutool.core.util.StrUtil;
import org.json.JSONException;
import org.json.JSONObject;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Random;
import java.util.zip.Deflater;
/**
* Module: GenerateTestUserSig
* <p>
* Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
* 其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
* <p>
* Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
* <p>
* 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
* 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
* 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
* <p>
* 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
* 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
* <p>
* Reference:https://cloud.tencent.com/document/product/269/32688#Server
*/
public class GenerateUserSigUtil {
/*public static void main(String[] args) {
System.out.println(gen32Random());
}*/
public static String get32Random() {
Random rand = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 1; i <= 32; i++) {
int randNum = rand.nextInt(9) + 1;
String num = randNum + "";
sb = sb.append(num);
}
String random = String.valueOf(sb);
return random;
}
/**
* 计算 UserSig 签名
* <p>
* 函数内部使用 HMAC-SHA256 非对称加密算法,对 SDKAPPID、userId 和 EXPIRETIME 进行加密。
*
* @note: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
* <p>
* 本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
* 这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
* 一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
* <p>
* 正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
* 由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
* <p>
* 文档:https://cloud.tencent.com/document/product/269/32688#Server
*/
public static String getUserSig(String userId) {
return getTlsSignature(AppletUtils.SDKAPPID, userId, AppletUtils.EXPIRETIME, null, AppletUtils.SECRETKEY);
}
/**
* 生成 tls 票据
*
* @param sdkappid 应用的 appid
* @param userId 用户 id
* @param expire 有效期,单位是秒
* @param userbuf 默认填写null
* @param priKeyContent 生成 tls 票据使用的私钥内容
* @return 如果出错,会返回为空,或者有异常打印,成功返回有效的票据
*/
private static String getTlsSignature(long sdkappid, String userId, long expire, byte[] userbuf, String priKeyContent) {
if (StrUtil.isEmpty(priKeyContent)) {
return "";
}
long currTime = System.currentTimeMillis() / 1000;
JSONObject sigDoc = new JSONObject();
try {
sigDoc.put("TLS.ver", "2.0");
sigDoc.put("TLS.identifier", userId);
sigDoc.put("TLS.sdkappid", sdkappid);
sigDoc.put("TLS.expire", expire);
sigDoc.put("TLS.time", currTime);
} catch (JSONException e) {
e.printStackTrace();
}
String base64UserBuf = null;
if (null != userbuf) {
base64UserBuf = Base64.encodeToString(userbuf, Base64.NO_WRAP);
try {
sigDoc.put("TLS.userbuf", base64UserBuf);
} catch (JSONException e) {
e.printStackTrace();
}
}
String sig = hmacSha256(sdkappid, userId, currTime, expire, priKeyContent, base64UserBuf);
if (sig.length() == 0) {
return "";
}
try {
sigDoc.put("TLS.sig", sig);
} catch (JSONException e) {
e.printStackTrace();
}
Deflater compressor = new Deflater();
compressor.setInput(sigDoc.toString().getBytes(Charset.forName("UTF-8")));
compressor.finish();
byte[] compressedBytes = new byte[2048];
int compressedBytesLength = compressor.deflate(compressedBytes);
compressor.end();
return new String(base64EncodeUrl(Arrays.copyOfRange(compressedBytes, 0, compressedBytesLength)));
}
private static String hmacSha256(long sdkappid, String userId, long currTime, long expire, String priKeyContent, String base64Userbuf) {
String contentToBeSigned = "TLS.identifier:" + userId + "\n"
+ "TLS.sdkappid:" + sdkappid + "\n"
+ "TLS.time:" + currTime + "\n"
+ "TLS.expire:" + expire + "\n";
if (null != base64Userbuf) {
contentToBeSigned += "TLS.userbuf:" + base64Userbuf + "\n";
}
try {
byte[] byteKey = priKeyContent.getBytes("UTF-8");
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec keySpec = new SecretKeySpec(byteKey, "HmacSHA256");
hmac.init(keySpec);
byte[] byteSig = hmac.doFinal(contentToBeSigned.getBytes("UTF-8"));
return new String(Base64.encode(byteSig, Base64.NO_WRAP));
} catch (UnsupportedEncodingException e) {
return "";
} catch (NoSuchAlgorithmException e) {
return "";
} catch (InvalidKeyException e) {
return "";
}
}
private static byte[] base64EncodeUrl(byte[] input) {
byte[] base64 = new String(Base64.encode(input, Base64.NO_WRAP)).getBytes();
for (int i = 0; i < base64.length; ++i)
switch (base64[i]) {
case '+':
base64[i] = '*';
break;
case '/':
base64[i] = '-';
break;
case '=':
base64[i] = '_';
break;
default:
break;
}
return base64;
}
}
静态变量工具类
package com.cxbdapp.hbhtyyBackground.utils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @Author cgp/zxy
* @Date 2020/11/02 11:40
* @Description 小程序公用静态变量工具类
*/
@Component
public class AppletUtils {
/**
* 拉取群历史消息
*
* @author zxy
*/
public static final String GROUP_MSG_GET_SIMPLE = "https://console.tim.qq.com/v4/group_open_http_svc/group_msg_get_simple?sdkappid=%s&identifier=%s&usersig=%s&random=%s&contenttype=json";
/**
* App 管理员帐号
*
* @author zxy
*/
public static String identifier;
@Value("${im.identifier}")
public void setIdentifier(String identifier) {
this.identifier = identifier;
}
/**
* 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
* <p>
* 进入腾讯云云通信[控制台](https://console.cloud.tencent.com/avc ) 创建应用,即可看到 SDKAppId,
* 它是腾讯云用于区分客户的唯一标识。
*
* @author zxy
*/
public static int SDKAPPID;
@Value("${tencent.cloud.sdkAppId}")
public void setSDKAPPID(int SDKAPPID) {
this.SDKAPPID = SDKAPPID;
}
/**
* 签名过期时间,建议不要设置的过短
* <p>
* 时间单位:秒
* 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
*
* @author zxy
*/
public static int EXPIRETIME;
@Value("${im.expireTime}")
public void setEXPIRETIME(int EXPIRETIME) {
this.EXPIRETIME = EXPIRETIME;
}
/**
* 计算签名用的加密密钥,获取步骤如下:
* <p>
* step1. 进入腾讯云云通信[控制台](https://console.cloud.tencent.com/avc ) ,如果还没有应用就创建一个,
* step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
* step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
* <p>
* 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
* 文档:https://cloud.tencent.com/document/product/269/32688#Server
*
* @author zxy
*/
public static String SECRETKEY;
@Value("${im.secretKey}")
public void setSECRETKEY(String SECRETKEY) {
this.SECRETKEY = SECRETKEY;
}
}
配置文件application.properties
spring.profiles.active=dev
#spring.profiles.active=test
#spring.profiles.active=online
# App 管理员帐号
im.identifier=administrator
# 签名过期时间,时间单位:秒;7 x 24 x 60 x 60 = 604800 = 7 天
im.expireTime=604800
# 腾讯云 SDKAppId
tencent.cloud.sdkAppId=1234123412
# 计算签名用的加密密钥
im.secretKey=...