企业微信-会话内容存档(从零开始,完整demo)
2022-10-24 15:45:48
280
{{single.collect_count}}

1、企业微信-会话内容存档-配置服务器

a.接收事件服务器配置

        1)token、sEncodingAESKey   随机获取即可,保存好下面会用到;

        2)url,需要后台部署服务,外网可以访问,接口如下;

/** * 验证回调URL * 企业开启回调模式时,企业微信会向验证url发送一个get请求 * 假设点击验证时,企业收到类似请求: * * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3×tamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D * * HTTP/1.1 Host: qy.weixin.qq.com * <p> * 接收到该请求时,企业应1.解析出Get请求的参数,包括消息体签名(msg_signature),时间戳(timestamp),随机数字串(nonce)以及企业微信推送过来的随机加密字符串(echostr), * 这一步注意作URL解码。 * 2.验证消息体签名的正确性 * 3. 解密出echostr原文,将原文当作Get请求的response,返回给企业微信 * 第2,3步可以用企业微信提供的库函数VerifyURL来实现。 */@Override@GetMapping("weChatPush")public String weChatPush() throws AesException {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();//String sToken = "QDG6xxx";String sToken = enterpriseWechatConfig.getQyChatToken();//String sCorpID = "wx5823bf9xxxxxxxx";String sCorpID = enterpriseWechatConfig.getQyCorpid();//String sEncodingAESKey = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1txxxxxxxxxxx";String sEncodingAESKey = enterpriseWechatConfig.getQyChatEncodingAESKey();log.info("获取 diamond 配置 sToken:{}sCorpID:{} sEncodingAESKey:{}", sToken, sCorpID, sEncodingAESKey);WXBizMsgCrypt wxcpt = new WXBizMsgCrypt(sToken, sEncodingAESKey, sCorpID);// 解析出url上的参数值如下:String sVerifyMsgSig = request.getParameter("msg_signature");String sVerifyTimeStamp = request.getParameter("timestamp");String sVerifyNonce = request.getParameter("nonce");String sVerifyEchoStr = request.getParameter("echostr");//需要返回的明文String sEchoStr = null;log.info("获取 url 参数 sVerifyMsgSig:{}sVerifyTimeStamp:{} sVerifyNonce:{}sVerifyEchoStr:{}",sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);try {sEchoStr = wxcpt.VerifyURL(sVerifyMsgSig, sVerifyTimeStamp, sVerifyNonce, sVerifyEchoStr);log.info("返回的明文: {}", sEchoStr);return sEchoStr;} catch (Exception e) {//验证URL失败,错误原因请查看异常log.info("验证URL失败,错误原因请查看异常e:{}", e);}return sEchoStr;}

b.接收事件服务器配置

        可能出现问题:ip不可信

        解决方案:请删掉配置的ip,不要设置ip!        不要设置ip!        不要设置ip!

c.配置公钥

        1)生成密钥对(RSA,2048,PKCS#1),保存好公钥和私钥,后面会用到

                最简单的方法:http://web.chacuo.net/netrsakeypair

        2)将公钥填写到企业微信后台

                保存公钥后可以查看到【公钥版本 1】,【管理凭证密钥 secret】 这个后面会用到

2、企业微信-会话内容存档-处理消息(以linux为例)

        企业微信官方文档:获取会话内容 - 接口文档 - 企业微信开发者中心s​​​​​​

        企业微信官方文档,真的烂! 起码对我很不友好(可能我理解能力太差了吧,好多地方有问题)

a.引入依赖

        可能会出现的错误信息:「class "org.bouncycastle.openssl.PEMException"'s signer information does not match signer information of other classes in the same package」

        原因:bcpg-jdk16 中的 bcprov-jdk16 与  bcpkix-jdk15on 中的 bcprov-jdk15on 重复

        解决方案:需要排除 bcprov-jdk16,否则会报, 代码如下

<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>

b.下载sdk,将sdk微信提供的几个类放到项目中,注意包名要相同

Finance文件乱码,我这里稍微修改了一下

