From a094f93033ba045e4c337995f28d658724b0ad41 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 2 Aug 2021 18:09:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(work):=20=E5=8F=8D=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E5=BE=AE=E4=BF=A1=E5=9B=9E=E8=B0=83=E9=80=9A=E7=9F=A5?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E6=A8=A1=E5=9E=8B=E6=97=B6=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E8=A7=A3=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...atApiClientEventSerializationExtensions.cs | 2 +- ...hatApiClientEventVerificationExtensions.cs | 2 +- .../WechatWorkEventSerializationException.cs | 24 ++ .../WechatWorkClientEventExtensions.cs | 44 --- ...tWorkClientEventSerializationExtensions.cs | 255 ++++++++++++++++++ .../Settings/Credentials.cs | 12 + .../Utilities/WxBizMsgCryptor.cs | 14 +- .../WechatWorkClientOptions.cs | 10 + 8 files changed, 310 insertions(+), 53 deletions(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Work/Exceptions/WechatWorkEventSerializationException.cs delete mode 100644 src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventSerializationExtensions.cs 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; } } }