当前位置: 首页 > 工具软件 > wechat4u.js > 使用案例 >

c# 微信支付API V3支付回调签名验证

燕和同
2023-12-01

微信支付官方文档对于签名验证的介绍:微信支付-开发者文档 (qq.com)

微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。

参数说明

一个支付回调的示例:

Headers:

 [{"Key":"Connection","Value":["Keep-Alive"]},{"Key":"Pragma","Value":["no-cache"]},{"Key":"Accept","Value":["*/*"]},{"Key":"Host","Value":["www.datatook.com"]},{"Key":"User-Agent","Value":["Mozilla/4.0"]},{"Key":"Wechatpay-Nonce","Value":["lL0M9v8R3WtTa5iusxEWgjXgjbyQeP3D"]},{"Key":"Wechatpay-Serial","Value":["4B771705B6FFCA007AAE05A3512E4EA923BF757E"]},{"Key":"Wechatpay-Signature","Value":["ImfeCH3vkB/iQ+nl+JSBuzFdx5aZf3V9+CAyqSww/xeavzcTGtqCO+bixh9Lylk0ZZ2/UJOfJd/KWL4BCqw+GMumAHVWMzb34a0kAdHimVuUt36dNz2ZI58mM/1RN75AMYuNcCptXPTgHzdGBl3+bE2VO+dnoK5q/X+lskFDcxb0LGvKVA7V/bVA0edhSBCaUeiEogajXVXDmHcaHk/4/fUmDD2NCJwjVh0CdynC5p6IQN+mkyITwlJQgASgN6+truAGFGnN50FunTVPXGLze/VbFbZg87flawWce+/KpmFqfSvhxyUOQ/axPMvZkSvMxIWCp7Y0kw6MfMXMCCP5Ow=="]},{"Key":"Wechatpay-Timestamp","Value":["1622016489"]}]

