c# .net 微信支付v3,支付结果通知处理;含AEAD_AES_256_GCM解码
作者的程序框架:.NET Framework 4.6.1
微信官方说明地址:
注意重点:
注意:
• 同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。 推荐的做法是,当商户系统收到通知进行处理时,先检查对应业务数据的状态,并判断该通知是否已经处理。如果未处理,则再进行处理;如果已处理,则直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。
• 如果在所有通知频率后没有收到微信侧回调,商户应调用查询订单接口确认订单状态。
特别提醒:商户系统对于开启结果通知的内容一定要做签名验证,并校验通知的信息是否与商户侧的信息一致,防止数据泄露导致出现“假通知”,造成资金损失。
参数解密
下面详细描述对通知数据进行解密的流程:
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个字节并可能为空字符串。
证书和回调报文解密
位置:文档中心>开发指南>证书和回调报文解密
官方提供的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>标签:AES,string,get,c#,通知,支付,new,data,public From: https://blog.51cto.com/cplvfx/5928914
/// 订单回调地址
/// </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);
}