mirror of
https://gitee.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git
synced 2025-07-15 05:13:17 +08:00
feat(tenpayv3): 重新实现 CertificateManager,支持记录生效时间、过期时间等信息
This commit is contained in:
parent
70d05e3aaa
commit
bfa6557314
@ -50,9 +50,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5.Services.BackgroundSe
|
||||
if (response.IsSuccessful())
|
||||
{
|
||||
client.DecryptResponseEncryptedData(ref response);
|
||||
foreach (var cert in response.CertificateList)
|
||||
foreach (var certificateModel in response.CertificateList)
|
||||
{
|
||||
_certificateManager.SetCertificate(cert.SerialNumber, cert.EncryptCertificate.CipherText);
|
||||
_certificateManager.AddEntry(new CertificateEntry(certificateModel));
|
||||
}
|
||||
|
||||
_logger.LogInformation("刷新微信商户平台证书成功。");
|
||||
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Exceptions
|
||||
{
|
||||
public class WechatTenpayEventVerificationException : WechatTenpayException
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
internal WechatTenpayEventVerificationException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal WechatTenpayEventVerificationException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal WechatTenpayEventVerificationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Exceptions
|
||||
{
|
||||
public class WechatTenpayResponseVerificationException : WechatTenpayException
|
||||
{
|
||||
/// <inheritdoc/>
|
||||
internal WechatTenpayResponseVerificationException()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal WechatTenpayResponseVerificationException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
internal WechatTenpayResponseVerificationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,32 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
||||
string callbackBody,
|
||||
string callbackSignature,
|
||||
string callbackSerialNumber)
|
||||
{
|
||||
return VerifyEventSignature(client, callbackTimestamp, callbackNonce, callbackBody, callbackSignature, callbackSerialNumber, out _);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
/// <param name="error"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static bool VerifyEventSignature(
|
||||
this WechatTenpayClient client,
|
||||
string callbackTimestamp,
|
||||
string callbackNonce,
|
||||
string callbackBody,
|
||||
string callbackSignature,
|
||||
string callbackSerialNumber,
|
||||
out Exception? error)
|
||||
{
|
||||
if (client == null) throw new ArgumentNullException(nameof(client));
|
||||
if (callbackTimestamp == null) throw new ArgumentNullException(nameof(callbackTimestamp));
|
||||
@ -39,18 +65,28 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
||||
{
|
||||
try
|
||||
{
|
||||
string certificate = client.CertificateManager.GetCertificate(callbackSerialNumber)!;
|
||||
string publicKey = Utilities.RSAUtility.ExportPublicKey(certificate);
|
||||
var cert = client.CertificateManager.GetEntry(callbackSerialNumber)!;
|
||||
if (!cert.HasValue)
|
||||
{
|
||||
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed, because there is no platform certificate matched the serial number.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return Utilities.RSAUtility.VerifyWithSHA256(
|
||||
publicKey: publicKey,
|
||||
error = null;
|
||||
return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
|
||||
certificate: cert.Value.Certificate,
|
||||
plainText: GetPlainTextForSignature(timestamp: callbackTimestamp, nonce: callbackNonce, body: callbackBody),
|
||||
signature: callbackSignature
|
||||
);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed. Please see the `InnerException` for more details.", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
error = new Exceptions.WechatTenpayEventVerificationException("Verify signature of event failed, because there is no platform certificate in the manager.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -19,6 +19,23 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
||||
/// <returns></returns>
|
||||
public static bool VerifyResponseSignature<TResponse>(this WechatTenpayClient client, TResponse response)
|
||||
where TResponse : WechatTenpayResponse
|
||||
{
|
||||
return VerifyResponseSignature(client, response, out _);
|
||||
}
|
||||
|
||||
/// <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="error"></param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="ArgumentNullException"></exception>
|
||||
public static bool VerifyResponseSignature<TResponse>(this WechatTenpayClient client, TResponse response, out Exception? error)
|
||||
where TResponse : WechatTenpayResponse
|
||||
{
|
||||
if (client == null) throw new ArgumentNullException(nameof(client));
|
||||
if (response == null) throw new ArgumentNullException(nameof(response));
|
||||
@ -27,18 +44,28 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
|
||||
{
|
||||
try
|
||||
{
|
||||
string certificate = client.CertificateManager.GetCertificate(response.WechatpayCertSerialNumber)!;
|
||||
string publicKey = Utilities.RSAUtility.ExportPublicKey(certificate);
|
||||
var cert = client.CertificateManager.GetEntry(response.WechatpayCertSerialNumber)!;
|
||||
if (!cert.HasValue)
|
||||
{
|
||||
error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed, because there is no platform certificate matched the serial number.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return Utilities.RSAUtility.VerifyWithSHA256(
|
||||
publicKey: publicKey,
|
||||
error = null;
|
||||
return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
|
||||
certificate: cert.Value.Certificate,
|
||||
plainText: GetPlainTextForSignature(response),
|
||||
signature: response.WechatpaySignature
|
||||
);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed. Please see the `InnerException` for more details.", ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
error = new Exceptions.WechatTenpayResponseVerificationException("Verify signature of response failed, because there is no platform certificate in the manager.");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,85 @@
|
||||
using System;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
|
||||
{
|
||||
/// <summary>
|
||||
/// 表示一个微信商户平台证书实体。
|
||||
/// </summary>
|
||||
public struct CertificateEntry : IEquatable<CertificateEntry>
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取证书序列号。
|
||||
/// <para>序列号相同的实体将被视为同一个证书。</para>
|
||||
/// </summary>
|
||||
public string SerialNumber { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取证书内容(CER 格式,即 -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE)
|
||||
/// </summary>
|
||||
public string Certificate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取生效时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset EffectiveTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取过期时间。
|
||||
/// </summary>
|
||||
public DateTimeOffset ExpireTime { get; }
|
||||
|
||||
public CertificateEntry(string serialNumber, string certificate, DateTimeOffset effectiveTime, DateTimeOffset expireTime)
|
||||
{
|
||||
if (string.IsNullOrEmpty(serialNumber))
|
||||
throw new ArgumentException("The value of `serialNumber` can not be empty.", nameof(serialNumber));
|
||||
if (string.IsNullOrEmpty(certificate))
|
||||
throw new ArgumentException("The value of `certificate` can not be empty.", nameof(serialNumber));
|
||||
if (!certificate.Trim().StartsWith("-----BEGIN CERTIFICATE-----") || !certificate.Trim().EndsWith("-----END CERTIFICATE-----"))
|
||||
throw new ArgumentException("The value of `certificate` is an invalid .cer file content.", nameof(serialNumber));
|
||||
|
||||
SerialNumber = serialNumber;
|
||||
Certificate = certificate;
|
||||
EffectiveTime = effectiveTime;
|
||||
ExpireTime = expireTime;
|
||||
}
|
||||
|
||||
public CertificateEntry(Models.QueryCertificatesResponse.Types.Certificate cert)
|
||||
: this(cert.SerialNumber, cert.EncryptCertificate.CipherText, cert.EffectiveTime, cert.ExpireTime)
|
||||
{
|
||||
}
|
||||
|
||||
public bool IsAvailable()
|
||||
{
|
||||
DateTimeOffset now = DateTimeOffset.Now;
|
||||
return EffectiveTime <= now && now < ExpireTime;
|
||||
}
|
||||
|
||||
public bool Equals(CertificateEntry other)
|
||||
{
|
||||
return string.Equals(SerialNumber, other.SerialNumber);
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (ReferenceEquals(null, obj))
|
||||
return false;
|
||||
|
||||
return (obj is CertificateEntry other) && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return SerialNumber.GetHashCode();
|
||||
}
|
||||
|
||||
public static bool operator ==(CertificateEntry left, CertificateEntry right)
|
||||
{
|
||||
return left.Equals(right);
|
||||
}
|
||||
|
||||
public static bool operator !=(CertificateEntry left, CertificateEntry right)
|
||||
{
|
||||
return !left.Equals(right);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
|
||||
@ -10,18 +9,30 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
|
||||
public abstract class CertificateManager
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据证书序列号获取证书(cer 格式)。
|
||||
/// 获取存储的全部证书。
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public abstract IEnumerable<CertificateEntry> AllEntries();
|
||||
|
||||
/// <summary>
|
||||
/// 添加一个证书实体。
|
||||
/// </summary>
|
||||
/// <param name="entry"></param>
|
||||
public abstract void AddEntry(CertificateEntry entry);
|
||||
|
||||
/// <summary>
|
||||
/// 根据证书序列号获取证书实体。
|
||||
/// </summary>
|
||||
/// <param name="serialNumber"></param>
|
||||
/// <returns></returns>
|
||||
public abstract string? GetCertificate(string serialNumber);
|
||||
public abstract CertificateEntry? GetEntry(string serialNumber);
|
||||
|
||||
/// <summary>
|
||||
/// 设置证书序列号与证书(cer 格式)的映射关系。
|
||||
/// 移除指定的证书实体。
|
||||
/// </summary>
|
||||
/// <param name="serialNumber"></param>
|
||||
/// <param name="certificate"></param>
|
||||
public abstract void SetCertificate(string serialNumber, string certificate);
|
||||
/// <returns></returns>
|
||||
public abstract bool RemoveEntry(string serialNumber);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -29,26 +40,40 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
|
||||
/// </summary>
|
||||
public class InMemoryCertificateManager : CertificateManager
|
||||
{
|
||||
private readonly IDictionary<string, string> _dict;
|
||||
private readonly ConcurrentDictionary<string, CertificateEntry> _dict;
|
||||
|
||||
public InMemoryCertificateManager()
|
||||
{
|
||||
_dict = new ConcurrentDictionary<string, string>();
|
||||
_dict = new ConcurrentDictionary<string, CertificateEntry>();
|
||||
}
|
||||
|
||||
public override string? GetCertificate(string serialNumber)
|
||||
public override IEnumerable<CertificateEntry> AllEntries()
|
||||
{
|
||||
if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber));
|
||||
|
||||
return _dict[serialNumber];
|
||||
return _dict.Values;
|
||||
}
|
||||
|
||||
public override void SetCertificate(string serialNumber, string certificate)
|
||||
public override void AddEntry(CertificateEntry entry)
|
||||
{
|
||||
if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber));
|
||||
if (certificate == null) throw new ArgumentNullException(nameof(certificate));
|
||||
_dict.TryRemove(entry.SerialNumber, out _);
|
||||
_dict.TryAdd(entry.SerialNumber, entry);
|
||||
}
|
||||
|
||||
_dict[serialNumber] = certificate;
|
||||
public override CertificateEntry? GetEntry(string serialNumber)
|
||||
{
|
||||
if (_dict.TryGetValue(serialNumber, out var entry))
|
||||
{
|
||||
if (entry.IsAvailable())
|
||||
return entry;
|
||||
|
||||
_dict.TryRemove(serialNumber, out _);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public override bool RemoveEntry(string serialNumber)
|
||||
{
|
||||
return _dict.TryRemove(serialNumber, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Org.BouncyCastle.Crypto;
|
||||
using Org.BouncyCastle.Crypto.Engines;
|
||||
using Org.BouncyCastle.Crypto.Parameters;
|
||||
using Org.BouncyCastle.OpenSsl;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
@ -24,7 +24,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
|
||||
TestClients.Instance.DecryptResponseEncryptedData(ref response);
|
||||
foreach (var certificateModel in response.CertificateList)
|
||||
{
|
||||
TestClients.GlobalCertificateManager.SetCertificate(certificateModel.SerialNumber, certificateModel.EncryptCertificate.CipherText);
|
||||
TestClients.GlobalCertificateManager.AddEntry(new Settings.CertificateEntry(certificateModel));
|
||||
}
|
||||
|
||||
Assert.True(TestClients.Instance.VerifyResponseSignature(response));
|
||||
|
Loading…
Reference in New Issue
Block a user