.net core 微信支付API V3支付成功回调通知使用

前言

        由于最近在接了个项目做微信支付,特将自认为比较麻烦支付回调 .net 版本记录下来,以备不时之需。

①、首先第一步,从http请求头和body请求体中获取到我们需要的数据信息

②、验证请求头中的Timestamp,与当前是否相差超过五分钟(微信官方文档建议  我们建议商户系统允许最多5分钟的时间偏差。如果时间戳与当前时间的偏差超过5分钟,您应拒绝处理当前的响应或回调通知。)

③、获取平台证书且解密,理论上此步骤在官方文档中介绍是 对比 Header 中的 平台序列号 ( serialNo ) 和 本地平台公钥 的 serialNo 是否一致,一致:直接返回 本地的平台公钥,不一致:调用 获取平台证书列表,本次使用每次均获取接口最新的证书

④、根据证书序列号,进行RSA解密签名校验证

⑤、解码body请求体中的数据,根据实际业务使用

1、参数示例

回调http请求头中的信息示例
{
	"Wechatpay-Signature": "WECHATPAY/SIGNTEST/LqRrlICoBzSzsWqnjS0ZStk77StI0Z47n7Ju4tHC+D8DSxxWhAgGmWWchKofIuKdg+Xk9G8IRyxyFk17btDTWaHUWn2o5drgj/nASUGhwXXXXXXXXXXXXXuw/tO0DtCFDon3b7yi6fJOuY0h6Qo9wvAvxjj/nSnS27DNvF7w04IIQCKzWP/rte9W4J2SflQ8GAUETb/Ckt2wbC6pNqKPdJ9Ax9pAkY/zhJEwUEbbzndpoJ6y5vsWiKRPQbLlVzuQ+A3BVJPUywiXopDkHFbiwG/nb7zfs+O0CoDW+JIxsHsFyjuxjhq82yM/uKTzGA==",
//验签的签名值
	"Wechatpay-Signature-Type": "WECHATPAY2-SHA256-RSA2048",
	"Wechatpay-Timestamp": "1734955435",//验签的时间戳
	"Wechatpay-Nonce": "KEPLIePcTQ80",//验签的随机字符串
	"Wechatpay-Serial": "XXXXXXXXXXXXXXXXXXXXXXXX"//验签的微信支付平台证书序列号/微信支付公钥ID
}

http中body参数
{
	"id": "10e73751-8512-5fe8-b58d-ab8756f84761",//通知id
	"create_time": "2024-12-23T10:17:06+08:00",
	"resource_type": "encrypt-resource",
	"event_type": "TRANSACTION.SUCCESS",
	"summary": "支付成功",
	"resource": {
		"original_type": "transaction",
		"algorithm": "AEAD_AES_256_GCM",
		"ciphertext": "qBe766HHWnDzTV6/IWSfmQm4dnrtaiq+R8sMZ4vt0SZlEnXnyPHsCOQRBAbHdWUCzJ0Qe7h21T2D5vIkh3LZFYnoaG5X7JeiRre6EUVb11hDpuZkbYFa4zDRNPOl6GkoGIm9eo1gbg0q+5oMG2NDd9TPd+x0I2HkKUgjtHqXYNhXbCQFzED/MAz3xhaNQ/FLlq8NPf2FVOgKsgiDkMVJoEK/UZSKavqqomzAoqvUUMML9ehp+a2nr9gTqbsPsDprDBjm+gZXQwl2UF0nWoP1ch93J4kOJN152/p9So7CuqsNauSXra7EO/CRWJsMiMe3XlqYFm3Hxdrg2rM2+oytuncSHnYAclPZcokEe6GDgnUpJbCeB3Akr5PLYQ/OSnegqLgZ/QjeCyvdvSnMFvwFYUJeDTv97XFfVYefZ+uRb5y3nlESXWQ4uzcQu5V6XRXuBoNktk7rpvD5t+Rj08XTwvLA/S9bAwU9HiSBFj4sWtqMZkEOxH9+6N+dXX/tPIgQzeHb8aJxGH0Jy7O3t2MctROwh1JxGCVL0kEYMSfSNJWxPbHZcUpygKnopAbrf/oTtA==",
	//数据密文,解密出主要的数据
		"associated_data": "transaction",
		"nonce": "Z3pvjKKZgXvR"
	}
}

2、用到的nuget包

3、以下是整个回调过程验签解密的示例

/// <summary>
/// 订单回调地址
/// </summary>
/// <returns></returns>
[HttpPost]
public ActionResult NotifyUrl()
{
    var result = new { code = "SUCCESS", message = "成功" };
    var httpContext = HttpContext;
    var request = Request;
    try
    {
        _logger.LogError("NotifyUrl收到请求");

        #region 获取字符串流 
        Stream s = request.Body;
        int count = 0;
        byte[] buffer = new byte[1024];
        StringBuilder builder = new StringBuilder();
        while ((count = s.Read(buffer, 0, 1024)) > 0)
        {
            builder.Append(Encoding.UTF8.GetString(buffer, 0, count));
        }
        s.Close();
        s.Dispose();
        var str = Encoding.UTF8.GetString(buffer);
        #endregion
        _logger.LogInformation($"获取字符串流:{str.Trim('\0')}\r\n" +
            $"==================================================\r\n");
        #region 获取请求头信息

        StringBuilder RHeadersStr = new StringBuilder();
        RHeadersStr.Append("{");
        foreach (var item in request.Headers)
        {
            RHeadersStr.Append($"\"{item.Key.ToString()}\":\"{item.Value}\",");

        }
        RHeadersStr.Remove((RHeadersStr.Length - 1), 1);
        RHeadersStr.Append("}");
        #endregion

        #region 把字符流和请求头的信息写入日志
        _logger.LogInformation($"获取请求头信息:{RHeadersStr.ToString().Trim()} \r\n " +
            $"==================================================\r\n");
        #endregion

        // 获取字符串流
        string contentStr = str.Trim('\0');

        // 获取请求头信息
        string headersStr = RHeadersStr.ToString().Trim();

        PaymentNoticeRoot notifyUrlModel = JsonConvert.DeserializeObject<PaymentNoticeRoot>(contentStr);
        JObject headers = JsonConvert.DeserializeObject<JObject>(headersStr);

        #region 签名时间戳验证
        // 签名时间戳
        long timestamp = long.Parse(headers["Wechatpay-Timestamp"].ToString());
        long timestamp2 = _weChatServer.GetUnixTimestampInSeconds(); // 第二个时间戳
        // 计算两个时间戳的差值
        long timeDiffInSeconds = Math.Abs(timestamp2 - timestamp);
        // 将秒数转换为分钟
        int minutesDiff = (int)(timeDiffInSeconds / 60);
        //记得打开!!!
        if (minutesDiff >= wechatPayTimeOut)
        {
            result = new { code = "FAIL", message = "时间戳已过期" };
            return Content(result.ToJson());
        }

        #endregion

        // 平台证书序列号
        string serial = headers["Wechatpay-Serial"].ToString();
        // 签名随机字符串
        string nonce = headers["Wechatpay-Nonce"].ToString();
        // 验签值
        string wechatpaySignature = headers["Wechatpay-Signature"].ToString();
        // 将 Wechatpay-Signature 字段值解码为字节数组
        byte[] signatureBytes = Convert.FromBase64String(wechatpaySignature);
        // 将字节数组转换为字符串
        string responseSignature = Encoding.UTF8.GetString(signatureBytes);
        //对比 Header 中的 平台序列号 ( serialNo ) 和 本地平台公钥 的 serialNo 是否一致
        //一致:直接返回 本地的平台公钥
        //不一致:调用 获取平台证书列表(https://api.mch.weixin.qq.com/v3/certificates) 接口 获取 新的 平台公钥 和 serialNo , 并 确保 和 Header 中的 serialNo 一致,保存到本地,返回公钥
        var certArr = _weChatServer.getCertificatesArr();

        #region 解密证书
        List<CertsModel> plainCerts = new List<CertsModel>();
        List<X509Certificate2> x509Certs = new List<X509Certificate2>();
        foreach (var item in certArr.Item2.data)
        {
            CertsModel model = new CertsModel()
            {
                SerialNo = item.serial_no,
                EffectiveTime = item.effective_time,
                ExpireTime = item.expire_time,
                PlainCertificate = AesGcmDecrypt(item.encrypt_certificate.associated_data, item.encrypt_certificate.nonce, item.encrypt_certificate.ciphertext)
            };

            X509Certificate2 x509Cert = new X509Certificate2(Encoding.UTF8.GetBytes(model.PlainCertificate));

            x509Certs.Add(x509Cert);
            plainCerts.Add(model);

        }

        #endregion


        #region 签名校验

        string message = $"{timestamp}\n{nonce}\n{notifyUrlModel.ToJson()}\n";

        var cert = x509Certs.Where(r => r.SerialNumber == serial).FirstOrDefault();  //根据序列号获取证书

        byte[] data = Encoding.UTF8.GetBytes(message);

        var rsaParam = cert.GetRSAPublicKey().ExportParameters(false);
        var rsa = new RSACryptoServiceProvider();
        rsa.ImportParameters(rsaParam);

        var isOk = rsa.VerifyData(data, CryptoConfig.MapNameToOID("SHA256"), Convert.FromBase64String(wechatpaySignature));  //签名校验
        _logger.LogInformation("证书签名结果:" + isOk);
        if (!isOk)
        {
            result = new { code = "FAIL", message = "证书签名校验不通过" };
            return Content(result.ToJson());
        }

        #endregion

        #region 保存证书文件

        SaveCerts(plainCerts);  //保存证书文件

        #endregion


        string APIV3Key = apiv3Secret;

        //获取解码数据
        var decryptStr = AesGcmDecrypt(notifyUrlModel.resource.associated_data, notifyUrlModel.resource.nonce, notifyUrlModel.resource.ciphertext);
        
        var notifyModel = JsonConvert.DeserializeObject<NotifyRoot>(decryptStr);
        if (notifyModel != null) 
        {
            var wxPayRecord = db.Queryable<Wx_PayOrder>().Where(x => x.openId == notifyModel.payer.openid && x.out_trade_no == notifyModel.out_trade_no).First();
            if (wxPayRecord != null) 
            {
                wxPayRecord.transaction_id = notifyModel.transaction_id;
                wxPayRecord.trade_type = notifyModel.trade_type;
                wxPayRecord.trade_state = notifyModel.trade_state;
                wxPayRecord.trade_state_desc = notifyModel.trade_state_desc;
                wxPayRecord.bank_type = notifyModel.bank_type;
                wxPayRecord.success_time = Convert.ToDateTime(notifyModel.success_time);
                wxPayRecord.payTime = Convert.ToDateTime(notifyModel.success_time);
                wxPayRecord.notifyJson = decryptStr;
                if (notifyModel.trade_state== "SUCCESS")
                {
                    wxPayRecord.payState = 1;
                }
                db.Updateable(wxPayRecord).ExecuteCommand();
            }
        }

        _logger.LogInformation("解密数据:" + decryptStr);
    }
    catch (Exception e)
    {
        _logger.LogError("NotifyUrl", e);
        result = new { code = "FAIL", message = e.Message };
        return Content(result.ToJson());
    }
    return Content(result.ToJson());
}

