feat(tenpayv3): 提供 ICertificateStorer 接口,并基于此重新实现验签的扩展方法

This commit is contained in:
Fu Diwei 2021-07-20 01:20:31 +08:00
parent 165c25102a
commit 044fcf6d56
12 changed files with 246 additions and 101 deletions

View File

@ -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";
}
}

View File

@ -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>

View File

@ -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";
}
}
}

View File

@ -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";
}
}
}

View File

@ -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";
}
}
}

View File

@ -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
{ {

View File

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

View File

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

View File

@ -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";
}
}

View File

@ -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");

View File

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