写在前面
由于公司业务需求,需要开发企业微信的会话存档,从开始阅读文档到获取解密数据,中间趟过了不少坑,在此记录一下。
一、准备工作
-
登录企业微信后台,获取企业配置参数,企业微信后台登录地址:企业微信登录地址
-
生成一对公私钥,保存好密钥对生成地址
-
获取企业编码
-
设置企业后台,管理工具->会话内容存档,设置可信IP地址,获取会话存档Secret,设置消息加密公钥(将第二步生成的公钥填进去),需要带上BEGIN以及END
-
下载SDK,windows下为.dll文件,linux系统下为libWeWorkFinanceSdk_Java.so,使用SDK需要将
Finance.java放入到项目的指定目录下,必须放下com.tencent.wework下 -
Finance.java修改静态方法,由于我们部署在linux服务器中,所以加载.so文件,其它文章有说配置全局变量或者啥环境,觉得太麻烦,我这里配置的是文件的绝对路径,将libWeWorkFinanceSdk_Java.so上传到服务器上指定位置
-
项目中添加需要使用的依赖,注意!!!需要排除掉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>
二、使用步骤
- 企业微信后台设置会话回调事件,可以自己生成或者随机获取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;
}
}
- 点击保存的时候会对填写的接口发起请求,验证成功才能保存成功
- 当在会话存档的用户接收或者发送消息时,会对配置的接口发起一次请求(此处请求为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;
}
解密方法文章来源:https://www.toymoban.com/news/detail-594879.html
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;
}
说明
- 拉取的消息数据,msg_id唯一,可以通过这个字段,对消息进行去重
- 在接收到回调时,如何去拉取最新的数据呢?第一次拉取消息的seq是0,拉取消息的返回数据中有每一条消息对应的seq,可以将最大的seq更新、保存下来,在接收到数据回调之后,拉取数据时将最新的seq传入接口
过程中出现的问题以及解决办法
- 找不到SDK,需要确定填写的路径上是否有对应版本的jar包
- 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包
- linux下报错:java.security.InvalidKeyException: unknown key type passed to RSA 解决办法:确认一下解密用的私钥,不要去除 BEGIN 、END以及换行符,保留原汁原味
结语
目前遇到的问题以及解决办法都在上面,当前是可以拉取并解密数据,如果你们还有其它问题,欢迎到评论区留言文章来源地址https://www.toymoban.com/news/detail-594879.html
到了这里,关于企业微信会话存档详细搭建以及问题处理的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!