企业微信会话存档详细搭建以及问题处理


写在前面

由于公司业务需求,需要开发企业微信的会话存档,从开始阅读文档到获取解密数据,中间趟过了不少坑,在此记录一下。


一、准备工作

  1. 登录企业微信后台,获取企业配置参数,企业微信后台登录地址:企业微信登录地址

  2. 生成一对公私钥,保存好密钥对生成地址在这里插入图片描述

  3. 获取企业编码在这里插入图片描述

  4. 设置企业后台,管理工具->会话内容存档,设置可信IP地址,获取会话存档Secret,设置消息加密公钥(将第二步生成的公钥填进去),需要带上BEGIN以及END在这里插入图片描述

  5. 下载SDK,windows下为.dll文件,linux系统下为libWeWorkFinanceSdk_Java.so,使用SDK需要将
    Finance.java放入到项目的指定目录下,必须放下com.tencent.wework下在这里插入图片描述

  6. Finance.java修改静态方法,由于我们部署在linux服务器中,所以加载.so文件,其它文章有说配置全局变量或者啥环境,觉得太麻烦,我这里配置的是文件的绝对路径,将libWeWorkFinanceSdk_Java.so上传到服务器上指定位置在这里插入图片描述

  7. 项目中添加需要使用的依赖,注意!!!需要排除掉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>
        <dependency>
            <groupId>org.bouncycastle</groupId>
            <artifactId>bcprov-jdk15on</artifactId>
            <version>1.64</version>
        </dependency>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.9</version>
        </dependency>

二、使用步骤

  1. 企业微信后台设置会话回调事件,可以自己生成或者随机获取Token和AESKey需要保存好后续有用;配置测试回调地址,请求方式为GET在这里插入图片描述
@GetMapping("/weChatPush")
    public String pathCheck(@RequestParam("msg_signature") String msgSignature
            , @RequestParam("timestamp") String timestamp
            , @RequestParam("nonce") String nonce
            , @RequestParam("echostr") String echostr) throws AesException {

        String sToken = "";
        String sCorpID = "";
        String sEncodingAESKey = "";
        WXBizJsonMsgCrypt wxcpt = new WXBizJsonMsgCrypt(sToken, sEncodingAESKey, sCorpID);
        //需要返回的明文
        String sEchoStr = null;
        log.info("获取 url 参数 msgSignature:{}  timestamp:{}   nonce:{}  echostr:{}",
                msgSignature, timestamp, nonce, echostr);
        try {
            sEchoStr = wxcpt.VerifyURL(msgSignature, timestamp, nonce, echostr);
            log.info("返回的明文: {}", sEchoStr);
            return sEchoStr;
        } catch (Exception e) {
            //验证URL失败,错误原因请查看异常
            log.error("验证URL失败,错误原因请查看异常", e);
        }
        return sEchoStr;
    }
/**
 * 对企业微信发送给企业后台的消息加解密示例代码.
 * 
 * @copyright Copyright (c) 1998-2014 Tencent Inc.
 */

// ------------------------------------------------------------------------

/**
 * 针对org.apache.commons.codec.binary.Base64,
 * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本)
 * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi
 */
package com.junbaooss.common.util.wechat;

import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.codec.binary.Base64;

/**
 * 提供接收和推送给企业微信消息的加解密接口(UTF8编码的字符串).
 * <ol>
 * 	<li>第三方回复加密消息给企业微信</li>
 * 	<li>第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。</li>
 * </ol>
 * 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案
 * <ol>
 * 	<li>在官方网站下载JCE无限制权限策略文件(JDK7的下载地址:
 *      http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html</li>
 * 	<li>下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt</li>
 * 	<li>如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件</li>
 * 	<li>如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件</li>
 * </ol>
 */
public class WXBizJsonMsgCrypt {
	static Charset CHARSET = Charset.forName("utf-8");
	Base64 base64 = new Base64();
	byte[] aesKey;
	String token;
	String receiveid;

	/**
	 * 构造函数
	 * @param token 企业微信后台,开发者设置的token
	 * @param encodingAesKey 企业微信后台,开发者设置的EncodingAESKey
	 * @param receiveid, 不同场景含义不同,详见文档
	 * 
	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
	 */
	public WXBizJsonMsgCrypt(String token, String encodingAesKey, String receiveid) throws AesException {
		if (encodingAesKey.length() != 43) {
			throw new AesException(AesException.IllegalAesKey);
		}

		this.token = token;
		this.receiveid = receiveid;
		aesKey = Base64.decodeBase64(encodingAesKey + "=");
	}