package com.tencent.wework;/** * 企业微信会话sdk * 官网文档字符集有问题,注释有找到补充一下 * @Author: hyl * @Date: 2022/2/25 */public class Finance {public native static long NewSdk();/** * 初始化函数 * Return值=0表示该API调用成功 * * @param [in]sdkNewSdk返回的sdk指针 * @param [in]corpid调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看 * @param [in]secret聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 * * @return 返回是否初始化成功 *0 - 成功 *!=0 - 失败 */public native static int Init(long sdk, String corpid, String secret);/** * 拉取聊天记录函数 * Return值=0表示该API调用成功 * * * @param [in]sdkNewSdk返回的sdk指针 * @param [in]seq从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 * @param [in]limit一次拉取的消息条数,最大值1000条,超过1000条会返回错误 * @param [in]proxy使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 * @param [in]passwd代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 * @param [out] chatDatas返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。 * * @return 返回是否调用成功 *0 - 成功 *!=0 - 失败 */public native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData);/** * 拉取媒体消息函数 * Return值=0表示该API调用成功 * * * @param [in]sdkNewSdk返回的sdk指针 * @param [in]sdkFileid从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid * @param [in]proxy使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 * @param [in]passwd代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 * @param [in]indexbuf媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。 * @param [out] media_data返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记) * * @return 返回是否调用成功 *0 - 成功 *!=0 - 失败 */public native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);/** * @brief 解析密文.企业微信自有解密内容 * @param [in]encrypt_key, getchatdata返回的encrypt_random_key,使用企业自持对应版本秘钥RSA解密后的内容 * @param [in]encrypt_msg, getchatdata返回的encrypt_chat_msg * @param [out] msg, 解密的消息明文 * @return 返回是否调用成功 *0 - 成功 *!=0 - 失败 */public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg);public native static void DestroySdk(long sdk);public native static long NewSlice();/** * @brief 释放slice,和NewSlice成对使用 * @return */public native static void FreeSlice(long slice);/** * @brief 获取slice内容 * @return 内容 */public native static String GetContentFromSlice(long slice);/** * @brief 获取slice内容长度 * @return 内容 */public native static int GetSliceLen(long slice);public native static long NewMediaData();public native static void FreeMediaData(long mediaData);/** * @brief 获取mediadata outindex * @return outindex */public native static String GetOutIndexBuf(long mediaData);/** * @brief 获取mediadata data数据 * @return data */public native static byte[] GetData(long mediaData);public native static int GetIndexLen(long mediaData);public native static int GetDataLen(long mediaData);/** * @brief 判断mediadata是否结束 * @return 1完成、0未完成 */public native static int IsMediaDataFinish(long mediaData);static {System.loadLibrary("WeWorkFinanceSdk_Java");}}

c.配置so文件

        官方提供了windows、linux两种环境,本人开发环境为mac系统,暂时未找到mac加载方法,只能本地开发,linux部署测试;

        方案1:so文件上传到指定目录,服务器启动加载外部so文件;

        1)将 libWeWorkFinanceSdk_Java.so 上传到 /home/solib 目录下(自己定义)

        2)linux环境启动项目时增加启动命令:-Djava.library.path=/home/solib  (如果配置到全局环境变量中也可以不增加启动命令)

        

        方案2:将so文件打包到项目中,服务器启动加载内部so文件;

        1)将so文件放到resources下,新建linux-x86-64文件夹内

         2)修改Finance类的静态代码块

