From 6630c357caa6a71e0d05ecff4976c09a226ebd32 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 4 Feb 2024 11:39:32 +0800 Subject: [PATCH] =?UTF-8?q?refactor(tenpayv3):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=8F=8D=E5=B0=84=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...TenpayClientRequestEncryptionExtensions.cs | 243 +++++-------- ...enpayClientResponseDecryptionExtensions.cs | 167 ++++----- .../Utilities/__Internal/ReflectionHelper.cs | 330 ++++++++++-------- .../TestCase_ResponseDecryptionTests.cs | 6 +- 4 files changed, 357 insertions(+), 389 deletions(-) diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientRequestEncryptionExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientRequestEncryptionExtensions.cs index 88c59645..65449ef9 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientRequestEncryptionExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientRequestEncryptionExtensions.cs @@ -6,150 +6,10 @@ using System.Reflection; namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 { using SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings; + using SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities; public static class WechatTenpayClientRequestEncryptionExtensions { - private static TRequest InnerEncryptRequestSensitivePropertyByRSA(WechatTenpayClient client, TRequest request) - where TRequest : WechatTenpayRequest - { - Utilities.ReflectionHelper.ReplacePropertyStringValue(ref request, (target, currentProp, oldValue) => - { - WechatTenpaySensitivePropertyAttribute? attribute = currentProp - .GetCustomAttributes() - .FirstOrDefault(attr => Constants.SignSchemes.WECHATPAY2_RSA_2048_WITH_SHA256.Equals(attr.Scheme)); - if (attribute is null) - return (false, oldValue); - - if (client.PlatformCertificateManager is null) - throw new WechatTenpayException("Failed to encrypt request, because the platform certificate manager is not initialized."); - - string certificate; - if (string.IsNullOrEmpty(request.WechatpayCertificateSerialNumber)) - { - // 如果未在请求中指定特定的平台证书序列号,从管理器中取过期时间最远的 - IEnumerable entries = client.PlatformCertificateManager.AllEntries() - .Where(e => CertificateEntry.ALGORITHM_TYPE_RSA.Equals(e.AlgorithmType)) - .OrderByDescending(e => e.ExpireTime); - if (!entries.Any()) - { - throw new WechatTenpayException("Failed to encrypt request, because the platform certificate manager is empty. Please make sure you have downloaded platform certificates first."); - } - - CertificateEntry entry = entries.First(); - certificate = entry.Certificate; - request.WechatpayCertificateSerialNumber = entry.SerialNumber; - } - else - { - // 如果已在请求中指定特定的平台证书序列号,直接从管理器中取值 - CertificateEntry? entry = client.PlatformCertificateManager.GetEntry(request.WechatpayCertificateSerialNumber!); - if (!entry.HasValue) - { - throw new WechatTenpayException($"Failed to encrypt request, because the platform certificate manager does not contain a certificate matched the serial number \"{request.WechatpayCertificateSerialNumber}\". Please make sure you have downloaded platform certificates first."); - } - - certificate = entry.Value.Certificate; - } - - string newValue; - switch (attribute.Algorithm) - { - case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS8_OAEP_WITH_SHA1_AND_MGF1: - { - newValue = Utilities.RSAUtility.EncryptWithECBByCertificate( - certificate: certificate, - plainText: oldValue - ); - } - break; - - case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS1: - { - newValue = Utilities.RSAUtility.EncryptWithECBByCertificate( - certificate: certificate, - plainText: oldValue, - paddingMode: "PKCS1PADDING" - ); - } - break; - - default: - { - throw new WechatTenpayException($"Failed to encrypt request. Unsupported encryption algorithm: \"{attribute.Algorithm}\"."); - } - } - - return (true, newValue); - }); - return request; - } - - private static TRequest InnerEncryptRequestSensitivePropertyBySM(WechatTenpayClient client, TRequest request) - where TRequest : WechatTenpayRequest - { - Utilities.ReflectionHelper.ReplacePropertyStringValue(ref request, (target, currentProp, oldValue) => - { - WechatTenpaySensitivePropertyAttribute? attribute = currentProp - .GetCustomAttributes() - .FirstOrDefault(attr => Constants.SignSchemes.WECHATPAY2_SM2_WITH_SM3.Equals(attr.Scheme)); - if (attribute is null) - return (false, oldValue); - - if (client.PlatformCertificateManager is null) - throw new WechatTenpayException("Failed to encrypt request, because the platform certificate manager is not initialized."); - - string certificate; - if (string.IsNullOrEmpty(request.WechatpayCertificateSerialNumber)) - { - // 如果未在请求中指定特定的平台证书序列号,从管理器中取过期时间最远的 - IEnumerable entries = client.PlatformCertificateManager.AllEntries() - .Where(e => CertificateEntry.ALGORITHM_TYPE_SM2.Equals(e.AlgorithmType)) - .OrderByDescending(e => e.ExpireTime); - if (!entries.Any()) - { - throw new WechatTenpayException("Failed to encrypt request, because the platform certificate manager is empty. Please make sure you have downloaded platform certificates first."); - } - - CertificateEntry entry = entries.First(); - certificate = entry.Certificate; - request.WechatpayCertificateSerialNumber = entry.SerialNumber; - } - else - { - // 如果已在请求中指定特定的平台证书序列号,直接从管理器中取值 - CertificateEntry? entry = client.PlatformCertificateManager.GetEntry(request.WechatpayCertificateSerialNumber!); - if (!entry.HasValue) - { - throw new WechatTenpayException($"Failed to encrypt request, because the platform certificate manager does not contain a certificate matched the serial number \"{request.WechatpayCertificateSerialNumber}\". Please make sure you have downloaded platform certificates first."); - } - - certificate = entry.Value.Certificate; - } - - string newValue; - switch (attribute.Algorithm) - { - case Constants.EncryptionAlgorithms.SM2_C1C3C2_ASN1: - { - newValue = Utilities.SM2Utility.EncryptByCertificate( - certificate: certificate, - plainText: oldValue, - asn1Encoding: true - ); - } - break; - - default: - { - throw new WechatTenpayException($"Failed to encrypt request. Unsupported encryption algorithm: \"{attribute.Algorithm}\"."); - } - } - - return (true, newValue); - }); - return request; - } - /// /// 加密请求中传入的敏感数据。该方法会改变传入的请求模型对象。 /// @@ -164,19 +24,98 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 try { - // 遍历并加密被标记为敏感数据的字段 - bool requireEncrypt = Attribute.IsDefined(request.GetType(), typeof(WechatTenpaySensitiveAttribute)); - if (requireEncrypt) - { - switch (client.Credentials.SignScheme) - { - case Constants.SignSchemes.WECHATPAY2_RSA_2048_WITH_SHA256: - return InnerEncryptRequestSensitivePropertyByRSA(client, request); + bool requireEncrypt = request.GetType().IsDefined(typeof(WechatTenpaySensitiveAttribute)); + if (!requireEncrypt) + return request; - case Constants.SignSchemes.WECHATPAY2_SM2_WITH_SM3: - return InnerEncryptRequestSensitivePropertyBySM(client, request); + string signScheme = client.Credentials.SignScheme; + string algorithmType = // 签名方式与加密算法保持一致:RSA_SHA256 签名需 RSA 加密,SM3 签名需 SM2 加密 + Constants.SignSchemes.WECHATPAY2_RSA_2048_WITH_SHA256.Equals(signScheme) ? CertificateEntry.ALGORITHM_TYPE_RSA : + Constants.SignSchemes.WECHATPAY2_SM2_WITH_SM3.Equals(signScheme) ? CertificateEntry.ALGORITHM_TYPE_SM2 : + throw new WechatTenpayException($"Failed to encrypt request. Unsupported signing scheme: \"{signScheme}\"."); + + ReflectionHelper.ReplaceObjectStringProperties(request, (_, currentProp, oldValue) => + { + if (currentProp is null || !currentProp.IsDefined(typeof(WechatTenpaySensitivePropertyAttribute))) + return (false, oldValue); + + WechatTenpaySensitivePropertyAttribute? attribute = currentProp + .GetCustomAttributes() + .FirstOrDefault(attr => attr.Scheme == signScheme); + if (attribute is null) + return (false, oldValue); + + string certificate; + if (string.IsNullOrEmpty(request.WechatpayCertificateSerialNumber)) + { + if (client.PlatformCertificateManager is null) + throw new WechatTenpayException("Failed to encrypt request, because the platform certificate manager is not initialized."); + + // 如果未在请求中指定特定的平台证书序列号,从管理器中取过期时间最远的 + IEnumerable entries = client.PlatformCertificateManager.AllEntries() + .Where(e => e.AlgorithmType == algorithmType) + .OrderByDescending(e => e.ExpireTime); + if (!entries.Any()) + { + throw new WechatTenpayException("Failed to encrypt request, because the platform certificate manager is empty. Please make sure you have downloaded platform (NOT merchant) certificates first."); + } + + CertificateEntry entry = entries.First(); + certificate = entry.Certificate; + request.WechatpayCertificateSerialNumber = entry.SerialNumber; } - } + else + { + // 如果已在请求中指定特定的平台证书序列号,直接从管理器中取值 + CertificateEntry? entry = client.PlatformCertificateManager?.GetEntry(request.WechatpayCertificateSerialNumber!); + if (!entry.HasValue) + { + throw new WechatTenpayException($"Failed to encrypt request, because the platform certificate manager does not contain a certificate matched the serial number \"{request.WechatpayCertificateSerialNumber}\". Please make sure you have downloaded platform (NOT merchant) certificates first."); + } + + certificate = entry.Value.Certificate; + } + + string newValue; + switch (attribute.Algorithm) + { + case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS8_OAEP_WITH_SHA1_AND_MGF1: + { + newValue = RSAUtility.EncryptWithECBByCertificate( + certificate: certificate, + plainText: oldValue + ); + } + break; + + case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS1: + { + newValue = RSAUtility.EncryptWithECBByCertificate( + certificate: certificate, + plainText: oldValue, + paddingMode: "PKCS1PADDING" + ); + } + break; + + case Constants.EncryptionAlgorithms.SM2_C1C3C2_ASN1: + { + newValue = SM2Utility.EncryptByCertificate( + certificate: certificate, + plainText: oldValue, + asn1Encoding: true + ); + } + break; + + default: + { + throw new WechatTenpayException($"Failed to encrypt request. Unsupported encryption algorithm: \"{attribute.Algorithm}\"."); + } + } + + return (true, newValue); + }); return request; } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseDecryptionExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseDecryptionExtensions.cs index fda1a03c..6b041fc3 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseDecryptionExtensions.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseDecryptionExtensions.cs @@ -5,11 +5,22 @@ using System.Text; namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 { + using SKIT.FlurlHttpClient.Wechat.TenpayV3.Models; + using SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities; + public static class WechatTenpayClientResponseDecryptionExtensions { - private static TResponse InnerDecryptResponseSensitiveProperty(WechatTenpayClient client, TResponse response) - where TResponse : Models.QueryCertificatesResponse + /// + /// 解密响应中返回的敏感数据。该方法会改变传入的响应模型对象。 + /// + /// + /// + /// + public static QueryCertificatesResponse DecryptResponseSensitiveProperty(this WechatTenpayClient client, QueryCertificatesResponse response) { + if (client is null) throw new ArgumentNullException(nameof(client)); + if (response is null) throw new ArgumentNullException(nameof(response)); + if (response.CertificateList is null) return response; @@ -25,7 +36,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 if (string.IsNullOrEmpty(client.Credentials.MerchantCertificatePrivateKey)) throw new WechatTenpayException("Failed to decrypt response, because the merchant private key is not set."); - certificate.EncryptCertificate.CipherText = Utilities.AESUtility.DecryptWithGCM( + certificate.EncryptCertificate.CipherText = AESUtility.DecryptWithGCM( key: client.Credentials.MerchantV3Secret, nonce: certificate.EncryptCertificate.Nonce, aad: certificate.EncryptCertificate.AssociatedData, @@ -41,11 +52,11 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 // REF: https://pay.weixin.qq.com/docs/merchant/development/shangmi/guide.html // 由于 SM4 密钥长度的限制,密钥由 APIv3 密钥通过国密 SM3 Hash 计算生成。SM4 密钥取其摘要(256bit)的前 128bit。 - byte[] secretBytes = Utilities.SM3Utility.Hash(Encoding.UTF8.GetBytes(client.Credentials.MerchantV3Secret)); + byte[] secretBytes = SM3Utility.Hash(Encoding.UTF8.GetBytes(client.Credentials.MerchantV3Secret)); byte[] keyBytes = new byte[16]; Array.Copy(secretBytes, keyBytes, keyBytes.Length); - byte[] plainBytes = Utilities.SM4Utility.DecryptWithGCM( + byte[] plainBytes = SM4Utility.DecryptWithGCM( keyBytes: keyBytes, nonceBytes: Encoding.UTF8.GetBytes(certificate.EncryptCertificate.Nonce), aadBytes: Encoding.UTF8.GetBytes(certificate.EncryptCertificate.AssociatedData ?? string.Empty), @@ -65,85 +76,6 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 return response; } - private static TResponse InnerDecryptResponseSensitivePropertyByRSA(WechatTenpayClient client, TResponse response) - where TResponse : WechatTenpayResponse - { - Utilities.ReflectionHelper.ReplacePropertyStringValue(ref response, (target, currentProp, oldValue) => - { - WechatTenpaySensitivePropertyAttribute? attribute = currentProp - .GetCustomAttributes() - .FirstOrDefault(attr => Constants.SignSchemes.WECHATPAY2_RSA_2048_WITH_SHA256.Equals(attr.Scheme)); - if (attribute is null) - return (false, oldValue); - - string newValue; - switch (attribute.Algorithm) - { - case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS8_OAEP_WITH_SHA1_AND_MGF1: - { - newValue = Utilities.RSAUtility.DecryptWithECB( - privateKey: client.Credentials.MerchantCertificatePrivateKey, - cipherText: oldValue - ); - } - break; - - case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS1: - { - newValue = Utilities.RSAUtility.DecryptWithECB( - privateKey: client.Credentials.MerchantCertificatePrivateKey, - cipherText: oldValue, - paddingMode: "PKCS1PADDING" - ); - } - break; - - default: - { - throw new WechatTenpayException($"Failed to decrypt response. Unsupported encryption algorithm: \"{attribute.Algorithm}\"."); - } - } - - return (true, newValue); - }); - return response; - } - - private static TResponse InnerDecryptResponseSensitivePropertyBySM(WechatTenpayClient client, TResponse response) - where TResponse : WechatTenpayResponse - { - Utilities.ReflectionHelper.ReplacePropertyStringValue(ref response, (target, currentProp, oldValue) => - { - WechatTenpaySensitivePropertyAttribute? attribute = currentProp - .GetCustomAttributes() - .FirstOrDefault(attr => Constants.SignSchemes.WECHATPAY2_SM2_WITH_SM3.Equals(attr.Scheme)); - if (attribute is null) - return (false, oldValue); - - string newValue; - switch (attribute.Algorithm) - { - case Constants.EncryptionAlgorithms.SM2_C1C3C2_ASN1: - { - newValue = Utilities.SM2Utility.Decrypt( - privateKey: client.Credentials.MerchantCertificatePrivateKey, - cipherText: oldValue, - asn1Encoding: true - ); - } - break; - - default: - { - throw new WechatTenpayException($"Failed to decrypt response. Unsupported encryption algorithm: \"{attribute.Algorithm}\"."); - } - } - - return (true, newValue); - }); - return response; - } - /// /// 解密响应中返回的敏感数据。该方法会改变传入的响应模型对象。 /// @@ -162,24 +94,69 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3 try { // [GET] /certificates 接口的响应模型需特殊处理 - if (response is Models.QueryCertificatesResponse queryCertificatesResponse) + if (response is QueryCertificatesResponse queryCertificatesResponse) { - return (InnerDecryptResponseSensitiveProperty(client, queryCertificatesResponse) as TResponse)!; + return (DecryptResponseSensitiveProperty(client, queryCertificatesResponse) as TResponse)!; } // 遍历并解密被标记为敏感数据的字段 bool requireDecrypt = Attribute.IsDefined(response.GetType(), typeof(WechatTenpaySensitiveAttribute)); - if (requireDecrypt) - { - switch (client.Credentials.SignScheme) - { - case Constants.SignSchemes.WECHATPAY2_RSA_2048_WITH_SHA256: - return InnerDecryptResponseSensitivePropertyByRSA(client, response); + if (!requireDecrypt) + return response; - case Constants.SignSchemes.WECHATPAY2_SM2_WITH_SM3: - return InnerDecryptResponseSensitivePropertyBySM(client, response); + string signScheme = client.Credentials.SignScheme; + + ReflectionHelper.ReplaceObjectStringProperties(response, (_, currentProp, oldValue) => + { + if (currentProp is null || !currentProp.IsDefined(typeof(WechatTenpaySensitivePropertyAttribute))) + return (false, oldValue); + + WechatTenpaySensitivePropertyAttribute? attribute = currentProp + .GetCustomAttributes() + .FirstOrDefault(attr => attr.Scheme == signScheme); + if (attribute is null) + return (false, oldValue); + + string newValue; + switch (attribute.Algorithm) + { + case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS8_OAEP_WITH_SHA1_AND_MGF1: + { + newValue = RSAUtility.DecryptWithECB( + privateKey: client.Credentials.MerchantCertificatePrivateKey, + cipherText: oldValue + ); + } + break; + + case Constants.EncryptionAlgorithms.RSA_2048_ECB_PKCS1: + { + newValue = RSAUtility.DecryptWithECB( + privateKey: client.Credentials.MerchantCertificatePrivateKey, + cipherText: oldValue, + paddingMode: "PKCS1PADDING" + ); + } + break; + + case Constants.EncryptionAlgorithms.SM2_C1C3C2_ASN1: + { + newValue = SM2Utility.Decrypt( + privateKey: client.Credentials.MerchantCertificatePrivateKey, + cipherText: oldValue, + asn1Encoding: true + ); + } + break; + + default: + { + throw new WechatTenpayException($"Failed to decrypt response. Unsupported encryption algorithm: \"{attribute.Algorithm}\"."); + } } - } + + return (true, newValue); + }); return response; } diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/__Internal/ReflectionHelper.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/__Internal/ReflectionHelper.cs index 1c3f6773..ad62769b 100644 --- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/__Internal/ReflectionHelper.cs +++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Utilities/__Internal/ReflectionHelper.cs @@ -1,172 +1,122 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; using System.Reflection; +using System.Runtime.CompilerServices; namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities { internal static partial class ReflectionHelper { - private static readonly Hashtable _cache = new Hashtable(); - - private static PropertyInfo[] GetTypedProperties(Type type) + public static void ReplaceObjectStringProperties(object targetObj, ReplaceObjectStringPropertiesReplacementDelegate replacement) { - if (type is null) throw new ArgumentNullException(nameof(type)); - - string key = type.FullName ?? type.AssemblyQualifiedName!; - PropertyInfo[]? properties = (PropertyInfo[]?)_cache[key]; - if (properties is null) - { - properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); - _cache[key] = properties; - } - - return properties; - } - - public static void ReplacePropertyStringValue(ref T obj, ReplacePropertyStringValueReplacementHandler replacement) - { - InnerReplacePropertyStringValue(ref obj, replacement); - } - - private static void InnerReplacePropertyStringValue(ref T obj, ReplacePropertyStringValueReplacementHandler replacement) - { - if (obj is null) throw new ArgumentNullException(nameof(obj)); + if (targetObj is null) throw new ArgumentNullException(nameof(targetObj)); if (replacement is null) throw new ArgumentNullException(nameof(replacement)); - Type objType = obj.GetType(); - if (!objType.IsClass) - throw new NotSupportedException(); - - if (objType.IsArray || obj is IList || obj is IDictionary) - { - InnerReplaceEachCollectionPropertyStringValue(ref obj, objType, replacement, null); - } - else - { - foreach (var childProp in GetTypedProperties(objType)) - { - if (!childProp.CanWrite) - continue; - - Type propType = childProp.PropertyType; - if (propType == typeof(string)) - { - string value = (string)childProp.GetValue(obj, null)!; - if (value is null) - continue; - - var result = replacement(obj, childProp, value); - if (result.Modified) - { - childProp.SetValue(obj, result.NewValue); - } - } - else if (propType.IsClass) - { - object? value = childProp.GetValue(obj, null); - if (value is null) - continue; - - InnerReplacePropertyStringValue(ref value, replacement); - childProp.SetValue(obj, value); - } - else - { - object? value = childProp.GetValue(obj, null); - if (value is null) - continue; - - InnerReplaceEachCollectionPropertyStringValue(ref value, propType, replacement, childProp); - } - } - } + ISet visited = new HashSet(ReferenceEqualityComparer.Instance); // 处理循环引用问题 + InnerReplaceObjectStringProperties(ref targetObj, null, replacement, visited); } + } - private static void InnerReplaceEachCollectionPropertyStringValue(ref T obj, Type objType, ReplacePropertyStringValueReplacementHandler replacement, PropertyInfo? currentProp) + partial class ReflectionHelper + { + public delegate (bool IsModified, string NewValue) ReplaceObjectStringPropertiesReplacementDelegate(object currentObj, PropertyInfo? currentProp, string oldValue); + + private static void InnerReplaceObjectStringProperties(ref object currentObj, PropertyInfo? currentProp, ReplaceObjectStringPropertiesReplacementDelegate replacement, ISet visited) { - if (objType.IsArray) - { - var array = (obj as Array)!; + if (currentObj is null) throw new ArgumentNullException(nameof(currentObj)); + if (replacement is null) throw new ArgumentNullException(nameof(replacement)); - for (int i = 0, len = array.Length; i < len; i++) + if (!visited.Add(currentObj)) return; + + Type type = currentObj.GetType(); + + // 跳过基元类型、枚举类型、抽象或接口类型,及部分 CLR 内置类型 + if (type.IsPrimitive || + type.IsEnum || + type.IsAbstract || + type.IsInterface || + type == typeof(Guid) || + type == typeof(DateTime) || + type == typeof(DateTimeOffset) || +#if NET5_0_OR_GREATER + type == typeof(DateOnly) || + type == typeof(TimeOnly) || +#endif + type == typeof(TimeSpan)) + { + return; + } + + // 处理数组类型 + if (type.IsArray) + { + Array currentObjAsArray = (Array)currentObj; + for (int i = 0; i < currentObjAsArray.Length; i++) { - object? element = array.GetValue(i); + object? element = currentObjAsArray.GetValue(i); if (element is null) continue; Type elementType = element.GetType(); if (elementType == typeof(string)) { - if (currentProp is null) + if (currentObjAsArray.IsReadOnly) continue; - if (!currentProp.CanWrite) + if ((string)element == string.Empty) continue; - var oldValue = (string)element!; - var resHandler = replacement(obj!, currentProp, oldValue); - if (resHandler.Modified && !array.IsReadOnly) - { - array.SetValue(resHandler.NewValue, i); - } - } - else if (elementType.IsClass) - { - InnerReplacePropertyStringValue(ref element, replacement); - //if (!array.IsReadOnly) - //{ - // array.SetValue(element, i); - //} + var res = replacement(currentObjAsArray, currentProp, (string)element); + if (res.IsModified) + currentObjAsArray.SetValue(res.NewValue, i); } else { - continue; + InnerReplaceObjectStringProperties(ref element, currentProp, replacement, visited); } } - } - else if (obj is IList) - { - var list = (obj as IList)!; - for (int i = 0, len = list.Count; i < len; i++) + return; + } + + // 处理列表类型 + if (currentObj is IList) + { + IList currentObjAsList = (IList)currentObj; + for (int i = 0; i < currentObjAsList.Count; i++) { - object? element = list[i]; + object? element = currentObjAsList[i]; if (element is null) continue; Type elementType = element.GetType(); if (elementType == typeof(string)) { - if (currentProp is null) + if (currentObjAsList.IsReadOnly) continue; - if (!currentProp.CanWrite) + if ((string)element == string.Empty) continue; - var oldValue = (string)element!; - var resHandler = replacement(obj, currentProp, oldValue); - if (resHandler.Modified && !list.IsReadOnly) - { - list[i] = resHandler.NewValue; - } - } - else if (elementType.IsClass) - { - InnerReplacePropertyStringValue(ref element, replacement); - //if (!list.IsReadOnly) - //{ - // list[i] = element; - //} + var res = replacement(currentObjAsList, currentProp, (string)element); + if (res.IsModified) + currentObjAsList[i] = res.NewValue; } else { - continue; + InnerReplaceObjectStringProperties(ref element, currentProp, replacement, visited); } } - } - else if (obj is IDictionary) - { - var dict = (obj as IDictionary)!; - foreach (DictionaryEntry entry in dict) + return; + } + + // 处理字典类型 + if (currentObj is IDictionary) + { + IDictionary currentObjAsDictionary = (IDictionary)currentObj; + foreach (DictionaryEntry entry in currentObjAsDictionary) { object? entryValue = entry.Value; if (entryValue is null) @@ -175,37 +125,137 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Utilities Type entryValueType = entryValue.GetType(); if (entryValueType == typeof(string)) { - if (currentProp is null) + if (currentObjAsDictionary.IsReadOnly) continue; - if (!currentProp.CanWrite) + if ((string)entryValue == string.Empty) continue; - string oldValue = (string)entryValue!; - var resHandler = replacement(obj, currentProp, oldValue); - if (resHandler.Modified && !dict.IsReadOnly) - { - dict[entry.Key] = resHandler.NewValue; - } - } - else if (entryValueType.IsClass) - { - InnerReplacePropertyStringValue(ref entryValue, replacement); - //if (!dict.IsReadOnly) - //{ - // dict[entry.Key] = entryValue; - //} + var res = replacement(currentObjAsDictionary, currentProp, (string)entryValue); + if (res.IsModified) + currentObjAsDictionary[entry.Key] = res.NewValue; } else { - continue; + InnerReplaceObjectStringProperties(ref entryValue, currentProp, replacement, visited); } } + + return; + } + + // 遍历属性 + foreach (PropertyInfo property in GetWritableProperties(type)) + { + Type propertyType = property.PropertyType; + if (propertyType == typeof(string)) + { + string? propertyValue = GetPropertyValue(currentObj, property); + if (propertyValue is null || propertyValue == string.Empty) + continue; + + var res = replacement(currentObj, property, propertyValue); + if (res.IsModified) + SetPropertyValue(currentObj, property, res.NewValue); + } + else + { + object? propertyValue = GetPropertyValue(currentObj, property); + if (propertyValue is null) + continue; + + InnerReplaceObjectStringProperties(ref propertyValue, property, replacement, visited); + } } } } partial class ReflectionHelper { - public delegate (bool Modified, string NewValue) ReplacePropertyStringValueReplacementHandler(object target, PropertyInfo currentProp, string oldValue); + private static readonly IDictionary _propsCache = new Dictionary(capacity: 128); + private static readonly Hashtable _getterCache = new Hashtable(capacity: 128); + private static readonly Hashtable _setterCache = new Hashtable(capacity: 128); + + private static PropertyInfo[] GetWritableProperties(Type type) + { + if (type is null) throw new ArgumentNullException(nameof(type)); + + if (!_propsCache.TryGetValue(type, out PropertyInfo[]? properties)) + { + properties = type.GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(e => e.CanRead && e.CanWrite) + .ToArray(); + _propsCache[type] = properties; + } + + return properties; + } + + private static T? GetPropertyValue(object targetObj, PropertyInfo property) + { + // 提供比 PropertyInfo.GetValue() 更快的属性取值方法 + // 只可针对热点类型使用,否则可能会更慢 + + if (targetObj is null) throw new ArgumentNullException(nameof(targetObj)); + if (property is null) throw new ArgumentNullException(nameof(property)); + + if (!property.CanRead) + throw new InvalidOperationException($"Property '{property.Name}' of type '{typeof(T).FullName}' does not have a getter."); + + Func? getter = _getterCache[property] as Func; + if (getter is null) + { + ParameterExpression targetExpr = Expression.Parameter(typeof(object)); + UnaryExpression castTargetExpr = Expression.Convert(targetExpr, targetObj.GetType()); + MemberExpression getPropertyValueExpr = Expression.Property(castTargetExpr, property); + UnaryExpression castPropertyValueExpr = Expression.Convert(getPropertyValueExpr, typeof(T)); + getter = Expression.Lambda>(castPropertyValueExpr, targetExpr).Compile(); + _getterCache[property] = getter; + } + + return getter.Invoke(targetObj); + } + + private static void SetPropertyValue(object targetObj, PropertyInfo property, T? value) + { + // 提供比 PropertyInfo.SetValue() 更快的属性赋值方法 + // 只可针对热点类型使用,否则可能会更慢 + + if (targetObj is null) throw new ArgumentNullException(nameof(targetObj)); + if (property is null) throw new ArgumentNullException(nameof(property)); + + if (!property.CanWrite) + throw new InvalidOperationException($"Property '{property.Name}' of type '{typeof(T).FullName}' does not have a setter."); + + Action? setter = _setterCache[property] as Action; + if (setter is null) + { + ParameterExpression targetExpr = Expression.Parameter(typeof(object)); + ParameterExpression propertyValueExpr = Expression.Parameter(typeof(T)); + UnaryExpression castTargetExpr = Expression.Convert(targetExpr, targetObj.GetType()); + UnaryExpression castPropertyValueExpr = Expression.Convert(propertyValueExpr, property.PropertyType); + MethodCallExpression setPropertyValueExpr = Expression.Call(castTargetExpr, property.GetSetMethod()!, castPropertyValueExpr); + setter = Expression.Lambda>(setPropertyValueExpr, targetExpr, propertyValueExpr).Compile(); + _setterCache[property] = setter; + } + + setter.Invoke(targetObj, value); + } + } + + partial class ReflectionHelper + { +#if NET5_0_OR_GREATER +#else + private sealed class ReferenceEqualityComparer : IEqualityComparer, IEqualityComparer + { + public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer(); + + private ReferenceEqualityComparer() { } + + public new bool Equals(object? x, object? y) => ReferenceEquals(x, y); + + public int GetHashCode(object? obj) => RuntimeHelpers.GetHashCode(obj!); + } +#endif } } diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_ResponseDecryptionTests.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_ResponseDecryptionTests.cs index fe6873f7..67a408b5 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_ResponseDecryptionTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/TestCase_ResponseDecryptionTests.cs @@ -9,6 +9,8 @@ using Xunit; namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests { + using SKIT.FlurlHttpClient.Internal; + public partial class TestCase_ResponseDecryptionTests { // 此处测试的 RSA/SM2 证书/公钥/私钥是自签名生成的,仅供执行 RSA/SM2 相关的单元测试,不能用于调用微信支付 API。 @@ -1151,8 +1153,8 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests private static T SetMockResponseRawStatusAsOk(T response) where T : WechatTenpayResponse { - FieldInfo fieldInfo = typeof(CommonResponseBase).GetField("_InternalRawStatus", BindingFlags.Instance | BindingFlags.NonPublic)!; - fieldInfo.SetValue(response, (int)HttpStatusCode.OK); + var accessor = _UnsafeAccessor.VisitCommonResponse(response); + accessor.RawStatus = (int)HttpStatusCode.OK; return response; } }