diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventSerializationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventSerializationExtensions.cs
index 6f637353..df8b867f 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventSerializationExtensions.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventSerializationExtensions.cs
@@ -9,7 +9,7 @@ using System.Xml.Serialization;
namespace SKIT.FlurlHttpClient.Wechat.Api
{
///
- /// 为 提供回调通知事件序列化的扩展方法。
+ /// 为 提供回调通知事件序列化相关的扩展方法。
///
public static class WechatApiClientEventSerializationExtensions
{
diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventVerificationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventVerificationExtensions.cs
index c2403cdf..7a8c50ae 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventVerificationExtensions.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventVerificationExtensions.cs
@@ -8,7 +8,7 @@ using System.Text.RegularExpressions;
namespace SKIT.FlurlHttpClient.Wechat.Api
{
///
- /// 为 提供回调通知事件验证的扩展方法。
+ /// 为 提供回调通知事件验证相关的扩展方法。
///
public static class WechatApiClientEventVerificationExtensions
{
diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Exceptions/WechatWorkEventSerializationException.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Exceptions/WechatWorkEventSerializationException.cs
new file mode 100644
index 00000000..e279bd07
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Exceptions/WechatWorkEventSerializationException.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace SKIT.FlurlHttpClient.Wechat.Work.Exceptions
+{
+ public class WechatWorkEventSerializationException : WechatWorkException
+ {
+ ///
+ internal WechatWorkEventSerializationException()
+ {
+ }
+
+ ///
+ internal WechatWorkEventSerializationException(string message)
+ : base(message)
+ {
+ }
+
+ ///
+ internal WechatWorkEventSerializationException(string message, Exception innerException)
+ : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs
deleted file mode 100644
index d13b17a8..00000000
--- a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs
+++ /dev/null
@@ -1,44 +0,0 @@
-using System;
-using System.IO;
-using System.Xml.Serialization;
-
-namespace SKIT.FlurlHttpClient.Wechat.Work
-{
- ///
- /// 为 提供回调通知事件的扩展方法。
- ///
- public static class WechatWorkClientEventExtensions
- {
- ///
- /// 从 JSON 反序列化得到 对象。
- ///
- ///
- ///
- ///
- public static TEvent DeserializeEventFromJson(this WechatWorkClient client, string callbackJson)
- where TEvent : WechatWorkEvent, WechatWorkEvent.Types.IJsonSerializable, new()
- {
- if (client == null) throw new ArgumentNullException(nameof(client));
- if (string.IsNullOrEmpty(callbackJson)) throw new ArgumentNullException(callbackJson);
-
- return client.JsonSerializer.Deserialize(callbackJson);
- }
-
- ///
- /// 从 XML 反序列化得到 对象。
- ///
- ///
- ///
- ///
- public static TEvent DeserializeEventFromXml(this WechatWorkClient client, string callbackXml)
- where TEvent : WechatWorkEvent, WechatWorkEvent.Types.IXmlSerializable, new()
- {
- if (client == null) throw new ArgumentNullException(nameof(client));
- if (string.IsNullOrEmpty(callbackXml)) throw new ArgumentNullException(callbackXml);
-
- using StringReader reader = new StringReader(callbackXml);
- XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml"));
- return (TEvent)xmlSerializer.Deserialize(reader);
- }
- }
-}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventSerializationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventSerializationExtensions.cs
new file mode 100644
index 00000000..bd85e72a
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventSerializationExtensions.cs
@@ -0,0 +1,255 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Xml.Serialization;
+
+namespace SKIT.FlurlHttpClient.Wechat.Work
+{
+ ///
+ /// 为 提供回调通知事件序列化相关的扩展方法。
+ ///
+ public static class WechatWorkClientEventSerializationExtensions
+ {
+ private class EncryptedWechatWorkEvent
+ {
+ [Newtonsoft.Json.JsonProperty("Encrypt")]
+ [System.Text.Json.Serialization.JsonPropertyName("Encrypt")]
+ public string EncryptedData { get; set; } = default!;
+
+ [Newtonsoft.Json.JsonProperty("TimeStamp")]
+ [System.Text.Json.Serialization.JsonPropertyName("TimeStamp")]
+ [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Converters.NumberTypedStringConverter))]
+ public string Timestamp { get; set; } = default!;
+
+ [Newtonsoft.Json.JsonProperty("Nonce")]
+ [System.Text.Json.Serialization.JsonPropertyName("Nonce")]
+ public string Nonce { get; set; } = default!;
+
+ [Newtonsoft.Json.JsonProperty("MsgSignature")]
+ [System.Text.Json.Serialization.JsonPropertyName("MsgSignature")]
+ public string Signature { get; set; } = default!;
+ }
+
+ private static TEvent InnerDeserializeEventFromJson(this WechatWorkClient client, string callbackJson)
+ where TEvent : WechatWorkEvent
+ {
+ if (client == null) throw new ArgumentNullException(nameof(client));
+ if (string.IsNullOrEmpty(callbackJson)) throw new ArgumentNullException(callbackJson);
+
+ try
+ {
+ if (string.IsNullOrEmpty(client.Credentials.PushEncodingAESKey))
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no encoding AES key.");
+
+ var encryptedEvent = client.JsonSerializer.Deserialize(callbackJson);
+ if (string.IsNullOrEmpty(encryptedEvent.EncryptedData))
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because of empty encrypted data.");
+
+ callbackJson = Utilities.WxBizMsgCryptor.AESDecrypt(
+ cipherText: encryptedEvent.EncryptedData,
+ encodingAESKey: client.Credentials.PushEncodingAESKey!,
+ out _
+ );
+
+ return client.JsonSerializer.Deserialize(callbackJson);
+ }
+ catch (WechatWorkException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatWorkEventSerializationException("Deserialize event failed. Please see the `InnerException` for more details.", ex);
+ }
+ }
+
+ private static TEvent InnerDeserializeEventFromXml(this WechatWorkClient client, string callbackXml)
+ where TEvent : WechatWorkEvent
+ {
+ if (client == null) throw new ArgumentNullException(nameof(client));
+ if (string.IsNullOrEmpty(callbackXml)) throw new ArgumentNullException(callbackXml);
+
+ try
+ {
+ if (!Utilities.WxBizMsgCryptor.TryParseXml(callbackXml, out callbackXml!))
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because of empty encrypted data.");
+
+ using var reader = new StringReader(callbackXml);
+
+ XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml"));
+ return (TEvent)xmlSerializer.Deserialize(reader)!;
+ }
+ catch (WechatWorkException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatWorkEventSerializationException("Deserialize event failed. Please see the `InnerException` for more details.", ex);
+ }
+ }
+
+ ///
+ /// 从 JSON 反序列化得到 对象。
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static TEvent DeserializeEventFromJson(this WechatWorkClient client, string callbackJson)
+ where TEvent : WechatWorkEvent, WechatWorkEvent.Types.IJsonSerializable, new()
+ {
+ return InnerDeserializeEventFromJson(client, callbackJson);
+ }
+
+ ///
+ /// 从 JSON 反序列化得到 对象。
+ ///
+ ///
+ ///
+ ///
+ public static WechatWorkEvent DeserializeEventFromJson(this WechatWorkClient client, string callbackJson)
+ {
+ return InnerDeserializeEventFromJson(client, callbackJson);
+ }
+
+ ///
+ /// 从 XML 反序列化得到 对象。
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static TEvent DeserializeEventFromXml(this WechatWorkClient client, string callbackXml)
+ where TEvent : WechatWorkEvent, WechatWorkEvent.Types.IXmlSerializable, new()
+ {
+ return InnerDeserializeEventFromXml(client, callbackXml);
+ }
+
+ ///
+ /// 从 XML 反序列化得到 对象。
+ ///
+ ///
+ ///
+ ///
+ public static WechatWorkEvent DeserializeEventFromXml(this WechatWorkClient client, string callbackXml)
+ {
+ return InnerDeserializeEventFromXml(client, callbackXml);
+ }
+
+ ///
+ /// 将 对象序列化成 JSON。
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string SerializeEventToJson(this WechatWorkClient client, TEvent callbackModel)
+ where TEvent : WechatWorkEvent, WechatWorkEvent.Types.IJsonSerializable, new()
+ {
+ string json;
+
+ try
+ {
+ json = client.JsonSerializer.Serialize(callbackModel);
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatWorkEventSerializationException("Serialize event failed. Please see the `InnerException` for more details.", ex);
+ }
+
+ if (string.IsNullOrEmpty(client.Credentials.PushEncodingAESKey))
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no encoding AES key.");
+ if (string.IsNullOrEmpty(client.Credentials.PushToken))
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no token.");
+
+ try
+ {
+ string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString();
+ string nonce = DateTimeOffset.Now.Ticks.ToString("x");
+ string cipher = Utilities.WxBizMsgCryptor.AESEncrypt(
+ plainText: json,
+ encodingAESKey: client.Credentials.PushEncodingAESKey!,
+ corpOrSuiteId: string.IsNullOrEmpty(client.Credentials.SuiteId) ? client.Credentials.CorpId : client.Credentials.SuiteId!
+ );
+ string sign = Utilities.WxBizMsgCryptor.GenerateSignature(
+ sToken: client.Credentials.PushToken!,
+ sTimestamp: timestamp,
+ sNonce: nonce,
+ sMsgEncrypt: cipher
+ );
+
+ json = client.JsonSerializer.Serialize(new EncryptedWechatWorkEvent()
+ {
+ EncryptedData = cipher,
+ Timestamp = timestamp,
+ Nonce = nonce,
+ Signature = sign
+ });
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed. Please see the `InnerException` for more details.", ex);
+ }
+
+ return json;
+ }
+
+ ///
+ /// 将 对象序列化成 XML。
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static string SerializeEventToXml(this WechatWorkClient client, TEvent callbackModel)
+ where TEvent : WechatWorkEvent, WechatWorkEvent.Types.IXmlSerializable, new()
+ {
+ string xml;
+
+ try
+ {
+ using var stream = new MemoryStream();
+ using var writer = new System.Xml.XmlTextWriter(stream, Encoding.UTF8);
+ writer.Formatting = System.Xml.Formatting.None;
+
+ XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml"));
+ XmlSerializerNamespaces xmlNamespace = new XmlSerializerNamespaces();
+ xmlNamespace.Add(string.Empty, string.Empty);
+ xmlSerializer.Serialize(writer, callbackModel, xmlNamespace);
+ writer.Flush();
+ xml = Encoding.UTF8.GetString(stream.ToArray());
+ xml = Regex.Replace(xml, "\\s+<\\w+ (xsi|d2p1):nil=\"true\"[^>]*/>", string.Empty, RegexOptions.IgnoreCase);
+ xml = Regex.Replace(xml, "<\\?xml[^>]*\\?>", string.Empty, RegexOptions.IgnoreCase);
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatWorkEventSerializationException("Serialize event failed. Please see the `InnerException` for more details.", ex);
+ }
+
+
+ if (string.IsNullOrEmpty(client.Credentials.PushEncodingAESKey))
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no encoding AES key.");
+ if (string.IsNullOrEmpty(client.Credentials.PushToken))
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because there is no token.");
+
+ try
+ {
+ string cipher = Utilities.WxBizMsgCryptor.AESEncrypt(
+ plainText: xml,
+ encodingAESKey: client.Credentials.PushEncodingAESKey!,
+ corpOrSuiteId: string.IsNullOrEmpty(client.Credentials.SuiteId) ? client.Credentials.CorpId : client.Credentials.SuiteId!
+ );
+
+ xml = Utilities.WxBizMsgCryptor.WrapXml(sToken: client.Credentials.PushToken!, sMsgEncrypt: cipher);
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed. Please see the `InnerException` for more details.", ex);
+ }
+
+ return xml;
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Settings/Credentials.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Settings/Credentials.cs
index af1f3cde..8da7db4e 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.Work/Settings/Credentials.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Settings/Credentials.cs
@@ -38,6 +38,16 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Settings
///
public string? SuiteSecret { get; }
+ ///
+ /// 初始化客户端时 的副本。
+ ///
+ public string? PushEncodingAESKey { get; }
+
+ ///
+ /// 初始化客户端时 的副本。
+ ///
+ public string? PushToken { get; }
+
internal Credentials(WechatWorkClientOptions options)
{
if (options == null) throw new ArgumentNullException(nameof(options));
@@ -48,6 +58,8 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Settings
ProviderSecret = options.ProviderSecret;
SuiteId = options.SuiteId;
SuiteSecret = options.SuiteSecret;
+ PushEncodingAESKey = options.PushEncodingAESKey;
+ PushToken = options.PushToken;
}
}
}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/WxBizMsgCryptor.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/WxBizMsgCryptor.cs
index b20002e8..19a8019a 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/WxBizMsgCryptor.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/WxBizMsgCryptor.cs
@@ -138,9 +138,9 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
///
/// 企业微信推送来的加密文本内容(即 `Encrypt` 字段的值)。
/// 企业微信后台设置的 EncodingAESKey。
- /// 企业微信 CorpId。
+ /// 企业微信 CorpId 或第三方应用的 SuiteId。
/// 解密后的文本内容。
- public static string AESDecrypt(string cipherText, string encodingAESKey, out string corpId)
+ public static string AESDecrypt(string cipherText, string encodingAESKey, out string corpOrSuiteId)
{
if (cipherText == null) throw new ArgumentNullException(nameof(cipherText));
if (encodingAESKey == null) throw new ArgumentNullException(nameof(encodingAESKey));
@@ -159,7 +159,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
Array.Copy(btmpMsg, 20, bMsg, 0, len);
Array.Copy(btmpMsg, 20 + len, bCorpId, 0, btmpMsg.Length - 20 - len);
- corpId = Encoding.UTF8.GetString(bCorpId);
+ corpOrSuiteId = Encoding.UTF8.GetString(bCorpId);
return Encoding.UTF8.GetString(bMsg);
}
@@ -168,13 +168,13 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
///
/// 返回给企业微信的原始文本内容。
/// 企业微信后台设置的 EncodingAESKey。
- /// 企业微信 CorpId。
+ /// 企业微信 CorpId 或第三方应用的 SuiteId。
/// 加密后的文本内容。
- public static string AESEncrypt(string plainText, string encodingAESKey, string corpId)
+ public static string AESEncrypt(string plainText, string encodingAESKey, string corpOrSuiteId)
{
if (plainText == null) throw new ArgumentNullException(nameof(plainText));
if (encodingAESKey == null) throw new ArgumentNullException(nameof(encodingAESKey));
- if (corpId == null) throw new ArgumentNullException(nameof(corpId));
+ if (corpOrSuiteId == null) throw new ArgumentNullException(nameof(corpOrSuiteId));
byte[] keyBytes = Convert.FromBase64String(encodingAESKey + "=");
byte[] ivBytes = new byte[16];
@@ -182,7 +182,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities
string randCode = CreateRandCode(16);
byte[] bRand = Encoding.UTF8.GetBytes(randCode);
- byte[] bCorpId = Encoding.UTF8.GetBytes(corpId);
+ byte[] bCorpId = Encoding.UTF8.GetBytes(corpOrSuiteId);
byte[] bMsgTmp = Encoding.UTF8.GetBytes(plainText);
byte[] bMsgLen = BitConverter.GetBytes(IPAddress.HostToNetworkOrder(bMsgTmp.Length));
byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bCorpId.Length + bMsgTmp.Length];
diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkClientOptions.cs
index a1ccda63..80c75bb6 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkClientOptions.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.Work/WechatWorkClientOptions.cs
@@ -48,5 +48,15 @@ namespace SKIT.FlurlHttpClient.Wechat.Work
/// 获取或设置企业微信第三方应用的 SuiteSecret。仅限第三方应用开发时使用。
///
public string? SuiteSecret { get; set; }
+
+ ///
+ /// 获取或设置企业微信服务器推送的 EncodingAESKey。
+ ///
+ public string? PushEncodingAESKey { get; set; }
+
+ ///
+ /// 获取或设置企业微信服务器推送的 Token。
+ ///
+ public string? PushToken { get; set; }
}
}