Body:

 {"id":"ebd1442a-156f-5e1f-8ca4-d43bf679a603","create_time":"2021-05-26T16:08:09+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"vocEJn2O7MWb9d21Zk48EiHuZfQMapbAjgHPPsvYisfHkZKs4jF15joWbrKb37HYbDuYVYgPoUta4u3i2c1YrcA9VxGWrO043dUO3Nj2QHURCEnIE9j+3G7iY+3PPYiJTI3M1w5vRw6+BDNn0atcLr5GuOy0ajvxpr7bu3GJa6fjuO23pbWw/rEfALaH3igl1gQeeSm+zkVmNeA5uXFrlmX8gMQkADDs+YVIKC84rBslyJZfhvX9rqpI+zPGtXJPs9lRM4qsbtVsqWMK+nEnB7wfj9qTa3fHh/lBtNXzCB29tlodzBPKQS+Pxa9GCMtf3HeIf8YognqF496eqq3kPlGEcF3BdqVu1VqzdW1e+9qzPu68GfexTkml8GliCdFdaO7eq9BUOoebulOLq/p58ZxORxoeaCKR3NfItmSVyHIPbS3wlulJgUDWFo0Umt4hkSmxRp4s+4P40Pc+w/tfAkQsQUYln7V1c1XR8IpWrQn22YGIOKGqB86Fs8Bw4F8S7QzKVhCbD075AoiRhGciXpFD6XNYiMF0gk7gB6eUooZj3aU1bkJzzmf29xUJktcK6G/IeLipE/KzKVeOJJ9t/bqjJRtbAqfEUnTsf7xgwZ2MjQ9gnFn4XG1TELypAyJnajnXfjBcwmEU/3oMxKDw35w9Oc+5V501JFmN/58iKSWYzFK9e48C6iRgZ5cOuU0ImAyF2rjF0KhofJwy7gDV19SteyJkzH+m+116ZiEa8iki","associated_data":"transaction","nonce":"C8EMTkPpAvH5"}}

在回调签名验证中需要用到几个参数:

serialNo:平台证书(公钥)序列号,验证签名需要用到平台公钥 PublicKey,通过 获取平台证书列表(https://api.mch.weixin.qq.com/v3/certificates) 接口获取,PublicKey 下载下来后保存在自己的服务器中和 serialNo 是一对,通过 serialNo 找到公钥,如果和保存的公钥 serialNo 不匹配,代表微信平台证书已经更新,需要重新下载平台公钥。在 Header 中获得,参数为:Wechatpay-Serial 示例中值为 4B771705B6FFCA007AAE05A3512E4EA923BF757E

timestamp:签名时间戳,在 Header 中获得参数为 Wechatpay-Timestamp  示例中值为 1622016489

nonce:签名随机字符串,在 Header 中获得,参数为 Wechatpay-Nonce 示例中值为 lL0M9v8R3WtTa5iusxEWgjXgjbyQeP3D

signature:签名,在 Header 中获得,参数为 Wechatpay-Signature 示例中值为 ImfeCH3vkB/iQ+nl+JSBuzFdx5aZf3V9+CAyqSww/xeavzcTGtqCO+bixh9Lylk0ZZ2/UJOfJd/KWL4BCqw+GMumAHVWMzb34a0kAdHimVuUt36dNz2ZI58mM/1RN75AMYuNcCptXPTgHzdGBl3+bE2VO+dnoK5q/X+lskFDcxb0LGvKVA7V/bVA0edhSBCaUeiEogajXVXDmHcaHk/4/fUmDD2NCJwjVh0CdynC5p6IQN+mkyITwlJQgASgN6+truAGFGnN50FunTVPXGLze/VbFbZg87flawWce+/KpmFqfSvhxyUOQ/axPMvZkSvMxIWCp7Y0kw6MfMXMCCP5Ow==

body:post请求的主体 content

// 平台证书序列号
string serialNo = Request.Headers.GetValues("Wechatpay-Serial").FirstOrDefault();

// 签名时间戳
string timestamp = Request.Headers.GetValues("Wechatpay-Timestamp").FirstOrDefault();

// 签名随机字符串
string nonce = Request.Headers.GetValues("Wechatpay-Nonce").FirstOrDefault();

// 验签值
string signature = Request.Headers.GetValues("Wechatpay-Signature").FirstOrDefault();

Request.Content.ReadAsStreamAsync().Result.Seek(0, System.IO.SeekOrigin.Begin);
// POST请求主体
string body = Request.Content.ReadAsStringAsync().Result;

签名验证

1. 需要构建待签名字符串 

签名时间戳\n

签名随机串\n

Post报文主体\n

代码:

string message = $"{timestamp}\n{nonce}\n{body}\n";

示例中结果是:

1622016489\n

lL0M9v8R3WtTa5iusxEWgjXgjbyQeP3D\n

{"id":"ebd1442a-156f-5e1f-8ca4-d43bf679a603","create_time":"2021-05-26T16:08:09+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"vocEJn2O7MWb9d21Zk48EiHuZfQMapbAjgHPPsvYisfHkZKs4jF15joWbrKb37HYbDuYVYgPoUta4u3i2c1YrcA9VxGWrO043dUO3Nj2QHURCEnIE9j+3G7iY+3PPYiJTI3M1w5vRw6+BDNn0atcLr5GuOy0ajvxpr7bu3GJa6fjuO23pbWw/rEfALaH3igl1gQeeSm+zkVmNeA5uXFrlmX8gMQkADDs+YVIKC84rBslyJZfhvX9rqpI+zPGtXJPs9lRM4qsbtVsqWMK+nEnB7wfj9qTa3fHh/lBtNXzCB29tlodzBPKQS+Pxa9GCMtf3HeIf8YognqF496eqq3kPlGEcF3BdqVu1VqzdW1e+9qzPu68GfexTkml8GliCdFdaO7eq9BUOoebulOLq/p58ZxORxoeaCKR3NfItmSVyHIPbS3wlulJgUDWFo0Umt4hkSmxRp4s+4P40Pc+w/tfAkQsQUYln7V1c1XR8IpWrQn22YGIOKGqB86Fs8Bw4F8S7QzKVhCbD075AoiRhGciXpFD6XNYiMF0gk7gB6eUooZj3aU1bkJzzmf29xUJktcK6G/IeLipE/KzKVeOJJ9t/bqjJRtbAqfEUnTsf7xgwZ2MjQ9gnFn4XG1TELypAyJnajnXfjBcwmEU/3oMxKDw35w9Oc+5V501JFmN/58iKSWYzFK9e48C6iRgZ5cOuU0ImAyF2rjF0KhofJwy7gDV19SteyJkzH+m+116ZiEa8iki","associated_data":"transaction","nonce":"C8EMTkPpAvH5"}}\n

2. 校验平台序列号 serialNo 是否一致

对比 Header 中的 平台序列号 ( serialNo ) 和 本地平台公钥 的 serialNo 是否一致

一致直接返回 本地的平台公钥

不一致:调用 获取平台证书列表(https://api.mch.weixin.qq.com/v3/certificates) 接口 获取 新的 平台公钥 和 serialNo , 并 确保 和 Header 中的 serialNo 一致,保存到本地,返回公钥

public class WXCertHelper
{
    List<WXPlatformPublicKey> CacheData { get; set; }

    private WXCertHelper()
    {
        CacheData = new List<WXPlatformPublicKey>();
    }

    static WXCertHelper _intance;

    // 单例模式
    public static WXCertHelper Intance
    {
        get
        {
            if (_intance == null)
            {
                _intance = new WXCertHelper();
                _intance.LoadCacheData();
            }
            return _intance;
        }
    }

    /// <summary>
    /// 从本地加载平台序列号
    /// </summary>
    void LoadCacheData()
    {
        CacheData.Clear();
        // 从数据库加载数据
    }
    /// <summary>
    /// 保存平台公钥到本地
    /// </summary>
    /// <param name="data"></param>
    void SaveCacheData()
    {
        //List<WXPlatformPublicKey> data = this.CacheData;
        // 刷新本地存储
    }

    public string GetPublicKey(string serialNo)
    {
        // 匹配缓存中的公钥
        var obj = CacheData.Find(w => w.serialNo == serialNo);
        if (obj != null)
        {
            return obj.PublicKey;
        }
        // 缓存中没有,重新下载
        else
        {
            this.RefreshDownCert();
            return GetPublicKey(serialNo);
        }
    }

    /// <summary>
    /// 重新从API下载平台公钥
    /// </summary>
    void RefreshDownCert()
    {
        APIHelper apiHelper = new APIHelper();

        CacheData.Clear();
        var result = apiHelper.DownCert();
        foreach (var o in result.data)
        {
            string publicKey = AesGcm.AesGcmDecrypt(o.encrypt_certificate.associated_data, o.encrypt_certificate.nonce, o.encrypt_certificate.ciphertext, ConfigData.Intance.WXPaySetting.wx_aes_key);
            CacheData.Add(new WXPlatformPublicKey()
            {
                serialNo = o.serial_no,
                PublicKey = publicKey
            });
        }
        SaveCacheData();

    }
}

public class WXPlatformPublicKey
{

    /// <summary>
    /// 平台证书序列号
    /// </summary>
    public string serialNo { get; set; }
    /// <summary>
    /// 平台证书公钥
    /// </summary>
    public string PublicKey { get; set; }
}

校验签名

/// <summary>
/// 验证签名
/// </summary>
/// <param name="message">签名字符串</param>
/// <param name="publickey">公钥</param>
/// <param name="signStr">签名</param>
/// <returns></returns>
public bool VerifySign(string message, string publickey, string signStr)
{
    try
    {
        var _publicKeyBytes = Encoding.UTF8.GetBytes(publickey);
        var x509 = new X509Certificate2(_publicKeyBytes);
        using (var rsa = (RSACryptoServiceProvider)x509.PublicKey.Key)
        {
            using (var sha256 = new SHA256CryptoServiceProvider())
            {
                var b = rsa.VerifyData(Encoding.UTF8.GetBytes(message), sha256, Convert.FromBase64String(signStr));
                return b;
            }
        }
    }
    catch { return false; }
}

var _publicKeyBytes = Encoding.UTF8.GetBytes(publickey);
var x509 = new X509Certificate2(_publicKeyBytes);
using (var rsa = (System.Security.Cryptography.RSACng)x509.PublicKey.Key)
{
    
    rsa.VerifyData(Encoding.UTF8.GetBytes(message), Convert.FromBase64String(signStr), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}

 类似资料: