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
*
* 接收到该请求时,企业应 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;
}
可能出现问题:ip不可信
解决方案:请删掉配置的ip,不要设置ip! 不要设置ip! 不要设置ip!
1)生成密钥对(RSA,2048,PKCS#1),保存好公钥和私钥,后面会用到
最简单的方法:http://web.chacuo.net/netrsakeypair
2)将公钥填写到企业微信后台
保存公钥后可以查看到【公钥版本 1】,【管理凭证密钥 secret】 这个后面会用到
企业微信官方文档:获取会话内容 - 接口文档 - 企业微信开发者中心s
企业微信官方文档,真的烂! 起码对我很不友好(可能我理解能力太差了吧,好多地方有问题)
可能会出现的错误信息:「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
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");
}
}
官方提供了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);
}
}
/**
* 拉会话消息
*/
@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字F32v5bfw3NzzwVHU\n" +
"-----END RSA PRIVATE KEY-----";