static {try {String path = System.getProperty("java.io.tmpdir");String name = "libWeWorkFinanceSdk_Java.so";// 获取sources下的资源ClassPathResource classPathResource = new ClassPathResource("linux-x86-64/" + name);InputStream in = classPathResource.getInputStream();// 写入到临时文件FileUtil.writeStream(path + name, in);System.load(path + name);log.info("{}so文件加载完成",path+ name );} catch (IOException e) {log.info("so文件加载识别:{}",e);}}

        

d.拉消息,并且解密

/** * 拉会话消息 */@Overridepublic void pullChat() {//使用sdk前需要初始化,初始化成功后的sdk可以一直使用。//如需并发调用sdk,建议每个线程持有一个sdk实例。log.info("加载企业微信sdk开始");long sdk = Finance.NewSdk();log.info("创建企业微信sdk成功");// 企业idString corpid = enterpriseWechatConfig.getQyCorpid();// 管理凭证密钥 配置完公钥后 可以获取String secret = enterpriseWechatConfig.getQyChatSecret();// 私钥,与公钥为一对String priKey = enterpriseWechatConfig.getQyChatPriKey();// 公钥版本号,判断消息能否解密String pubKeyVer = enterpriseWechatConfig.getQyChatPubKeyVer();log.info("读取配置文件corpid:{}secret:{}priKey:{}", corpid, secret, priKey);Finance.Init(sdk, corpid, secret); // 初始化// seq 表示该企业存档消息序号,该序号单调递增,拉取序号建议设置为上次拉取返回结果中最大序号。// 首次拉取时seq传0,sdk会返回【有效期内】最早的消息。int seq = 0; // 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0(这个值需要记录下来,以便下一次的拉去)int limit = 1000;//创建切片long slice = Finance.NewSlice();try {//拉取聊天记录long ret = Finance.GetChatData(sdk, seq, limit, null, null, 5, slice);if (ret != 0) {log.info("拉取聊天记录失败 ret:{}", ret);return;}//获取切片中的内容String contentFromSlice = Finance.GetContentFromSlice(slice);log.info(seq + ",拉去的聊天记录密文结果:{}", contentFromSlice);// 测试完成后去掉JSONObject contentJsonObject = JSONObject.parseObject(contentFromSlice);//聊天内容JSONArray chatdata = contentJsonObject.getJSONArray("chatdata");for (int i = 0; i < chatdata.size(); i++) {log.info("开始循环处理,第{}条数据", i);JSONObject data = chatdata.getJSONObject(i);//公钥版本Integer publicKeyVer = data.getInteger("publickey_ver");if(ObjectUtil.notEqual(publicKeyVer,pubKeyVer)){log.info("公钥版本不一致,无法解密当前消息,当前消息版本:{} 系统配置版本:{}",publicKeyVer,pubKeyVer);continue;}//加密密钥String encryptRandomKey = data.getString("encrypt_random_key");//加密聊天消息String encryptChatMsg = data.getString("encrypt_chat_msg");long msg = Finance.NewSlice();try {// 获取加密密钥String encryptKey = RSAEncrypt.decryptRSA(encryptRandomKey, priKey);log.info("解析密文.企业微信自有解密内容");// 解析密文.企业微信自有解密内容ret = Finance.DecryptData(sdk, encryptKey, encryptChatMsg, msg);if (ret != 0) {log.info("解密聊天记录失败 ret :{}", ret);continue;}// 获取切片中的内容String plaintext = Finance.GetContentFromSlice(msg);log.info("解密结果:{}", plaintext);// 释放sliceFinance.FreeSlice(msg);JSONObject plaintextJson = JSONObject.parseObject(plaintext);// 文件类型"text"文本 ,"revoke"撤回消息String msgtype = plaintextJson.getString("msgtype");if (StrUtil.equals(msgtype, "text")) {log.info("文本消息:{}", plaintextJson.getJSONObject("text").getString("content"));} else {log.info("其他消息");}log.info("会话内容写入数据库 ,存储消息,类型,时间,userid,等信息:{}", plaintextJson);} catch (Exception e) {log.error("循环拉会话异常:e:{}", e);}}} catch (Exception e) {log.error("拉会话消息异常:e:{}", e);} finally {// 释放sliceFinance.FreeSlice(slice);}}

解密消息时,需要注意消息中 publickey_ver 字段和企业微信后台中版本号一致, 每次更新公钥后版本号都会+1,只有更改后发送的消息才会 使用新的版本号! 

私钥格式:

String priKey = "-----BEGIN RSA PRIVATE KEY-----\n" +"MIICXAIBAAKBgQCLTqqYHxxxxx省略100字F32v5bfw3NzzwVHU\n" +"-----END RSA PRIVATE KEY-----";

回帖
全部回帖({{commentCount}})
{{item.user.nickname}} {{item.user.group_title}} {{item.friend_time}}
{{item.content}}
{{item.comment_content_show ? '取消' : '回复'}} 删除
回帖
{{reply.user.nickname}} {{reply.user.group_title}} {{reply.friend_time}}
{{reply.content}}
{{reply.comment_content_show ? '取消' : '回复'}} 删除
回帖
收起
没有更多啦~
{{commentLoading ? '加载中...' : '查看更多评论'}}