	// 生成4个字节的网络字节序
	byte[] getNetworkBytesOrder(int sourceNumber) {
		byte[] orderBytes = new byte[4];
		orderBytes[3] = (byte) (sourceNumber & 0xFF);
		orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
		orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
		orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
		return orderBytes;
	}

	// 还原4个字节的网络字节序
	int recoverNetworkBytesOrder(byte[] orderBytes) {
		int sourceNumber = 0;
		for (int i = 0; i < 4; i++) {
			sourceNumber <<= 8;
			sourceNumber |= orderBytes[i] & 0xff;
		}
		return sourceNumber;
	}

	// 随机生成16位字符串
	String getRandomStr() {
		String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
		Random random = new Random();
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < 16; i++) {
			int number = random.nextInt(base.length());
			sb.append(base.charAt(number));
		}
		return sb.toString();
	}

	/**
	 * 对明文进行加密.
	 * 
	 * @param text 需要加密的明文
	 * @return 加密后base64编码的字符串
	 * @throws AesException aes加密失败
	 */
	String encrypt(String randomStr, String text) throws AesException {
		ByteGroup byteCollector = new ByteGroup();
		byte[] randomStrBytes = randomStr.getBytes(CHARSET);
		byte[] textBytes = text.getBytes(CHARSET);
		byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
		byte[] receiveidBytes = receiveid.getBytes(CHARSET);

		// randomStr + networkBytesOrder + text + receiveid
		byteCollector.addBytes(randomStrBytes);
		byteCollector.addBytes(networkBytesOrder);
		byteCollector.addBytes(textBytes);
		byteCollector.addBytes(receiveidBytes);

		// ... + pad: 使用自定义的填充方式对明文进行补位填充
		byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
		byteCollector.addBytes(padBytes);

		// 获得最终的字节流, 未加密
		byte[] unencrypted = byteCollector.toBytes();

		try {
			// 设置加密模式为AES的CBC模式
			Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
			SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
			IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
			cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);

			// 加密
			byte[] encrypted = cipher.doFinal(unencrypted);

			// 使用BASE64对加密后的字符串进行编码
			String base64Encrypted = base64.encodeToString(encrypted);

			return base64Encrypted;
		} catch (Exception e) {
			e.printStackTrace();
			throw new AesException(AesException.EncryptAESError);
		}
	}

	/**
	 * 对密文进行解密.
	 * 
	 * @param text 需要解密的密文
	 * @return 解密得到的明文
	 * @throws AesException aes解密失败
	 */
	String decrypt(String text) throws AesException {
		byte[] original;
		try {
			// 设置解密模式为AES的CBC模式
			Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
			SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
			IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
			cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);

			// 使用BASE64对密文进行解码
			byte[] encrypted = Base64.decodeBase64(text);

			// 解密
			original = cipher.doFinal(encrypted);
		} catch (Exception e) {
			e.printStackTrace();
			throw new AesException(AesException.DecryptAESError);
		}

		String jsonContent, from_receiveid;
		try {
			// 去除补位字符
			byte[] bytes = PKCS7Encoder.decode(original);

			// 分离16位随机字符串,网络字节序和receiveid
			byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

			int jsonLength = recoverNetworkBytesOrder(networkOrder);

			jsonContent = new String(Arrays.copyOfRange(bytes, 20, 20 + jsonLength), CHARSET);
			from_receiveid = new String(Arrays.copyOfRange(bytes, 20 + jsonLength, bytes.length),
					CHARSET);
		} catch (Exception e) {
			e.printStackTrace();
			throw new AesException(AesException.IllegalBuffer);
		}

		// receiveid不相同的情况
		if (!from_receiveid.equals(receiveid)) {
			throw new AesException(AesException.ValidateCorpidError);
		}
		return jsonContent;

	}

	/**
	 * 将企业微信回复用户的消息加密打包.
	 * <ol>
	 * 	<li>对要发送的消息进行AES-CBC加密</li>
	 * 	<li>生成安全签名</li>
	 * 	<li>将消息密文和安全签名打包成json格式</li>
	 * </ol>
	 * 
	 * @param replyMsg 企业微信待回复用户的消息,json格式的字符串
	 * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
	 * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
	 * 
	 * @return 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的json格式的字符串
	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
	 */
	public String EncryptMsg(String replyMsg, String timeStamp, String nonce) throws AesException {
		// 加密
		String encrypt = encrypt(getRandomStr(), replyMsg);

		// 生成安全签名
		if (timeStamp == "") {
			timeStamp = Long.toString(System.currentTimeMillis());
		}

		String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);

		// System.out.println("发送给平台的签名是: " + signature[1].toString());
		// 生成发送的json
		String result = JsonParse.generate(encrypt, signature, timeStamp, nonce);
		return result;
	}

	/**
	 * 检验消息的真实性,并且获取解密后的明文.
	 * <ol>
	 * 	<li>利用收到的密文生成安全签名,进行签名验证</li>
	 * 	<li>若验证通过,则提取json中的加密消息</li>
	 * 	<li>对消息进行解密</li>
	 * </ol>
	 * 
	 * @param msgSignature 签名串,对应URL参数的msg_signature
	 * @param timeStamp 时间戳,对应URL参数的timestamp
	 * @param nonce 随机串,对应URL参数的nonce
	 * @param postData 密文,对应POST请求的数据
	 * 
	 * @return 解密后的原文
	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
	 */
	public String DecryptMsg(String msgSignature, String timeStamp, String nonce, String postData)
			throws AesException {

		// 密钥,公众账号的app secret
		// 提取密文
		Object[] encrypt = JsonParse.extract(postData);

		// 验证安全签名
		String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt[1].toString());

		// 和URL中的签名比较是否相等
		// System.out.println("第三方收到URL中的签名:" + msg_sign);
		// System.out.println("第三方校验签名:" + signature);
		if (!signature.equals(msgSignature)) {
			throw new AesException(AesException.ValidateSignatureError);
		}

		// 解密
		String result = decrypt(encrypt[1].toString());
		return result;
	}

	/**
	 * 验证URL
	 * @param msgSignature 签名串,对应URL参数的msg_signature
	 * @param timeStamp 时间戳,对应URL参数的timestamp
	 * @param nonce 随机串,对应URL参数的nonce
	 * @param echoStr 随机串,对应URL参数的echostr
	 * 
	 * @return 解密之后的echostr
	 * @throws AesException 执行失败,请查看该异常的错误码和具体的错误信息
	 */
	public String VerifyURL(String msgSignature, String timeStamp, String nonce, String echoStr)
			throws AesException {
		String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);

		if (!signature.equals(msgSignature)) {
			throw new AesException(AesException.ValidateSignatureError);
		}

		String result = decrypt(echoStr);
		return result;
	}

}
  1. 点击保存的时候会对填写的接口发起请求,验证成功才能保存成功
  2. 当在会话存档的用户接收或者发送消息时,会对配置的接口发起一次请求(此处请求为POST),项目中可以根据回调去拉取会话数据,此处回调参数没有什么意义,不用在意参数,拉取消息解密代码及解密方法如下
