diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/FormDataFields.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/Internal/FormDataFields.cs
similarity index 100%
rename from src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/FormDataFields.cs
rename to src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/Internal/FormDataFields.cs
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/SignAlgorithms.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/SignAlgorithms.cs
new file mode 100644
index 00000000..56ffe932
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Constants/SignAlgorithms.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
+{
+ public static class SignAlgorithms
+ {
+ ///
+ /// WECHATPAY2-SHA256-RSA2048。
+ ///
+ public const string WECHATPAY2_SHA256_RSA2048 = "WECHATPAY2-SHA256-RSA2048";
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventDecryptionExtensions.cs
similarity index 96%
rename from src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventExtensions.cs
rename to src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventDecryptionExtensions.cs
index 73afbc90..fdc60163 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventExtensions.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventDecryptionExtensions.cs
@@ -3,9 +3,9 @@
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{
///
- /// 为 提供回调通知事件的扩展方法。
+ /// 为 提供回调通知事件敏感数据解密的扩展方法。
///
- public static class WechatTenpayClientEventExtensions
+ public static class WechatTenpayClientEventDecryptionExtensions
{
///
/// 反序列化得到 对象。
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs
new file mode 100644
index 00000000..8c51113a
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientEventVerificationExtensions.cs
@@ -0,0 +1,80 @@
+using System;
+using System.Text;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
+{
+ ///
+ /// 为 提供回调通知事件签名验证的扩展方法。
+ ///
+ public static class WechatTenpayClientEventVerificationExtensions
+ {
+ ///
+ /// 验证回调通知事件签名。
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml
+ ///
+ ///
+ ///
+ /// 微信回调通知中的 Wechatpay-Timestamp 字段。
+ /// 微信回调通知中的 Wechatpay-Nonce 字段。
+ /// 微信回调通知中请求正文。
+ /// 微信回调通知中的 Wechatpay-Signature 字段。
+ /// 微信回调通知中的 Wechatpay-Serial 字段。
+ ///
+ public static bool VerifyEventSignature(
+ this WechatTenpayClient client,
+ string callbackTimestamp,
+ string callbackNonce,
+ string callbackBody,
+ string callbackSignature,
+ string callbackSerialNumber)
+ where TResponse : WechatTenpayResponse
+ {
+ if (client == null) throw new ArgumentNullException(nameof(client));
+ if (callbackTimestamp == null) throw new ArgumentNullException(nameof(callbackTimestamp));
+ if (callbackNonce == null) throw new ArgumentNullException(nameof(callbackNonce));
+ if (callbackBody == null) throw new ArgumentNullException(nameof(callbackBody));
+ if (callbackSignature == null) throw new ArgumentNullException(nameof(callbackSignature));
+ if (callbackSerialNumber == null) throw new ArgumentNullException(nameof(callbackSerialNumber));
+
+ if (client.WechatCertificateStorer == null)
+ {
+ throw new Exceptions.WechatTenpayResponseVerificationException($"You must set an instance of `{nameof(Settings.ICertificateStorer)}` at first.");
+ }
+ else
+ {
+ string? certificate = client.WechatCertificateStorer.Get(callbackSerialNumber);
+ if (certificate == null)
+ throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get certificate by the serial number, may not be stored.");
+
+ string? publicKey = null;
+ try
+ {
+ publicKey = Utilities.RSAUtility.ExportPublicKey(certificate);
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get public key of the certificate, may not be a valid certificate data.", ex);
+ }
+
+ try
+ {
+ return Utilities.RSAUtility.VerifyWithSHA256(
+ publicKey: publicKey,
+ plainText: GetPlainTextForSignature(timestamp: callbackTimestamp, nonce: callbackNonce, body: callbackBody),
+ signature: callbackSignature
+ );
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatTenpayResponseVerificationException("Verify event signature failed.", ex);
+ }
+ }
+ }
+
+ private static string GetPlainTextForSignature(string timestamp, string nonce, string body)
+ {
+ return $"{timestamp}\n{nonce}\n{body}\n";
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs
new file mode 100644
index 00000000..0b45bf7c
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerificationExtensions.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Text;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
+{
+ ///
+ /// 为 提供响应签名验证的扩展方法。
+ ///
+ public static class WechatTenpayClientResponseVerificationExtensions
+ {
+ ///
+ /// 验证响应签名。
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
+ /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml
+ ///
+ ///
+ ///
+ ///
+ ///
+ public static bool VerifyResponseSignature(this WechatTenpayClient client, TResponse response)
+ where TResponse : WechatTenpayResponse
+ {
+ if (client == null) throw new ArgumentNullException(nameof(client));
+ if (response == null) throw new ArgumentNullException(nameof(response));
+
+ if (client.WechatCertificateStorer == null)
+ {
+ throw new Exceptions.WechatTenpayResponseVerificationException($"You must set an instance of `{nameof(Settings.ICertificateStorer)}` at first.");
+ }
+ else
+ {
+ if (response.WechatpayCertSerialNumber == null)
+ throw new Exceptions.WechatTenpayResponseVerificationException("Cannot read the serial number in headers of this response.");
+
+ string? certificate = client.WechatCertificateStorer.Get(response.WechatpayCertSerialNumber);
+ if (certificate == null)
+ throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get certificate by the serial number, may not be stored.");
+
+ string? publicKey = null;
+ try
+ {
+ publicKey = Utilities.RSAUtility.ExportPublicKey(certificate);
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatTenpayResponseVerificationException("Cannot get public key of the certificate, may not be a valid certificate data.", ex);
+ }
+
+ try
+ {
+ return Utilities.RSAUtility.VerifyWithSHA256(
+ publicKey: publicKey,
+ plainText: GetPlainTextForSignature(response),
+ signature: response.WechatpaySignature
+ );
+ }
+ catch (Exception ex)
+ {
+ throw new Exceptions.WechatTenpayResponseVerificationException("Verify response signature failed.", ex);
+ }
+ }
+ }
+
+ private static string GetPlainTextForSignature(WechatTenpayResponse response)
+ {
+ string timestamp = response.WechatpayTimestamp;
+ string nonce = response.WechatpayNonce;
+ string body = Encoding.UTF8.GetString(response.RawBytes);
+ return $"{timestamp}\n{nonce}\n{body}\n";
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerifyExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerifyExtensions.cs
deleted file mode 100644
index 2e86dd8d..00000000
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Extensions/WechatTenpayClientResponseVerifyExtensions.cs
+++ /dev/null
@@ -1,81 +0,0 @@
-using System;
-using System.Text;
-
-namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
-{
- ///
- /// 为 提供响应签名验证的扩展方法。
- ///
- public static class WechatTenpayClientResponseVerifyExtensions
- {
- ///
- /// 验证响应签名(使用微信支付平台证书公钥)。
- /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
- /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml
- ///
- ///
- ///
- ///
- ///
- ///
- public static bool VerifyResponseSignature(this WechatTenpayClient client, TResponse response, string publicKey)
- where TResponse : WechatTenpayResponse
- {
- if (client == null) throw new ArgumentNullException(nameof(client));
- if (response == null) throw new ArgumentNullException(nameof(response));
- if (string.IsNullOrEmpty(publicKey)) throw new ArgumentNullException(publicKey);
-
- try
- {
- return Utilities.RSAUtility.VerifyWithSHA256(
- publicKey: publicKey,
- plainText: GetPlainTextForSignature(response),
- signature: response.WechatpaySignature
- );
- }
- catch (Exception ex)
- {
- throw new Exceptions.WechatTenpayResponseVerificationException("Verify response signature failed.", ex);
- }
- }
-
- ///
- /// 验证响应签名(使用微信支付平台证书)。
- /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml
- /// REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml
- ///
- ///
- ///
- ///
- ///
- ///
- public static bool VerifyResponseSignatureByCertificate(this WechatTenpayClient client, TResponse response, string certificate)
- where TResponse : WechatTenpayResponse
- {
- if (client == null) throw new ArgumentNullException(nameof(client));
- if (response == null) throw new ArgumentNullException(nameof(response));
- if (string.IsNullOrEmpty(certificate)) throw new ArgumentNullException(certificate);
-
- try
- {
- return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
- certificate: certificate,
- plainText: GetPlainTextForSignature(response),
- signature: response.WechatpaySignature
- );
- }
- catch (Exception ex)
- {
- throw new Exceptions.WechatTenpayResponseVerificationException("Verify response signature failed.", ex);
- }
- }
-
- private static string GetPlainTextForSignature(WechatTenpayResponse response)
- {
- string timestamp = response.WechatpayTimestamp;
- string nonce = response.WechatpayNonce;
- string body = Encoding.UTF8.GetString(response.RawBytes);
- return $"{timestamp}\n{nonce}\n{body}\n";
- }
- }
-}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs
index 6f8e101e..2d426685 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Interceptors/WechatTenpaySignInterceptor.cs
@@ -53,7 +53,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Interceptors
switch (_scheme)
{
- case WechatTenpayAuthSchemes.WECHATPAY2_SHA256_RSA2048:
+ case Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048:
{
try
{
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/ICertificateStorer.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/ICertificateStorer.cs
new file mode 100644
index 00000000..0c99f9ff
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/ICertificateStorer.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
+{
+ ///
+ /// 微信商户平台证书存储器接口。
+ ///
+ public interface ICertificateStorer
+ {
+ ///
+ /// 根据证书序列号获取证书(cer 格式)。
+ ///
+ ///
+ ///
+ string? Get(string serialNumber);
+
+ ///
+ /// 设置证书序列号与证书(cer 格式)的映射关系。
+ ///
+ ///
+ ///
+ void Set(string serialNumber, string certificate);
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/InMemoryCertificateStorer.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/InMemoryCertificateStorer.cs
new file mode 100644
index 00000000..37afe581
--- /dev/null
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/Settings/InMemoryCertificateStorer.cs
@@ -0,0 +1,37 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
+{
+ ///
+ /// 一个基于内存实现的 。
+ ///
+ public class InMemoryCertificateStorer : ICertificateStorer
+ {
+ public IDictionary _dict;
+
+ public InMemoryCertificateStorer()
+ {
+ _dict = new ConcurrentDictionary();
+ }
+
+ string? ICertificateStorer.Get(string serialNumber)
+ {
+ if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber));
+
+ return _dict[serialNumber];
+ }
+
+ void ICertificateStorer.Set(string serialNumber, string certificate)
+ {
+ if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber));
+ if (certificate == null) throw new ArgumentNullException(nameof(certificate));
+
+ _dict[serialNumber] = certificate;
+ }
+ }
+}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayAuthSchemes.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayAuthSchemes.cs
deleted file mode 100644
index 372fecdd..00000000
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayAuthSchemes.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-using System;
-
-namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
-{
- ///
- /// 微信支付 API 接口签名认证方式。
- ///
- public static class WechatTenpayAuthSchemes
- {
- ///
- /// WECHATPAY2-SHA256-RSA2048(默认)。
- ///
- public const string WECHATPAY2_SHA256_RSA2048 = "WECHATPAY2-SHA256-RSA2048";
- }
-}
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs
index a7afc7a2..d2e084fb 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClient.cs
@@ -39,6 +39,11 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
///
internal string WechatMerchantV3Secret { get; }
+ ///
+ /// 获取当前客户端使用的微信商户平台证书存储器。
+ ///
+ internal Settings.ICertificateStorer? WechatCertificateStorer { get; }
+
///
/// 获取当前客户端使用的 JSON 序列化器。
///
@@ -59,6 +64,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
WechatMerchantCertSerialNumber = options.MerchantCertSerialNumber;
WechatMerchantCertPrivateKey = options.MerchantCertPrivateKey;
WechatMerchantV3Secret = options.MerchantV3Secret;
+ WechatCertificateStorer = options.CertificateStorer;
FlurlClient.BaseUrl = options.Endpoints ?? WechatTenpayEndpoints.DEFAULT;
FlurlClient.Headers.Remove("Accept");
diff --git a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs
index a73a442d..3c339680 100644
--- a/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs
+++ b/src/SKIT.FlurlHttpClient.Wechat.TenpayV3/WechatTenpayClientOptions.cs
@@ -34,9 +34,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
///
/// 获取或设置微信支付 API 签名认证方式。
- /// 默认值:
+ /// 默认值:
///
- public string AuthScheme { get; set; } = WechatTenpayAuthSchemes.WECHATPAY2_SHA256_RSA2048;
+ public string AuthScheme { get; set; } = Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048;
///
/// 获取或设置微信商户号。
@@ -57,5 +57,11 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// 获取或设置微信商户 API 证书私钥。
///
public string MerchantCertPrivateKey { get; set; } = default!;
+
+ ///
+ /// 获取或设置微信商户平台证书存储器。
+ /// 默认值:
+ ///
+ public Settings.ICertificateStorer? CertificateStorer { get; set; } = new Settings.InMemoryCertificateStorer();
}
}