前言
由于最近在接了个项目做微信支付,特将自认为比较麻烦支付回调 .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();
}