mirror of
https://gitee.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git
synced 2025-07-15 23:13:32 +08:00
feat(tenpayv3): 提供 ICertificateStorer 接口,并基于此重新实现验签的扩展方法
This commit is contained in:
parent
165c25102a
commit
044fcf6d56
@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Constants
|
||||||
|
{
|
||||||
|
public static class SignAlgorithms
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WECHATPAY2-SHA256-RSA2048。
|
||||||
|
/// </summary>
|
||||||
|
public const string WECHATPAY2_SHA256_RSA2048 = "WECHATPAY2-SHA256-RSA2048";
|
||||||
|
}
|
||||||
|
}
|
@ -3,9 +3,9 @@
|
|||||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 为 <see cref="WechatTenpayClient"/> 提供回调通知事件的扩展方法。
|
/// 为 <see cref="WechatTenpayClient"/> 提供回调通知事件敏感数据解密的扩展方法。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class WechatTenpayClientEventExtensions
|
public static class WechatTenpayClientEventDecryptionExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// <para>反序列化得到 <see cref="WechatTenpayEvent"/> 对象。</para>
|
/// <para>反序列化得到 <see cref="WechatTenpayEvent"/> 对象。</para>
|
@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 为 <see cref="WechatTenpayClient"/> 提供回调通知事件签名验证的扩展方法。
|
||||||
|
/// </summary>
|
||||||
|
public static class WechatTenpayClientEventVerificationExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <para>验证回调通知事件签名。</para>
|
||||||
|
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml </para>
|
||||||
|
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse"></typeparam>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="callbackTimestamp">微信回调通知中的 Wechatpay-Timestamp 字段。</param>
|
||||||
|
/// <param name="callbackNonce">微信回调通知中的 Wechatpay-Nonce 字段。</param>
|
||||||
|
/// <param name="callbackBody">微信回调通知中请求正文。</param>
|
||||||
|
/// <param name="callbackSignature">微信回调通知中的 Wechatpay-Signature 字段。</param>
|
||||||
|
/// <param name="callbackSerialNumber">微信回调通知中的 Wechatpay-Serial 字段。</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool VerifyEventSignature<TResponse>(
|
||||||
|
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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
using System;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 为 <see cref="WechatTenpayClient"/> 提供响应签名验证的扩展方法。
|
||||||
|
/// </summary>
|
||||||
|
public static class WechatTenpayClientResponseVerificationExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// <para>验证响应签名。</para>
|
||||||
|
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml </para>
|
||||||
|
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TResponse"></typeparam>
|
||||||
|
/// <param name="client"></param>
|
||||||
|
/// <param name="response"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static bool VerifyResponseSignature<TResponse>(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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,81 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 为 <see cref="WechatTenpayClient"/> 提供响应签名验证的扩展方法。
|
|
||||||
/// </summary>
|
|
||||||
public static class WechatTenpayClientResponseVerifyExtensions
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// <para>验证响应签名(使用微信支付平台证书公钥)。</para>
|
|
||||||
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml </para>
|
|
||||||
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml </para>
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TResponse"></typeparam>
|
|
||||||
/// <param name="client"></param>
|
|
||||||
/// <param name="response"></param>
|
|
||||||
/// <param name="publicKey"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool VerifyResponseSignature<TResponse>(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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// <para>验证响应签名(使用微信支付平台证书)。</para>
|
|
||||||
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml </para>
|
|
||||||
/// <para>REF: https://pay.weixin.qq.com/wiki/doc/apiv3_partner/wechatpay/wechatpay4_1.shtml </para>
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TResponse"></typeparam>
|
|
||||||
/// <param name="client"></param>
|
|
||||||
/// <param name="response"></param>
|
|
||||||
/// <param name="certificate"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public static bool VerifyResponseSignatureByCertificate<TResponse>(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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -53,7 +53,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Interceptors
|
|||||||
|
|
||||||
switch (_scheme)
|
switch (_scheme)
|
||||||
{
|
{
|
||||||
case WechatTenpayAuthSchemes.WECHATPAY2_SHA256_RSA2048:
|
case Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048:
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 微信商户平台证书存储器接口。
|
||||||
|
/// </summary>
|
||||||
|
public interface ICertificateStorer
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据证书序列号获取证书(cer 格式)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serialNumber"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
string? Get(string serialNumber);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置证书序列号与证书(cer 格式)的映射关系。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="serialNumber"></param>
|
||||||
|
/// <param name="certificate"></param>
|
||||||
|
void Set(string serialNumber, string certificate);
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 一个基于内存实现的 <see cref="ICertificateStorer"/>。
|
||||||
|
/// </summary>
|
||||||
|
public class InMemoryCertificateStorer : ICertificateStorer
|
||||||
|
{
|
||||||
|
public IDictionary<string, string> _dict;
|
||||||
|
|
||||||
|
public InMemoryCertificateStorer()
|
||||||
|
{
|
||||||
|
_dict = new ConcurrentDictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 微信支付 API 接口签名认证方式。
|
|
||||||
/// </summary>
|
|
||||||
public static class WechatTenpayAuthSchemes
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// WECHATPAY2-SHA256-RSA2048(默认)。
|
|
||||||
/// </summary>
|
|
||||||
public const string WECHATPAY2_SHA256_RSA2048 = "WECHATPAY2-SHA256-RSA2048";
|
|
||||||
}
|
|
||||||
}
|
|
@ -39,6 +39,11 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal string WechatMerchantV3Secret { get; }
|
internal string WechatMerchantV3Secret { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前客户端使用的微信商户平台证书存储器。
|
||||||
|
/// </summary>
|
||||||
|
internal Settings.ICertificateStorer? WechatCertificateStorer { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取当前客户端使用的 JSON 序列化器。
|
/// 获取当前客户端使用的 JSON 序列化器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -59,6 +64,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
|||||||
WechatMerchantCertSerialNumber = options.MerchantCertSerialNumber;
|
WechatMerchantCertSerialNumber = options.MerchantCertSerialNumber;
|
||||||
WechatMerchantCertPrivateKey = options.MerchantCertPrivateKey;
|
WechatMerchantCertPrivateKey = options.MerchantCertPrivateKey;
|
||||||
WechatMerchantV3Secret = options.MerchantV3Secret;
|
WechatMerchantV3Secret = options.MerchantV3Secret;
|
||||||
|
WechatCertificateStorer = options.CertificateStorer;
|
||||||
|
|
||||||
FlurlClient.BaseUrl = options.Endpoints ?? WechatTenpayEndpoints.DEFAULT;
|
FlurlClient.BaseUrl = options.Endpoints ?? WechatTenpayEndpoints.DEFAULT;
|
||||||
FlurlClient.Headers.Remove("Accept");
|
FlurlClient.Headers.Remove("Accept");
|
||||||
|
@ -34,9 +34,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置微信支付 API 签名认证方式。
|
/// 获取或设置微信支付 API 签名认证方式。
|
||||||
/// <para>默认值:<see cref="WechatTenpayAuthSchemes.WECHATPAY2_SHA256_RSA2048"/></para>
|
/// <para>默认值:<see cref="Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048"/></para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AuthScheme { get; set; } = WechatTenpayAuthSchemes.WECHATPAY2_SHA256_RSA2048;
|
public string AuthScheme { get; set; } = Constants.SignAlgorithms.WECHATPAY2_SHA256_RSA2048;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取或设置微信商户号。
|
/// 获取或设置微信商户号。
|
||||||
@ -57,5 +57,11 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
|||||||
/// 获取或设置微信商户 API 证书私钥。
|
/// 获取或设置微信商户 API 证书私钥。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MerchantCertPrivateKey { get; set; } = default!;
|
public string MerchantCertPrivateKey { get; set; } = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置微信商户平台证书存储器。
|
||||||
|
/// <para>默认值:<see cref="Settings.InMemoryCertificateStorer"/></para>
|
||||||
|
/// </summary>
|
||||||
|
public Settings.ICertificateStorer? CertificateStorer { get; set; } = new Settings.InMemoryCertificateStorer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user