C#获取企业微信《会话内容存档》


因为公司某些原因需要使用企业微信的会话内容存档内容,看微信的文档踩了一些坑,现在将项目代码记录下来,以备各位码农同行查阅。

项目使用 .NET8.0架构,节本结构如下图:

项目中的Lib是下载的微信SDK,项目地址为:https://wwcdn.weixin.qq.com/node/wework/images/sdk_win_v1.1.zip

1:建立项目Mian方法,没什么好说的:

` private static void Main(string[] args)
{

  var getchat = new GetChatDataService();
  getchat.GetChatDataList();

}`

2:GetChatDataService 获取消息类:
`
public class GetChatDataService
{

    private long InitSDK()
    {
        long sdk = Finance.NewSdk();

        // 这里填写 企业微信 corpid,secret
        var corpid = "wwexxxxxx";//企业ID
        var secret = "xxxxxxxxxx";//企业secret
        Finance.Init(sdk, corpid, secret);

        return sdk;
    }

    public void GetChatDataList()
    {
        try
        {
            var sdk = InitSDK();
            int seq = 1;
            int limit = 1000;// 每次最多请求1000 条
            int timeOut = 500;
            long slice = Finance.NewSlice();
            var ret = Finance.GetChatData(sdk, seq, limit, "", "", timeOut, slice);//获取会话记录数据
            CheckResultInt(ret, nameof(GetChatDataList));
            var resResultStr = this.GetContentFromSlice(slice);  //获取返回文本
            var resData = CheckAndGetResultText(resResultStr, "chatdata", nameof(GetChatDataList));
            #region
            JArray jArrayData = JArray.Parse(resData);
            foreach (var item in jArrayData)
            {
                var chatData = this.DecryptChatData(item["encrypt_random_key"]?.ToString(), item["encrypt_chat_msg"]?.ToString());

if (chat.msgtype == "image")//判断消息类型是否为image
{
GetMsgImage(chat);//保存图片
}
}
#endregion
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
}

    #region  解密 chatdata
    /// <summary>
    /// encrypt_random_key内容解密说明:
    /// encrypt_random_key是使用企业在管理端填写的公钥(使用模值为2048bit的秘钥),采用RSA加密算法进行加密处理后base64 encode的内容,加密内容为企业微信产生。RSA使用PKCS1。
    ///企业通过GetChatData获取到会话数据后:
    ///a) 需首先对每条消息的encrypt_random_key内容进行base64 decode, 得到字符串str1.
    ///b) 使用publickey_ver指定版本的私钥,使用RSA PKCS1算法对str1进行解密,得到解密内容str2.
    ///c) 得到str2与对应消息的encrypt_chat_msg,调用下方描述的DecryptData接口,即可获得消息明文。
    ///  解密 chatdata
    /// </summary>
    /// <param name="encrypt_random_key"></param>
    /// <param name="encrypt_chat_msg"></param>
    /// <returns></returns>
    public string DecryptChatData(string encrypt_random_key, string encrypt_chat_msg)
    {
        try
        {

            #region privatekey
            var privatekey = @"xxxxxxx";//RSA私钥
            #endregion


            if (string.IsNullOrWhiteSpace(privatekey))
                throw new Exception("privatekey 私钥为空!");

            var sliceMsg = Finance.NewSlice();
            var random_key = RSAHelper.RSADecrypt(encrypt_random_key, "utf-8", privatekey) ;
            var ret = Finance.DecryptData(random_key, encrypt_chat_msg, sliceMsg);

            //得到str2与对应消息的encrypt_chat_msg,调用下方描述的DecryptData接口,即可获得消息明文
            CheckResultInt(ret, nameof(DecryptChatData));

            //获取返回文本
            var resResultStr = this.GetContentFromSlice(sliceMsg);

            return resResultStr;
        }
        catch (Exception ex)
        {
            throw new Exception(ex.Message);
        }
    }
    #endregion

    #region 获取文本
    /// <summary>
    /// 获取文本
    /// </summary>
    /// <param name="slice"></param>
    /// <returns></returns>
    private string GetContentFromSlice(long slice)
    {
        int len = Finance.GetSliceLen(slice);

        byte[] vbyte = new byte[len];

        var intPtr = Finance.GetContentFromSlice(slice);

        System.Runtime.InteropServices.Marshal.Copy(intPtr, vbyte, 0, vbyte.Length);

        return Encoding.UTF8.GetString(vbyte);
    }
    #endregion

    #region check
    /// <summary>
    /// 验证 sdk 返回的 int信息
    /// </summary>
    private void CheckResultInt(long ret, string methodName = "")
    {
        if (ret == 0) return;

        throw new Exception($"【{methodName}】 验证失败,返回为:{ret}");

    }

    /// <summary>
    ///  验证 sdk 返回的数据信息
    /// </summary>
    /// <param name="result">SDK返回的结果集</param>
    /// <param name="dataColumn">data 列名</param>
    /// <param name="methodName">请求的方法名</param>
    /// <returns></returns>
    private string CheckAndGetResultText(string result, string dataColumn, string methodName = "")
    {
        if (string.IsNullOrWhiteSpace(result))
            throw new Exception($"CheckResultText 【{methodName}】 验证失败,返回结果为空");

        try
        {
            JToken jToken = JToken.Parse(result);

            if (jToken["errcode"].ToString() == "0")
            {
                return jToken[dataColumn].ToString();
            }

            throw new Exception($"【{methodName}】数据返回失败,errmsg:{jToken["errmsg"]}");

        }
        catch (Exception ex)
        {
            throw new Exception($"【{methodName}】解析失败,错误:{ex.Message}");
        }
    }
    #endregion
}

 3:RSAHelper RSA加密解密类:
public static class RSAHelper
{
private static string DEFAULT_CHARSET = "UTF-8";

   /// <summary>
   /// 加密
   /// </summary>
   /// <param name="content"></param>
   /// <param name="charset"></param>
   /// <param name="publicKeyPem"></param>
   /// <returns></returns>
   /// <exception cref="Exception"></exception>
   public static string RSAEncrypt(string content, string charset, string publicKeyPem)
   {
       try
       {
           //假设私钥长度为1024, 1024/8-11=117。
           //如果明文的长度小于117,直接全加密,然后转base64。(data.Length <= maxBlockSize)
           //如果明文长度大于117,则每117分一段加密,写入到另一个Stream中,最后转base64。while (blockSize > 0)                 

           //转为纯字符串,不带格式
           publicKeyPem = publicKeyPem.Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();

           RSA rsa = RSA.Create();
           rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicKeyPem), out _);

           if (string.IsNullOrEmpty(charset))
           {
               charset = DEFAULT_CHARSET;
           }
           byte[] data = Encoding.GetEncoding(charset).GetBytes(content);
           int maxBlockSize = rsa.KeySize / 8 - 11; //加密块最大长度限制
           if (data.Length <= maxBlockSize)
           {
               byte[] cipherbytes = rsa.Encrypt(data, RSAEncryptionPadding.Pkcs1);
               return Convert.ToBase64String(cipherbytes);
           }
           MemoryStream plaiStream = new MemoryStream(data);
           MemoryStream crypStream = new MemoryStream();
           byte[] buffer = new byte[maxBlockSize];
           int blockSize = plaiStream.Read(buffer, 0, maxBlockSize);
           while (blockSize > 0)
           {
               byte[] toEncrypt = new byte[blockSize];
               Array.Copy(buffer, 0, toEncrypt, 0, blockSize);
               byte[] cryptograph = rsa.Encrypt(toEncrypt, RSAEncryptionPadding.Pkcs1);
               crypStream.Write(cryptograph, 0, cryptograph.Length);
               blockSize = plaiStream.Read(buffer, 0, maxBlockSize);
           }

           return Convert.ToBase64String(crypStream.ToArray(), Base64FormattingOptions.None);
       }
       catch (Exception ex)
       {
           throw new Exception("EncryptContent = " + content + ",charset = " + charset, ex);
       }
   }

   /// <summary>
   /// 解密
   /// </summary>
   /// <param name="content"></param>
   /// <param name="charset"></param>
   /// <param name="privateKeyPem"></param>
   /// <param name="keyFormat"></param>
   /// <returns></returns>
   /// <exception cref="Exception"></exception>
   public static string RSADecrypt(string content, string charset, string privateKeyPem, string keyFormat = "PKCS8")
   {
       try
       {
           //假设私钥长度为1024, 1024/8 =128。
           //如果明文的长度小于 128,直接全解密。(data.Length <= maxBlockSize)
           //如果明文长度大于 128,则每 128 分一段解密,写入到另一个Stream中,最后 GetString。while (blockSize > 0)                                 

           //转为纯字符串,不带格式
           privateKeyPem = privateKeyPem.Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();
           privateKeyPem = privateKeyPem.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();


           RSA rsaCsp = RSA.Create();
           if (keyFormat == "PKCS8")
               rsaCsp.ImportPkcs8PrivateKey(Convert.FromBase64String(privateKeyPem), out _);
           else if (keyFormat == "PKCS1")
               rsaCsp.ImportRSAPrivateKey(Convert.FromBase64String(privateKeyPem), out _);
           else
               throw new Exception("只支持PKCS8,PKCS1");

           if (string.IsNullOrEmpty(charset))
           {
               charset = DEFAULT_CHARSET;
           }
           byte[] data = Convert.FromBase64String(content);
           int maxBlockSize = rsaCsp.KeySize / 8; //解密块最大长度限制
           if (data.Length <= maxBlockSize)
           {
               byte[] cipherbytes = rsaCsp.Decrypt(data, RSAEncryptionPadding.Pkcs1);
               return Encoding.GetEncoding(charset).GetString(cipherbytes);
           }
           MemoryStream crypStream = new MemoryStream(data);
           MemoryStream plaiStream = new MemoryStream();
           byte[] buffer = new byte[maxBlockSize];
           int blockSize = crypStream.Read(buffer, 0, maxBlockSize);
           while (blockSize > 0)
           {
               byte[] toDecrypt = new byte[blockSize];
               Array.Copy(buffer, 0, toDecrypt, 0, blockSize);
               byte[] cryptograph = rsaCsp.Decrypt(toDecrypt, RSAEncryptionPadding.Pkcs1);
               plaiStream.Write(cryptograph, 0, cryptograph.Length);
               blockSize = crypStream.Read(buffer, 0, maxBlockSize);
           }

           return Encoding.GetEncoding(charset).GetString(plaiStream.ToArray());
       }
       catch (Exception ex)
       {
           throw new Exception("DecryptContent = " + content + ",charset = " + charset, ex);
       }
   }

}
`
4:企业微信调用类 Finance:

`
public static class Finance
{

    private const string DllName = "Lib\\WeWorkFinanceSdk.dll";

    [DllImport(DllName)]
    public extern static long NewSdk();

    /**
     * 初始化函数
     * Return值=0表示该API调用成功
     * 
     * @param [in]  sdk			NewSdk返回的sdk指针
     * @param [in]  corpid      调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看
     * @param [in]  secret		聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看
     *						
     *
     * @return 返回是否初始化成功
     *      0   - 成功
     *      !=0 - 失败
     */
    [DllImport(DllName)]
    public extern 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 - 失败	
     */
    [DllImport(DllName)] public extern 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 - 失败
     */
    [DllImport(DllName)] public extern static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData);

    /**
     * @brief 解析密文
     * @param [in]  encrypt_key, getchatdata返回的encrypt_key
     * @param [in]  encrypt_msg, getchatdata返回的content
     * @param [out] msg, 解密的消息明文
     * @return 返回是否调用成功
     *      0   - 成功
     *      !=0 - 失败
     */
    [DllImport(DllName)]
    public extern static int DecryptData(String encrypt_key, String encrypt_msg, long msg);


    [DllImport(DllName)] public extern static void DestroySdk(long sdk);
    [DllImport(DllName)] public extern static long NewSlice();
    /**
     * @brief 释放slice,和NewSlice成对使用
     * @return 
     */
    [DllImport(DllName)] public extern static void FreeSlice(long slice);

    /**
     * @brief 获取slice内容
     * @return 内容
     */
    [DllImport(DllName)]
    // IntPtr 换成 String 就需要将下面这个注释启用
    //[return: MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(UTF8Marshaler))]
    public extern static IntPtr GetContentFromSlice(long slice);

    /**
     * @brief 获取slice内容长度
     * @return 内容
     */
    [DllImport(DllName)] public extern static int GetSliceLen(long slice);
    [DllImport(DllName)] public extern static long NewMediaData();
    [DllImport(DllName)] public extern static void FreeMediaData(long mediaData);

    /**
     * @brief 获取mediadata outindex
     * @return outindex
     */
    [DllImport(DllName)] public extern static String GetOutIndexBuf(long mediaData);
    /**
     * @brief 获取mediadata data数据
     * @return data
     */
    [DllImport(DllName)] public extern static IntPtr GetData(long mediaData);
    [DllImport(DllName)] public extern static int GetIndexLen(long mediaData);
    [DllImport(DllName)] public extern static int GetDataLen(long mediaData);

    /**
     * @brief 判断mediadata是否结束
     * @return 1完成、0未完成
     */
    [DllImport(DllName)] public extern static int IsMediaDataFinish(long mediaData);
}

获取图片(image)类型:
public void GetMsgImage(ChatMessage msg)
{
try
{
var sdk = InitSDK();
var ret = 0;
if (ret != 0)
{
//sdk需要主动释放
Finance.DestroySdk(sdk);
return;
}
//拉取媒体文件
string index = "";
int isfinish = 0;
var timeout = 0L;
string filepath = @"d:\chatMadia";
var Suffix = GetMadiaSuffix(msg.msgtype);//自己写的判断文件名后缀的方法
var filename = filepath + "\" + msg.msgid + Suffix;//保存路径
var listbyte = new List();
while (isfinish == 0)
{
var mediaData = Finance.NewMediaData();
ret = Finance.GetMediaData(sdk, index, msg.image.sdkfileid, "", "", timeout, mediaData);
if (ret != 0)
{
return;
}
if (!Directory.Exists(filepath))
{
Directory.CreateDirectory(filepath);
}
var len = Finance.GetDataLen(mediaData);
byte[] bytes = new byte[Finance.GetDataLen(mediaData)];
Marshal.Copy(Finance.GetData(mediaData), bytes, 0, Finance.GetDataLen(mediaData));//装载第一次分片数据
listbyte.AddRange(bytes);
if (Finance.IsMediaDataFinish(mediaData) == 1)
{
Finance.FreeMediaData(mediaData);//如果已经全部接收完毕,释放资源
break;
}
else
{
index = Finance.GetOutIndexBuf(mediaData);
}
}
Finance.DestroySdk(sdk);
byte[] byt = listbyte.ToArray();//合并几次收到的byte
FileStream file = new FileStream(filename, FileMode.Create, FileAccess.ReadWrite);
file.Write(byt, 0, byt.Length);
file.Close();
}
catch (Exception ex)
{

  }

}
`
项目代码全部上完,不出意外的将企业ID与secret替换一下就可以取回消息记录了。

