feat(tenpayv3): 重新实现 CertificateManager,支持记录生效时间、过期时间等信息

This commit is contained in:
Fu Diwei 2021-11-25 11:58:06 +08:00
parent 70d05e3aaa
commit bfa6557314
9 changed files with 251 additions and 31 deletions

View File

@ -50,9 +50,9 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Sample_Net5.Services.BackgroundSe
if (response.IsSuccessful()) if (response.IsSuccessful())
{ {
client.DecryptResponseEncryptedData(ref response); 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("刷新微信商户平台证书成功。"); _logger.LogInformation("刷新微信商户平台证书成功。");

View File

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

View File

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

View File

@ -27,6 +27,32 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
string callbackBody, string callbackBody,
string callbackSignature, string callbackSignature,
string callbackSerialNumber) 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 (client == null) throw new ArgumentNullException(nameof(client));
if (callbackTimestamp == null) throw new ArgumentNullException(nameof(callbackTimestamp)); if (callbackTimestamp == null) throw new ArgumentNullException(nameof(callbackTimestamp));
@ -39,18 +65,28 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{ {
try try
{ {
string certificate = client.CertificateManager.GetCertificate(callbackSerialNumber)!; var cert = client.CertificateManager.GetEntry(callbackSerialNumber)!;
string publicKey = Utilities.RSAUtility.ExportPublicKey(certificate); 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( error = null;
publicKey: publicKey, return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
certificate: cert.Value.Certificate,
plainText: GetPlainTextForSignature(timestamp: callbackTimestamp, nonce: callbackNonce, body: callbackBody), plainText: GetPlainTextForSignature(timestamp: callbackTimestamp, nonce: callbackNonce, body: callbackBody),
signature: callbackSignature 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; return false;
} }

View File

@ -19,6 +19,23 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
/// <returns></returns> /// <returns></returns>
public static bool VerifyResponseSignature<TResponse>(this WechatTenpayClient client, TResponse response) public static bool VerifyResponseSignature<TResponse>(this WechatTenpayClient client, TResponse response)
where TResponse : WechatTenpayResponse 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 (client == null) throw new ArgumentNullException(nameof(client));
if (response == null) throw new ArgumentNullException(nameof(response)); if (response == null) throw new ArgumentNullException(nameof(response));
@ -27,18 +44,28 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3
{ {
try try
{ {
string certificate = client.CertificateManager.GetCertificate(response.WechatpayCertSerialNumber)!; var cert = client.CertificateManager.GetEntry(response.WechatpayCertSerialNumber)!;
string publicKey = Utilities.RSAUtility.ExportPublicKey(certificate); 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( error = null;
publicKey: publicKey, return Utilities.RSAUtility.VerifyWithSHA256ByCertificate(
certificate: cert.Value.Certificate,
plainText: GetPlainTextForSignature(response), plainText: GetPlainTextForSignature(response),
signature: response.WechatpaySignature 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; return false;
} }

View File

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

View File

@ -1,5 +1,4 @@
using System; using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
@ -10,18 +9,30 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
public abstract class CertificateManager public abstract class CertificateManager
{ {
/// <summary> /// <summary>
/// 根据证书序列号获取证书cer 格式)。 /// 获取存储的全部证书。
/// </summary>
/// <returns></returns>
public abstract IEnumerable<CertificateEntry> AllEntries();
/// <summary>
/// 添加一个证书实体。
/// </summary>
/// <param name="entry"></param>
public abstract void AddEntry(CertificateEntry entry);
/// <summary>
/// 根据证书序列号获取证书实体。
/// </summary> /// </summary>
/// <param name="serialNumber"></param> /// <param name="serialNumber"></param>
/// <returns></returns> /// <returns></returns>
public abstract string? GetCertificate(string serialNumber); public abstract CertificateEntry? GetEntry(string serialNumber);
/// <summary> /// <summary>
/// 设置证书序列号与证书cer 格式)的映射关系。 /// 移除指定的证书实体
/// </summary> /// </summary>
/// <param name="serialNumber"></param> /// <param name="serialNumber"></param>
/// <param name="certificate"></param> /// <returns></returns>
public abstract void SetCertificate(string serialNumber, string certificate); public abstract bool RemoveEntry(string serialNumber);
} }
/// <summary> /// <summary>
@ -29,26 +40,40 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.Settings
/// </summary> /// </summary>
public class InMemoryCertificateManager : CertificateManager public class InMemoryCertificateManager : CertificateManager
{ {
private readonly IDictionary<string, string> _dict; private readonly ConcurrentDictionary<string, CertificateEntry> _dict;
public InMemoryCertificateManager() 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.Values;
return _dict[serialNumber];
} }
public override void SetCertificate(string serialNumber, string certificate) public override void AddEntry(CertificateEntry entry)
{ {
if (serialNumber == null) throw new ArgumentNullException(nameof(serialNumber)); _dict.TryRemove(entry.SerialNumber, out _);
if (certificate == null) throw new ArgumentNullException(nameof(certificate)); _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 _);
} }
} }
} }

View File

@ -3,7 +3,6 @@ using System.IO;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using Org.BouncyCastle.Crypto; using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.OpenSsl; using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security; using Org.BouncyCastle.Security;

View File

@ -24,7 +24,7 @@ namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
TestClients.Instance.DecryptResponseEncryptedData(ref response); TestClients.Instance.DecryptResponseEncryptedData(ref response);
foreach (var certificateModel in response.CertificateList) 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)); Assert.True(TestClients.Instance.VerifyResponseSignature(response));