业务需求,需要用企业微信实现消息通信,一对一通信有接口可以直接实现,下面就说一下这群聊,
企业微信并没有给出群聊获取消息的接口,就需要引入会话存档SDK来获取群聊消息,下面说一下前期的准备工作。
先就是填一个申请表,开通会话存档权限,这个功能是收费的,但是先一个月免费,足够开发调试了,
能用到这个功能的,说明那些基础的已经知道了,那就直接开始了
生成公钥和密钥对,地址:http://web.chacuo.net/netrsakeypair
将公钥填写在客户端,注意版本,修改一个公钥,对应的版本就需要+1
需要这样引入依赖,不然的话有2个依赖有冲突,无法解密
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpg-jdk16</artifactId>
<version>1.46</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.64</version>
</dependency>
将sdk中的.dll文件直接放入C:\Windows\System32目录下,然后在环境变量的Path中添加该目录。
注意:sdk中的Finace.java文件必须放入项目的com.tencent.wework目录下
将下载的.so文件放在改项目的lib文件目录下面,方便寻找,尽量不要放在系统的lib目录下面
需要在dockerfile文件中配置动态链路库,找到改.so文件
第一句是copy文件到lib下面 第二局在启动时加入改环境变量
COPY ./lib/libWeWorkFinanceSdk_Java.so /usr/lib/libWeWorkFinanceSdk_Java.so
ENV LD_LIBRARY_PATH=/disk2/docker/qywx/lib:$LD_LIBRARY_PATH
static {
if(isWindows()){
System.out.println(System.getProperty("java.library.path"));
System.loadLibrary("WeWorkFinanceSdk");
}else {
System.out.println(System.getProperty("java.library.path"));
System.loadLibrary("WeWorkFinanceSdk_Java");
}
}
public static boolean isWindows() {
String osName = System.getProperties().getProperty("os.name");
System.out.println("current system is " + osName);
return osName.toUpperCase().indexOf("WINDOWS") != -1;
}
注意区别:加载windows和linux的文件是不一样的,仔细琢磨以下,这里可以把path打印出来看一下
如果部署到线上linux服务器,引入SDK无报错也无反应,大概率就是这里文件没有加载上
这里插一下如何配置回调url,需要封装get和post两种请求,get只用于验证,post用于业务,这一点要仔细看文档,通过返回的消息体,就可以解析出里面的消息内容,通过mq返回给前端
@GetMapping("/message/callback")
@ResponseBody()
public String callback(HttpServletRequest request, @RequestParam("msg_signature") String sVerifyMsgSig, @RequestParam("timestamp") String sVerifyTimeStamp, @RequestParam("nonce") String sVerifyNonce, @RequestParam("echostr") String sVerifyEchoStr) throws Exception {
String sToken = "";
String sCorpID = "";
String sEncodingAESKey = "";
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);
String sEchoStr; //需要返回的明文
try {
sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp,
sVerifyNonce, sVerifyEchoStr);
return sEchoStr;
// 验证URL成功,将sEchoStr返回
// HttpUtils.SetResponse(sEchoStr);
} catch (Exception e) {
//验证URL失败,错误原因请查看异常
e.printStackTrace();
}
return "error";
}
@PostMapping("/message/callback")
@ResponseBody()
public void callbackData(HttpServletRequest request, @RequestBody() String sRespData, @RequestParam("msg_signature") String sVerifyMsgSig, @RequestParam("timestamp") String sReqTimeStamp, @RequestParam("nonce") String sReqNonce) throws Exception {
String sToken = "";
String sCorpID = "";
String sEncodingAESKey = "";
WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);
String sMsg = wxcpt.DecryptMsg(sVerifyMsgSig, sReqTimeStamp, sReqNonce, sRespData);
System.out.println("解密decrypt企业微信推送的消息->sMsg:" + sMsg);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(sMsg);
InputSource is = new InputSource(sr);
Document document = db.parse(is);
//获取整个XML消息体,进行解析
Element root = document.getDocumentElement();
//返回消息的公共部分,内容,创建时间,消息ID,来源,去处
NodeList nodelistTime = root.getElementsByTagName("CreateTime");
String CreateTime = nodelistTime.item(0).getTextContent();
NodeList nodelistFrom = root.getElementsByTagName("FromUserName");
String fromUserName = nodelistFrom.item(0).getTextContent();
NodeList nodelistTo = root.getElementsByTagName("ToUserName");
String toUserName = nodelistTo.item(0).getTextContent();
Map<String, String> msgMap = new HashMap<String, String>();
msgMap.put("CreateTime", CreateTime);
msgMap.put("fromUser", fromUserName);
msgMap.put("to", toUserName);
//消息类型,不同消息返回不同消息体
NodeList typeNodelist = root.getElementsByTagName("MsgType");
String MsgType = typeNodelist.item(0).getTextContent();
msgMap.put("msgType", MsgType);
//文本直接返回
if (StringUtils.equals(MsgType, "text")) {
NodeList nodelistMsgId = root.getElementsByTagName("MsgId");
String MsgId = nodelistMsgId.item(0).getTextContent();
msgMap.put("msgId", MsgId);
NodeList nodelist = root.getElementsByTagName("Content");
String Content = nodelist.item(0).getTextContent();
msgMap.put("text", Content);
//自定义返回消息类型,只支持字符串类型
Map<String, Object> getMsg = new HashMap<String, Object>();
getMsg.put("msg", msgMap);
String message = JSONArray.toJSON(getMsg).toString();
rabbitTemplate.convertAndSend("qywx_exange", null, message);
//图片返回图片需要增加url和mediaId
}
}
几个注意的点:
1.seq是传入的每次拉取消息索引,可以根据返回消息的长度获取该值
Integer newSeq = (Integer) chatdata.getJSONObject(chatdata.length()-1).get("seq");
System.out.println(newSeq);
2.消息解密和消息加密、
3.linux服务器上消息拉取会乱码,用下面那种方式转一下,不行的话,还是乱码就是Linxu服务器不支持GBK编码,目前我也没解决
public static Map pullMsg(Integer seq) throws Exception {
long sdk = Finance.NewSdk();
Finance.Init(sdk, corpid, secret); // 初始化
long ret = 0;
//int seq = 0;
int limit = 1000;
long slice = Finance.NewSlice();
List msgList = new ArrayList();
Map resultMap = new HashMap();
//拉取聊天记录
ret = Finance.GetChatData(sdk, seq, limit, null, null, 15, slice);
if (ret != 0) {
return resultMap;
}
//获取切片中的内容
String getchatdata = Finance.GetContentFromSlice(slice);
JSONObject jo = new JSONObject(getchatdata);
//聊天内容
JSONArray chatdata = jo.getJSONArray("chatdata");
if(chatdata.length() > 1){
Integer newSeq = (Integer) chatdata.getJSONObject(chatdata.length()-1).get("seq");
resultMap.put("lastSeq",newSeq);
}else{
resultMap.put("lastSeq",0);
}
System.out.println("消息数:" + chatdata.length());
String msgContent = null;
for (int i = 0; i < chatdata.length(); i++) {
String item = chatdata.get(i).toString();
JSONObject data = new JSONObject(item);
//加密密钥
String encrypt_random_key = data.getString("encrypt_random_key");
//加密聊天消息
String encrypt_chat_msg = data.getString("encrypt_chat_msg");
long msg = Finance.NewSlice();
try {
//解密
String message = RSAEncrypt.decryptRSA(encrypt_random_key, priKey);
//使用SDK获取msg明文
ret = Finance.DecryptData(sdk, message, encrypt_chat_msg, msg);
System.out.println(ret);
if (ret != 0) {
return resultMap;
}
msgContent =new String(Finance.GetContentFromSlice(msg).getBytes("GBK"),"UTF-8");
msgList.add(msgContent);
Finance.FreeSlice(msg);
} catch (Exception e) {
e.printStackTrace();
}
}
Finance.FreeSlice(slice);
resultMap.put("data",msgList);
return resultMap;
}
这个方法拿过去就不要动了,直接就能用
public class RSAEncrypt {
public static String decryptRSA(String str, String privateKey) throws Exception {
Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
//此处的"RSA/ECB/PKCS1Padding", "BC"不可以改变,改变会导致解密乱码
Cipher rsa = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
rsa.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKey));
byte[] utf8 = rsa.doFinal(Base64.decodeBase64(str));
String result = new String(utf8, "UTF-8");
return result;
}
public static PrivateKey getPrivateKey (String privateKey) throws Exception {
Reader privateKeyReader = new StringReader(privateKey);
PEMParser privatePemParser = new PEMParser(privateKeyReader);
Object privateObject = privatePemParser.readObject();
if (privateObject instanceof PEMKeyPair) {
PEMKeyPair pemKeyPair = (PEMKeyPair) privateObject;
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKey privKey = converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo());
return privKey;
}
return null;
}
}
最后,总结一下吧,因为这个功能是要实时获取群聊的消息,但这个会话存档不能做到,有点鸡肋,主要是做不到实时,会话存档嘛,有点延迟,大概6-7s吧,体验不好,看了企微那边的介绍,也问了客服找了案例,大多数用这个会话存档做scrm系统