下面讲一下一些坑:

1:

encrypt_random_key内容解密说明:
encrypt_random_key是使用企业在管理端填写的公钥(使用模值为2048bit的秘钥),采用RSA加密算法进行加密处理后base64 encode的内容,加密内容为企业微信产生。RSA使用PKCS1。
企业通过GetChatData获取到会话数据后:
a) 需首先对每条消息的encrypt_random_key内容进行base64 decode,得到字符串str1.
b) 使用publickey_ver指定版本的私钥,使用RSA PKCS1算法对str1进行解密,得到解密内容str2.
c) 得到str2与对应消息的encrypt_chat_msg,调用下方描述的DecryptData接口,即可获得消息明文。

====

腾讯的文档这里提到的解密过程,a=>b=>c 三个步骤,a步骤是不需要的,只需要进行b=>c步骤就可以了,腾讯的客服回答是返回的encrypt_random_key是已经解码的数据,不需要用户再实现解码;

b步骤中有一个大坑:《使用publickey_ver指定版本的私钥,使用RSA PKCS1算法对str1进行解密》,开始用一直是使用RSA PKCS1算法解码,一直提示错误,后面心一横,使用RSA PKCS8解码,见了个鬼,OK了。所以b步骤这里使用的是RSA PKCS8算法解码。

