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);
}
}
}