zl程序教程

您现在的位置是:首页 >  后端

当前栏目

c# .net 微信支付v3,支付结果通知处理;含AEAD_AES_256_GCM解码

c#Net微信 处理 结果 支付 通知 解码
2023-09-11 14:14:50 时间

 c# .net 微信支付v3,支付结果通知处理;含AEAD_AES_256_GCM解码

作者的程序框架:.NET Framework 4.6.1

微信官方说明地址:

 注意重点:

微信支付-开发者文档

https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_5.shtml

注意:
• 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

• 如果在所有通知频率后没有收到微信侧回调,商户应调用查询订单接口确认订单状态。


特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。

参数解密

下面详细描述对通知数据进行解密的流程:

1、用商户平台上设置的APIv3密钥【微信商户平台—>账户设置—>API安全—>设置APIv3密钥】,记为key;
2、针对resource.algorithm中描述的算法(目前为AEAD_AES_256_GCM),取得对应的参数nonce和associated_data;
3、使用key、nonce和associated_data,对数据密文resource.ciphertext进行解密,得到JSON形式的资源对象;

注: AEAD_AES_256_GCM算法的接口细节,请参考rfc5116。微信支付使用的密钥key长度为32个字节,随机串nonce长度12个字节,associated_data长度小于16个字节并可能为空字符串。

证书和回调报文解密

微信支付-开发者文档

https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml

位置:文档中心>开发指南>证书和回调报文解密

官方提供的Net代码

public class AesGcm
{
    private static string ALGORITHM = "AES/GCM/NoPadding";
    private static int TAG_LENGTH_BIT = 128;
    private static int NONCE_LENGTH_BYTE = 12;
    private static string AES_KEY = "yourkeyhere";

    public static string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
    {
        GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
        AeadParameters aeadParameters = new AeadParameters(
            new KeyParameter(Encoding.UTF8.GetBytes(AES_KEY)), 
            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);
    }
}

这里有个坑,如果你直接用是会报错的

所以需要引用一个包

给项目添加引用包

项目》引用》右键“NuGet”》搜索“Portable.BouncyCastle”》找到如图的包安装即可

 

我安装版本数据

描述:BouncyCastle portable version with support for .NET 4, .NET Standard 2.0

版本:1.9.0

作者:Claire Novotny

发布日期:2021年10月19日 (2021/10/19)

项目URL:https://www.bouncycastle.org/csharp/

完整代码

AEAD_AES_256_GCM 解码工具类

using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace test1.WeiXinPay.Tools
{
    /// <summary>
    /// 解密微信通知结果帮助类
    /// 参考资料:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml
    /// .NET5环境使用该代码,需要安装Portable.BouncyCastle组件
    /// </summary>
    public class AesGcmHelper
    {
        private static string ALGORITHM = "AES/GCM/NoPadding";
        private static int TAG_LENGTH_BIT = 128;
        private static int NONCE_LENGTH_BYTE = 12;
        private static string AES_KEY = string.Empty;

        /// <summary>
        ///  解密微信通知结果帮助类
        /// </summary>
        /// <param name="associatedData">通知数据resource下的associated_data</param>
        /// <param name="nonce">通知数据resource.nonce</param>
        /// <param name="ciphertext">通知数据resource.ciphertext</param>
        /// <param name="APIV3Key">APIV3的密钥key</param>
        /// <returns></returns>
        public static string AesGcmDecrypt(string associatedData, string nonce, string ciphertext, string APIV3Key)
        {
            GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
            AeadParameters aeadParameters = new AeadParameters(
                new KeyParameter(Encoding.UTF8.GetBytes(APIV3Key)),
                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>
    public class NotifyUrlModel
    {
        /// <summary>
        /// 通知的唯一ID
        /// </summary>
        public string id { get; set; }
        /// <summary>
        /// 通知创建的时间,遵循rfc3339标准格式,
        /// </summary>
        public string create_time { get; set; }
        /// <summary>
        /// 通知的资源数据类型,支付成功通知为encrypt-resource
        /// </summary>
        public string resource_type { get; set; }
        /// <summary>
        /// 通知的类型,支付成功通知的类型为TRANSACTION.SUCCESS
        /// </summary>
        public string event_type { get; set; }
        /// <summary>
        /// 回调摘要:如:支付成功
        /// </summary>
        public string summary { get; set; }
        /// <summary>
        /// 通知资源数据 json格式,
        /// </summary>
        public ResourceModel resource { get; set; }
    }

    /// <summary>
    /// 通知资源数据 json格式,
    /// </summary>
    public class ResourceModel
    {
        /// <summary>
        /// 原始回调类型,为transaction
        /// </summary>
        public string original_type { get; set; }
        /// <summary>
        /// 对开启结果数据进行加密的加密算法,目前只支持AEAD_AES_256_GCM
        /// </summary>
        public string algorithm { get; set; }
        /// <summary>
        /// Base64编码后的开启/停用结果数据密文
        /// </summary>
        public string ciphertext { get; set; }
        /// <summary>
        /// 附加数据
        /// </summary>
        public string associated_data { get; set; }
        /// <summary>
        /// 加密使用的随机串
        /// </summary>
        public string nonce { get; set; }
    }

回调页面-业务代码

 /// <summary>
        /// 订单回调地址
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public JsonResult NotifyUrl()
        {
            var result = new { code= "SUCCESS" , message = "成功" };
            var httpContext= HttpContext;
            var request = Request;


            try
            {
                #region 获取字符串流 
                System.IO.Stream s = request.InputStream;
                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.Flush();
                s.Close();
                s.Dispose();
                var str = Encoding.UTF8.GetString(buffer);
                #endregion


                #region 获取请求头信息
                StringBuilder RHeadersStr = new StringBuilder();
                RHeadersStr.Append("{");
                foreach (var item in request.Headers)
                {
                    RHeadersStr.Append($"\"{item.ToString()}\":\"{request.Headers[item.ToString()]}\",");
                }
                RHeadersStr.Remove((RHeadersStr.Length - 1), 1);
                RHeadersStr.Append("}");  
                #endregion

                #region 把字符流和请求头的信息写入日志
                log.Info($@"
                获取字符串流:{str.Trim('\0')}\r\n
                获取请求头信息:{RHeadersStr.ToString().Trim()}\r\n
                ==================================================\r\n
                ");
                #endregion

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

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

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

                string APIV3Key = "XXXXXadsadsadasddddAPIV3Key";

                //获取解码数据
                var decryptStr = AesGcmHelper.AesGcmDecrypt(notifyUrlModel.resource.associated_data, notifyUrlModel.resource.nonce, notifyUrlModel.resource.ciphertext, APIV3Key);


            }
            catch (Exception e)
            {
                log.Error(e.LogErrorTxt());
                throw;
            }




            return Json(result);
        }