protected void SaveCerts(List<CertsModel> plainCerts)
{
    var info = Directory.CreateDirectory(_outPath);

    foreach (var item in plainCerts)
    {
        string fileFullName = info.FullName + "wechatpay_" + item.SerialNo + ".pem";

        using (var fileStream = new FileStream(fileFullName, FileMode.Create, FileAccess.Write))
        {
            byte[] data = Encoding.ASCII.GetBytes(item.PlainCertificate);

            fileStream.Write(data, 0, data.Length);
        }
    }
}

/// <summary>
/// 证书/回调报文解密
/// </summary>
/// <param name="associatedData"></param>
/// <param name="nonce"></param>
/// <param name="ciphertext"></param>
/// <returns></returns>
protected string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
{
    GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
    AeadParameters aeadParameters = new AeadParameters(
        new KeyParameter(Encoding.UTF8.GetBytes(apiv3Secret)),
        128,
        Encoding.UTF8.GetBytes(nonce),
        Encoding.UTF8.GetBytes(associatedData));
    gcmBlockCipher.Init(false, aeadParameters);

    byte[] data = Convert.FromBase64String(ciphertext);
    byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
    int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0);
    gcmBlockCipher.DoFinal(plaintext, length);
    return Encoding.UTF8.GetString(plaintext);
}


 /// <summary>
 /// 秒级时间戳
 /// </summary>
 /// <returns></returns>
 public long GetUnixTimestampInSeconds()
 {
     DateTimeOffset now = DateTimeOffset.UtcNow;
     return now.ToUnixTimeSeconds();
 }
 /// <summary>
 /// 随机串
 /// </summary>
 /// <param name="length"></param>
 /// <returns></returns>
 public string GenerateRandomString(int length)
 {
     StringBuilder sb = new StringBuilder(length);

     for (int i = 0; i < length; i++)
     {
         sb.Append(chars[random.Next(chars.Length)]);
     }

     return sb.ToString();
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值