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

文章目录

  • 写在前面
  • 一、准备工作
  • 二、使用步骤
  • 说明
  • 过程中出现的问题以及解决办法
  • 结语


写在前面

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


一、准备工作

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

  2. 生成一对公私钥,保存好密钥对生成地址企业微信会话存档详细搭建以及问题处理_第1张图片

  3. 获取企业编码企业微信会话存档详细搭建以及问题处理_第2张图片

  4. 设置企业后台,管理工具->会话内容存档,设置可信IP地址,获取会话存档Secret,设置消息加密公钥(将第二步生成的公钥填进去),需要带上BEGIN以及END企业微信会话存档详细搭建以及问题处理_第3张图片

  5. 下载SDK,windows下为.dll文件,linux系统下为libWeWorkFinanceSdk_Java.so,使用SDK需要将
    Finance.java放入到项目的指定目录下,必须放下com.tencent.wework下企业微信会话存档详细搭建以及问题处理_第4张图片

  6. Finance.java修改静态方法,由于我们部署在linux服务器中,所以加载.so文件,其它文章有说配置全局变量或者啥环境,觉得太麻烦,我这里配置的是文件的绝对路径,将libWeWorkFinanceSdk_Java.so上传到服务器上指定位置企业微信会话存档详细搭建以及问题处理_第5张图片

  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企业微信会话存档详细搭建以及问题处理_第6张图片
@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编码的字符串).
 * 
    *
  1. 第三方回复加密消息给企业微信
  2. *
  3. 第三方收到企业微信发送的消息,验证消息的安全性,并对消息进行解密。
  4. *
* 说明:异常java.security.InvalidKeyException:illegal Key Size的解决方案 *
    *
  1. 在官方网站下载JCE无限制权限策略文件(JDK7的下载地址: * http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
  2. *
  3. 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
  4. *
  5. 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
  6. *
  7. 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
  8. *
*/ 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; } /** * 将企业微信回复用户的消息加密打包. *
    *
  1. 对要发送的消息进行AES-CBC加密
  2. *
  3. 生成安全签名
  4. *
  5. 将消息密文和安全签名打包成json格式
  6. *
* * @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; } /** * 检验消息的真实性,并且获取解密后的明文. *
    *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. *
  3. 若验证通过,则提取json中的加密消息
  4. *
  5. 对消息进行解密
  6. *
* * @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 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 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以及换行符,保留原汁原味

结语

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

你可能感兴趣的:(企业微信,服务器,windows,java)