@PostMapping("/weChatPush")
    public String callBack() {

        informationPullService.pullAndSaveMessage();

        return "200";
    }


public void pullAndSaveMessage() {
        long sdk = Finance.NewSdk();
        log.info("sdk:{}", sdk);
        log.info("初始化:{}", String.valueOf(Finance.Init(sdk, "", "")));
        String priKey = "-----BEGIN RSA PRIVATE KEY-----\n" +
               
                "-----END RSA PRIVATE KEY-----";

        /**
         * 获取seq,缓存如果不存在,则默认为0
         */
        String redisKey = "weChatMessage:gather:junbao";

        Long seq = 0L;

        if (redisUtil.exists(redisKey)) {

            seq = Long.parseLong(redisUtil.getString(redisKey));
        }

        long slice = Finance.NewSlice();
        long ret = Finance.GetChatData(sdk, seq, 100, null, null, 30, slice);
        if (ret != 0) {
            log.info("getchatdata ret " + ret);
            return;
        }

        WeChatMessageDTO weChatMessageDTO = JSONObject.parseObject(Finance.GetContentFromSlice(slice), WeChatMessageDTO.class);

        if (0 != weChatMessageDTO.getErrcode()) {
            log.error("获取会话数据失败:{}", weChatMessageDTO.getErrmsg());
            return;
        }

        //聊天内容
        List<WeChatMessageItemDTO> chatdata = weChatMessageDTO.getChatdata();
        for (WeChatMessageItemDTO chatdatum : chatdata) {

            //消息序号
            Long dataLong = chatdatum.getSeq();

            if (dataLong > seq) {
                seq = dataLong;
            }

            //消息ID
            String msgid = chatdatum.getMsgid();

            //加密密钥
            String encryptRandomKey = chatdatum.getEncrypt_random_key();
            //加密聊天消息
            String encryptChatMsg = chatdatum.getEncrypt_chat_msg();
            long msg = Finance.NewSlice();
            try {
                // 获取加密密钥
                String encryptKey = RsaUtil.decryptRSA(encryptRandomKey, priKey);
                // 解析密文.企业微信自有解密内容
                ret = Finance.DecryptData(sdk, encryptKey, encryptChatMsg, msg);
                if (ret != 0) {
                    log.info("解密聊天记录失败 ret :{}", ret);
                    continue;
                }
                // 获取切片中的内容
                String plaintext = Finance.GetContentFromSlice(msg);
                // 释放slice
                Finance.FreeSlice(msg);
                JSONObject plaintextJson = JSONObject.parseObject(plaintext);
                // 文件类型  "text"文本 ,"revoke"撤回消息
                String msgtype = plaintextJson.getString("msgtype");

                WeChatMessageGather weChatMessageGather = WeChatMessageGather.builder()
                        .msgId(msgid)
                        .messageType(msgtype)
                        .content(plaintext)
                        .seq(seq)
                        .createTime(new Date())
                        .build();

                boolean save = weChatMessageGatherService.save(weChatMessageGather);

                log.info("会话内容写入数据库:{}", save);
            } catch (Exception e) {
                log.error("循环拉会话异常", e);
            }
        }

        redisUtil.setDataAndExpiryDays(redisKey, String.valueOf(seq), 365);

        Finance.FreeSlice(slice);

        Finance.DestroySdk(sdk);
    }

@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeChatMessageDTO implements Serializable {

/**
 * 0=拉取成功
 */
private Integer errcode;

private String errmsg;

private List<WeChatMessageItemDTO> chatdata;

}

@Data
@AllArgsConstructor
@NoArgsConstructor
public class WeChatMessageItemDTO implements Serializable {

/**
 * 消息序号
 */
private Long seq;

/**
 * 消息ID
 */
private String msgid;

private Integer publickey_ver;

private String encrypt_random_key;

private String encrypt_chat_msg;

}

解密方法

    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 {
        StringReader 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;
    }

说明

  1. 拉取的消息数据,msg_id唯一,可以通过这个字段,对消息进行去重
  2. 在接收到回调时,如何去拉取最新的数据呢?第一次拉取消息的seq是0,拉取消息的返回数据中有每一条消息对应的seq,可以将最大的seq更新、保存下来,在接收到数据回调之后,拉取数据时将最新的seq传入接口

过程中出现的问题以及解决办法

  1. 找不到SDK,需要确定填写的路径上是否有对应版本的jar包
  2. windows下报错:org.bouncycastle.openssl.PEMException"'s signer information does not match signer information of other classes in the same package ,linux下报错:org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.VerifyError: class org.bouncycastle.asn1.ASN1Primitive overrides final method equals.(Ljava/lang/Object;)Z 解决办法:确认一下pom有没有排除重复的jar包
  3. linux下报错:java.security.InvalidKeyException: unknown key type passed to RSA 解决办法:确认一下解密用的私钥,不要去除 BEGIN 、END以及换行符,保留原汁原味

结语

目前遇到的问题以及解决办法都在上面,当前是可以拉取并解密数据,如果你们还有其它问题,欢迎到评论区留言

  • 2
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
Spring Boot是一种用于开发Java应用程序的开源框架,它简化了基于Spring框架的应用程序的配置和部署。企业微信是一款为企业提供即时通讯和协同办公的应用,它能轻松满足企业的沟通和协作需求。 为了在Spring Boot应用程序中引入企业微信会话存档SDK,需要执行以下步骤: 1. 下载企业微信会话存档SDK:你可以从企业微信官方网站或其他可信来源下载适用于Java企业微信会话存档SDK。 2. 引入SDK依赖:将下载的SDK文件导入Spring Boot应用程序的依赖管理系统中。可以使用Maven或Gradle等构建工具来管理依赖,并在配置文件中指定SDK的版本号。 3. 创建企业微信会话存档SDK的配置文件:在Spring Boot的配置文件中,添加企业微信会话存档SDK所需的配置参数,如企业微信的Corpid、Secret和Agentid等。 4. 创建企业微信会话存档SDK的服务类:在Spring Boot应用程序中创建一个服务类,用于调用企业微信会话存档SDK的接口。根据业务需求,可以根据文档提供的接口进行数据的存储、获取等操作。 5. 在应用程序中调用企业微信会话存档服务:在需要使用企业微信会话存档功能的地方,通过注入企业微信会话存档服务的实例,调用相应的方法来实现会话存档的功能。 需要注意的是,引入企业微信会话存档SDK前,要确保企业微信账号已通过认证并开通了相应的权限。 以上是使用Spring Boot引入企业微信会话存档SDK的基本步骤,根据具体的需求,可能还需要对业务逻辑进行进一步的处理和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值