网络知识 娱乐 企业微信-会话内容存档(从零开始,完整demo)

企业微信-会话内容存档(从零开始,完整demo)

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

a.接收事件服务器配置

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

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

/**
     * 验证回调URL
     * 企业开启回调模式时,企业微信会向验证url发送一个get请求
     * 假设点击验证时,企业收到类似请求:
     * * GET /cgi-bin/wxpush?msg_signature=5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3&timestamp=1409659589&nonce=263014780&echostr=P9nAzCzyDtyTWESHep1vC5X9xho%2FqYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp%2B4RPcs8TgAE7OaBO%2BFZXvnaqQ%3D%3D
     * * HTTP/1.1 Host: qy.weixin.qq.com
     * 

* 接收到该请求时,企业应 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,否则会报, 代码如下

        
            org.bouncycastle
            bcpg-jdk16
            1.46
            
                
                    org.bouncycastle
                    bcprov-jdk16
                
            
        
        
            org.bouncycastle
            bcpkix-jdk15on
            1.64
        

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]  sdk			NewSdk返回的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]  sdk				NewSdk返回的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]  sdk				NewSdk返回的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.拉消息,并且解密

/**
     * 拉会话消息
     */
    @Override
    public void pullChat() {

        //使用sdk前需要初始化,初始化成功后的sdk可以一直使用。
        //如需并发调用sdk,建议每个线程持有一个sdk实例。
        log.info("加载企业微信sdk开始");
        long sdk = Finance.NewSdk();
        log.info("创建企业微信sdk成功");
        // 企业id
        String 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);
                    // 释放slice
                    Finance.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 {
            // 释放slice
            Finance.FreeSlice(slice);
        }
    }

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

私钥格式:

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