diff --git a/CHANGELOG.md b/CHANGELOG.md index b2498c09..7f36ce7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,10 @@ SKIT.FlurlHttpClient.Wechat.Api 更新日志 +- Release 1.10.1 + + - **修复**:修复 XmlSerializer 潜在的内存泄漏问题。([GitHub Issue #11](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat/issues/11)) + - Release 1.10.0 - **新增**:新增第三方平台申请开通直播相关接口。 @@ -44,8 +48,6 @@ - **新增**:随官方更新小程序联盟推客端相关接口模型。 - - **修复**:修复 XmlSerializer 潜在的内存泄漏问题。([GitHub Issue #11](https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat/issues/11)) - - **修复**:修复 AES 解密结果结尾有冗余的空白字符问题。 - Release 1.9.0 diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventExtensions.cs index ffb5b645..490beddb 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Extensions/WechatApiClientEventExtensions.cs @@ -79,10 +79,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Api callbackXml = Utilities.WxBizMsgCryptor.AESDecrypt(cipherText: encryptedXml!, encodingAESKey: client.Credentials.PushEncodingAESKey!, out _); } - using var reader = new StringReader(callbackXml); - - XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml")); - return (TEvent)xmlSerializer.Deserialize(reader)!; + return Utilities.XmlUtility.Deserialize(callbackXml); } catch (WechatApiException) { diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj b/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj index 5a7fb209..38dbc6df 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/SKIT.FlurlHttpClient.Wechat.Api.csproj @@ -12,7 +12,7 @@ MIT https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat Flurl.Http Wechat Weixin MicroMessage MiniProgram MiniGame MiniStore 微信 公众号 服务号 订阅号 小程序 小游戏 小商店 公众平台 开放平台 微信公众号 微信服务号 微信订阅号 微信小程序 微信小游戏 微信小商店 微信公众平台 微信开放平台 - 1.10.0 + 1.10.1 基于 Flurl.Http 的微信 API 客户端,支持公众平台(订阅号、服务号、小程序、小游戏、小商店)、开放平台等平台,支持基础服务、模板消息、订阅消息、客服消息、动态消息、菜单管理、素材管理、留言管理、用户管理、账号管理、数据统计、微信门店、微信小店、智能接口、一物一码、微信发票、微信非税缴费、插件管理、附近的小程序、小程序码、小程序搜索、URL Scheme、URL Link、即时配送、物流助手、直播、生物认证、虚拟支付、开放数据、对局匹配、帧同步、内容安全、安全风控、第三方平台等功能。 Fu Diwei git diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs index 07d0a63f..1d79405d 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/AESUtility.cs @@ -35,6 +35,31 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities } } + /// + /// 基于 CBC 模式加密数据。 + /// + /// AES 密钥字节数组。 + /// 加密使用的初始化向量字节数组。 + /// 待加密数据字节数组。 + /// 加密后的数据字节数组。 + public static byte[] EncryptWithCBC(byte[] keyBytes, byte[] ivBytes, byte[] plainBytes) + { + if (keyBytes == null) throw new ArgumentNullException(nameof(keyBytes)); + if (ivBytes == null) throw new ArgumentNullException(nameof(ivBytes)); + if (plainBytes == null) throw new ArgumentNullException(nameof(plainBytes)); + + using (SymmetricAlgorithm aes = Aes.Create()) + { + aes.Mode = CipherMode.CBC; + aes.Padding = PaddingMode.PKCS7; + aes.Key = keyBytes; + aes.IV = ivBytes; + + using ICryptoTransform transform = aes.CreateEncryptor(); + return transform.TransformFinalBlock(plainBytes, 0, plainBytes.Length); + } + } + /// /// 基于 CBC 模式解密数据。 /// @@ -54,5 +79,25 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities ); return Encoding.UTF8.GetString(plainBytes); } + + /// + /// 基于 CBC 模式加密数据。 + /// + /// 经 Base64 编码后的 AES 密钥。 + /// 经 Base64 编码后的 AES 初始化向量。 + /// 待加密文本。 + /// 经 Base64 编码的加密后的数据。 + public static string EncryptWithCBC(string encodingKey, string encodingIV, string plainText) + { + if (encodingKey == null) throw new ArgumentNullException(nameof(encodingKey)); + if (plainText == null) throw new ArgumentNullException(nameof(plainText)); + + byte[] plainBytes = EncryptWithCBC( + keyBytes: Convert.FromBase64String(encodingKey), + ivBytes: Convert.FromBase64String(encodingIV), + plainBytes: Encoding.UTF8.GetBytes(plainText) + ); + return Convert.ToBase64String(plainBytes); + } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/Internal/XmlUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/Internal/XmlUtility.cs index cd7f4ec2..190a3260 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/Internal/XmlUtility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Api/Utilities/Internal/XmlUtility.cs @@ -11,7 +11,23 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities internal static class XmlUtility { // REF: https://docs.microsoft.com/zh-cn/dotnet/api/system.xml.serialization.xmlserializer#dynamically-generated-assemblies - private static Hashtable _serializers = new Hashtable(); + private static readonly Hashtable _xmlSerializers = new Hashtable(); + private static readonly XmlRootAttribute _xmlRoot = new XmlRootAttribute("xml"); + + private static XmlSerializer GetTypedSerializer(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + string skey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); + XmlSerializer? xmlSerializer = (XmlSerializer?)_xmlSerializers[skey]; + if (xmlSerializer == null) + { + xmlSerializer = new XmlSerializer(type, _xmlRoot); + _xmlSerializers[skey] = xmlSerializer; + } + + return xmlSerializer; + } public static string Serialize(Type type, object obj) { @@ -24,19 +40,12 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities settings.WriteEndDocumentOnClose = false; settings.NamespaceHandling = NamespaceHandling.OmitDuplicates; - string skey = type.AssemblyQualifiedName; - XmlSerializer? xmlSerializer = (XmlSerializer)_serializers[skey]; - if (xmlSerializer == null) - { - xmlSerializer = new XmlSerializer(type, new XmlRootAttribute("xml")); - _serializers[skey] = xmlSerializer; - } - using var stream = new MemoryStream(); using var writer = XmlWriter.Create(stream, settings); - XmlSerializerNamespaces xmlNamespace = new XmlSerializerNamespaces(); - xmlNamespace.Add(string.Empty, string.Empty); - xmlSerializer.Serialize(writer, obj, xmlNamespace); + XmlSerializer serializer = GetTypedSerializer(type); + XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); + ns.Add(string.Empty, string.Empty); + serializer.Serialize(writer, obj, ns); writer.Flush(); xml = Encoding.UTF8.GetString(stream.ToArray()); xml = Regex.Replace(xml, "\\s*<\\w+ (xsi|d2p1):nil=\"true\"[^>]*/>", string.Empty, RegexOptions.IgnoreCase); @@ -50,5 +59,18 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.Utilities { return Serialize(typeof(T), obj); } + + public static object Deserialize(Type type, string xml) + { + using var reader = new StringReader(xml); + XmlSerializer serializer = GetTypedSerializer(type); + return serializer.Deserialize(reader)!; + } + + public static T Deserialize(string xml) + where T : class + { + return (T)Deserialize(typeof(T), xml); + } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIPlatformClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIPlatformClientEventExtensions.cs index 5fc79c66..f7072c4d 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIPlatformClientEventExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Extensions/WechatOpenAIPlatformClientEventExtensions.cs @@ -41,11 +41,7 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI throw new Exceptions.WechatOpenAIEventSerializationException("Encrypt event failed, because of empty encrypted data."); callbackXml = Utilities.WxBizMsgCryptor.AESDecrypt(cipherText: encryptedXml!, encodingAESKey: client.Credentials.EncodingAESKey!, out _); - - using var reader = new StringReader(callbackXml); - - XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml")); - return (TEvent)xmlSerializer.Deserialize(reader)!; + return Utilities.XmlUtility.Deserialize(callbackXml); } catch (WechatOpenAIException) { diff --git a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/Internal/XmlUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/Internal/XmlUtility.cs index 92d2ffdf..569681fc 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/Internal/XmlUtility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.OpenAI/Utilities/Internal/XmlUtility.cs @@ -11,7 +11,23 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Utilities internal static class XmlUtility { // REF: https://docs.microsoft.com/zh-cn/dotnet/api/system.xml.serialization.xmlserializer#dynamically-generated-assemblies - private static Hashtable _serializers = new Hashtable(); + private static readonly Hashtable _xmlSerializers = new Hashtable(); + private static readonly XmlRootAttribute _xmlRoot = new XmlRootAttribute("xml"); + + private static XmlSerializer GetTypedSerializer(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + string skey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); + XmlSerializer? xmlSerializer = (XmlSerializer?)_xmlSerializers[skey]; + if (xmlSerializer == null) + { + xmlSerializer = new XmlSerializer(type, _xmlRoot); + _xmlSerializers[skey] = xmlSerializer; + } + + return xmlSerializer; + } public static string Serialize(Type type, object obj) { @@ -24,19 +40,12 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Utilities settings.WriteEndDocumentOnClose = false; settings.NamespaceHandling = NamespaceHandling.OmitDuplicates; - string skey = type.AssemblyQualifiedName; - XmlSerializer? xmlSerializer = (XmlSerializer)_serializers[skey]; - if (xmlSerializer == null) - { - xmlSerializer = new XmlSerializer(type, new XmlRootAttribute("xml")); - _serializers[skey] = xmlSerializer; - } - using var stream = new MemoryStream(); using var writer = XmlWriter.Create(stream, settings); - XmlSerializerNamespaces xmlNamespace = new XmlSerializerNamespaces(); - xmlNamespace.Add(string.Empty, string.Empty); - xmlSerializer.Serialize(writer, obj, xmlNamespace); + XmlSerializer serializer = GetTypedSerializer(type); + XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); + ns.Add(string.Empty, string.Empty); + serializer.Serialize(writer, obj, ns); writer.Flush(); xml = Encoding.UTF8.GetString(stream.ToArray()); xml = Regex.Replace(xml, "\\s*<\\w+ (xsi|d2p1):nil=\"true\"[^>]*/>", string.Empty, RegexOptions.IgnoreCase); @@ -50,5 +59,18 @@ namespace SKIT.FlurlHttpClient.Wechat.OpenAI.Utilities { return Serialize(typeof(T), obj); } + + public static object Deserialize(Type type, string xml) + { + using var reader = new StringReader(xml); + XmlSerializer serializer = GetTypedSerializer(type); + return serializer.Deserialize(reader)!; + } + + public static T Deserialize(string xml) + where T : class + { + return (T)Deserialize(typeof(T), xml); + } } } diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs index f048e65b..348f4fc9 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Extensions/WechatWorkClientEventExtensions.cs @@ -71,11 +71,7 @@ namespace SKIT.FlurlHttpClient.Wechat.Work throw new Exceptions.WechatWorkEventSerializationException("Encrypt event failed, because of empty encrypted data."); callbackXml = Utilities.WxBizMsgCryptor.AESDecrypt(cipherText: encryptedXml!, encodingAESKey: client.Credentials.PushEncodingAESKey!, out _); - - using var reader = new StringReader(callbackXml); - - XmlSerializer xmlSerializer = new XmlSerializer(typeof(TEvent), new XmlRootAttribute("xml")); - return (TEvent)xmlSerializer.Deserialize(reader)!; + return Utilities.XmlUtility.Deserialize(callbackXml); } catch (WechatWorkException) { diff --git a/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/Internal/XmlUtility.cs b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/Internal/XmlUtility.cs index 99bdf5fc..19d8e8d7 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/Internal/XmlUtility.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.Work/Utilities/Internal/XmlUtility.cs @@ -11,7 +11,23 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities internal static class XmlUtility { // REF: https://docs.microsoft.com/zh-cn/dotnet/api/system.xml.serialization.xmlserializer#dynamically-generated-assemblies - private static Hashtable _serializers = new Hashtable(); + private static readonly Hashtable _xmlSerializers = new Hashtable(); + private static readonly XmlRootAttribute _xmlRoot = new XmlRootAttribute("xml"); + + private static XmlSerializer GetTypedSerializer(Type type) + { + if (type == null) throw new ArgumentNullException(nameof(type)); + + string skey = type.AssemblyQualifiedName ?? type.GetHashCode().ToString(); + XmlSerializer? xmlSerializer = (XmlSerializer?)_xmlSerializers[skey]; + if (xmlSerializer == null) + { + xmlSerializer = new XmlSerializer(type, _xmlRoot); + _xmlSerializers[skey] = xmlSerializer; + } + + return xmlSerializer; + } public static string Serialize(Type type, object obj) { @@ -24,19 +40,12 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities settings.WriteEndDocumentOnClose = false; settings.NamespaceHandling = NamespaceHandling.OmitDuplicates; - string skey = type.AssemblyQualifiedName; - XmlSerializer? xmlSerializer = (XmlSerializer)_serializers[skey]; - if (xmlSerializer == null) - { - xmlSerializer = new XmlSerializer(type, new XmlRootAttribute("xml")); - _serializers[skey] = xmlSerializer; - } - using var stream = new MemoryStream(); using var writer = XmlWriter.Create(stream, settings); - XmlSerializerNamespaces xmlNamespace = new XmlSerializerNamespaces(); - xmlNamespace.Add(string.Empty, string.Empty); - xmlSerializer.Serialize(writer, obj, xmlNamespace); + XmlSerializer serializer = GetTypedSerializer(type); + XmlSerializerNamespaces ns = new XmlSerializerNamespaces(); + ns.Add(string.Empty, string.Empty); + serializer.Serialize(writer, obj, ns); writer.Flush(); xml = Encoding.UTF8.GetString(stream.ToArray()); xml = Regex.Replace(xml, "\\s*<\\w+ (xsi|d2p1):nil=\"true\"[^>]*/>", string.Empty, RegexOptions.IgnoreCase); @@ -50,5 +59,18 @@ namespace SKIT.FlurlHttpClient.Wechat.Work.Utilities { return Serialize(typeof(T), obj); } + + public static object Deserialize(Type type, string xml) + { + using var reader = new StringReader(xml); + XmlSerializer serializer = GetTypedSerializer(type); + return serializer.Deserialize(reader)!; + } + + public static T Deserialize(string xml) + where T : class + { + return (T)Deserialize(typeof(T), xml); + } } } diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiSecurityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiSecurityTests.cs index 292b3f06..439a77e5 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiSecurityTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiSecurityTests.cs @@ -1,11 +1,4 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Xunit; namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests @@ -40,13 +33,12 @@ namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests { string iv = "KEWv/gyiIwAfHvjrLeaX6w=="; string key = "YZJqKnNFi0KAiKUc0ggC2g=="; - string cipherData = "Gu2PVnxVWl+jK4F8c0liG1TiwQfaibvddu8eU1zWDDmVpPlM8ewsDzDr3l8VMY01ifZgFWNsr5QyBG0IKwM6lJNXzZHkaK9AQ4ZkVq7PYwdqNQkrg0QmKzntLMTRVNuY+TqPhXGPrOhup/orxwwCUBqheFPPwVbMeOdwrpVNyOdtsHRWQUefXN5UtDBb40pPHon4DbiHBQb5TjBPMrEF2Q=="; + string cipherText = "Gu2PVnxVWl+jK4F8c0liGxfkB5Bj3m5HRvwgEIk1Yb+36RZ3Bg7YmUnud/ooiHz0PQroipsH7GCjlGwUeT04NwmrFaP1y3dRYPLpS43ed9QZWcFIFo+8vTs3Zco6S98DUvaNEAs8duhz/BzfBOZaIHMziRqEtPFI0ZDzCgJluBirJ6Wl3UkygZ5/QLo3KA53qGdip7K48Rq8XbCwuidTCw=="; - string actualPlainData = Utilities.AESUtility.DecryptWithCBC(encodingKey: key, encodingIV: iv, encodingCipherText: cipherData).Trim(); - string actualPhoneNumber = JsonConvert.DeserializeObject(actualPlainData)["phoneNumber"].ToObject(); - string expectedPhoneNumber = "18677245613"; - - Assert.Equal(expectedPhoneNumber, actualPhoneNumber, ignoreCase: true); + string expectedPlainData = "{\"phoneNumber\":\"186****5613\",\"purePhoneNumber\":\"186****5613\",\"countryCode\":\"86\",\"watermark\":{\"timestamp\":1634545675,\"appid\":\"wxc****17e87e0e0a7\"}}"; + string actualPlainData = Utilities.AESUtility.DecryptWithCBC(encodingKey: key, encodingIV: iv, encodingCipherText: cipherText); + + Assert.Equal(expectedPlainData, actualPlainData, ignoreCase: true); } } }