之后直接调用腾讯SDK解密就可以得到原消息内容了。

====

在项目加载的时候腾讯的SDK需要放在项目的:xxx\RSA_Demo\RSA_Demo\bin\Debug\net8.0\Lib  这个文件夹下。

工5分dll文件。

====

关于RSA私钥公钥的生成,我使用的是openssl生成的。必须使用2048模生成,要不然通不过腾讯的验证。

私钥PRIVATE KEY,我使用xxxx替换了一些原本的内容:

-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDCIJXrZBUwBTc9
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ex3bNmDjIDEJdzztjZZBLO3Gx2whC9pq+cNLfYqWuBtiSzS9n1u1S0xDuLPGRHx8
qofhmAMPcy33gEQQeCUJuV5OG9jPTSxQZDVelIQQLxayZxwLeZgOWH7PVNdSLY30
Zb0TtoyW59orXA4krmLaZ1G1ZQKBgQDdsJjhVsHHGZ2JOLBte07p+v4VVyDPDPxi
SPhUV4Ak2XTNG5l1AEXHb0oGwltPLwxESiisioV/xHRS7ynlB4i+gxuj++ANwGZP
s8RhP6Yvem35yk6FiNAxfduS/pgAJkHMc9FlPJvLbEHxrfm5KeKfokwhfhAJdY8m
dF9nBTAkYQKBgEVT0pdWBrZPl0VeXap7vOQKkTQsxH2U7rbYztshNff/vKULsWTc
EVXLYYzzKyTe9VSfcBFFVDStboumqokzhAp0pJ2mlVSxylr9jRbQySWG3ypdR5Ml
KowxihYnOfG6iaXS2P+2xqEAMAcuJ3Sp8iuijSAuW/6E3aric5fv1AkxAoGAGlWF
A5eTszv2u7sxMgAo0qCPGCfebNoFDQPQA+zU+wud1VOG+iALKfKtX3os8I4NLfuF
M2HNE+1ZSBTC7ELl2oOmf+dGqTuGq8cV99tguVkYwUhn5XLoEEj8EU0O702cGVZU
tGrrstFsT/IzrOwt0HquAniAHS+Kzq2aO5mhK2ECgYAJwN3ZEjPkfGK5MXA/Xzdx
7if21jCEuicAUyWFS3bod8jLoyBHJHHyTunc/G8U1lrNBXB2EVQBMTDEVkXkAkCi
TM2b79MZY6Aj+mqDf8xmJJOhtrXOe+lbGOnRQZ5wS8YuCIpgS39xpcxcnsSfAHo8
ISAKzVwaoDs1fh6qgSC/9w==
-----END PRIVATE KEY-----

公钥 PUBLIC KEY:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwiCV62QVMAU3PetBOfZ+
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
RQIDAQAB
-----END PUBLIC KEY-----

以上就是取回企业微信消息记录的一些总结,希望能帮到广大踩坑的同行。

posted @ 2024-07-19 14:30  XXIIVV  阅读(266)  评论(0)    收藏  举报