微信支付回调文档
目录
概述
微信支付 V3 版本回调验签流程:
- 列号)、
Wechatpay-Timestamp(时间戳)、Wechatpay-Nonce(随机数)、Wechatpay-Signature(签名)微信服务器回调时携带 headers:Wechatpay-Serial(证书序 - 根据
Wechatpay-Serial获取对应的微信平台证书公钥 - 使用公钥验签
Wechatpay-Signature - 验签通过后,解密回调数据中的加密内容
- 处理业务逻辑
涉及对象
| 对象 | 说明 |
|---|---|
HomeController |
控制器,接收微信回调请求 |
HomeService |
服务层,处理支付回调业务逻辑 |
WeChatPayNotifyData |
回调通知数据DTO |
h_bd_order |
订单实体 |
h_bd_cmkorderstate |
订单状态实体 |
核心配置
csharp
// 微信支付配置
public readonly static string _appid = "wx1234567890abcdef"; // 小程序AppID
public readonly static string _secret = "1234567890abcdef1234567890abcdef"; // 小程序AppSecret
public readonly static string _mch_id = "1234567890"; // 商户号
public readonly static string serial_no = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; // API证书序列号
public readonly static string _apiV3Key = "1234567890abcdef1234567890abcdef"; // APIv3密钥(32字符)
public readonly static string privateKey = @"-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END PRIVATE KEY-----"; // 商户API私钥(PKCS#8格式)
完整代码
Controller层
csharp
using DocumentFormat.OpenXml.Spreadsheet;
using Hyzx.Cxy.Common;
using Hyzx.Cxy.Common.Attributes;
using Hyzx.Cxy.Common.ConfigOptions;
using Hyzx.Cxy.Common.Core;
using Hyzx.Cxy.Common.Helper;
using Hyzx.Cxy.Entity.DTO;
using Hyzx.Cxy.Entity.DTO.Query;
using Hyzx.Cxy.Entity.VO;
using Hyzx.Cxy.IServices;
using Hyzx.Cxy.IServices.smallprogram;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NPOI.SS.Formula.Functions;
using QRCoder;
using QRCoder.Core;
using SkiaSharp;
using System.Drawing;
using System.Text.Json;
using DescriptionAttribute = System.ComponentModel.DescriptionAttribute;
namespace Hyzx.Cxy.Api.Controllers.smallprogram
{
/// <summary>
/// 小程序首页
/// </summary>
[Route("/api/[controller]/[action]")]
[ApiController]
[ApiExplorerSettings(GroupName = nameof(SwaggerVersionOption.小程序))]
public class HomeController : ControllerBase
{
public IHomeService _homeService;
private readonly IOrderService _orderService;
private readonly string AppID;
private readonly string AppSecret;
private readonly string corpid;
private readonly string corpsecret;
private readonly ILogger<AuthorizationController> _logger;
private readonly ISynchronousService _synchronousService;
public HomeController(IHomeService homeService, IOrderService orderService,
ILogger<AuthorizationController> logger, ISynchronousService synchronousService)
{
_homeService = homeService;
AppID = "wx1234567890abcdef";
AppSecret = "1234567890abcdef1234567890abcdef";
corpid = "ww1234567890abcdef";
corpsecret = "1234567890abcdef1234567890abcdef";
_orderService = orderService;
_logger = logger;
_synchronousService = synchronousService;
}
/// <summary>
/// 支付成功回调
/// </summary>
/// <remarks>
/// 微信支付回调通知
///
/// Headers:
/// - Wechatpay-Timestamp: 时间戳
/// - Wechatpay-Nonce: 随机字符串
/// - Wechatpay-Signature: 签名
/// - Wechatpay-Serial: 证书序列号
/// </remarks>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
[NotAudit]
[Description("支付成功回调")]
public async Task<APIResult> HandleNotify()
{
try
{
// 1. 获取通知头信息
var wechatpayTimestamp = Request.Headers["Wechatpay-Timestamp"];
var wechatpayNonce = Request.Headers["Wechatpay-Nonce"];
var wechatpaySignature = Request.Headers["Wechatpay-Signature"];
var wechatpaySerial = Request.Headers["Wechatpay-Serial"];
// 2. 读取请求体
using var reader = new StreamReader(Request.Body);
var notifyJson = await reader.ReadToEndAsync();
// 重置Body位置,以便后续过滤器可以重新读取
Request.Body.Position = 0;
// 3. 调用Service处理回调
return await _homeService.ProcessPaidOrderAsync(
wechatpayTimestamp,
wechatpayNonce,
wechatpaySignature,
wechatpaySerial,
notifyJson);
}
catch (Exception ex)
{
_logger.LogInformation($"支付回调失败:{ex.Message}");
throw new Exception(ex.Message);
}
}
}
}
Service层
csharp
using Hyzx.Cxy.Common;
using Hyzx.Cxy.Common.Core;
using Hyzx.Cxy.Common.Extensions;
using Hyzx.Cxy.Common.Helper;
using Hyzx.Cxy.Entity.Domain;
using Hyzx.Cxy.Entity.DTO;
using Hyzx.Cxy.IServices.smallprogram;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SqlSugar;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
namespace Hyzx.Cxy.Services.smallprogram
{
/// <summary>
/// 小程序首页服务
/// </summary>
public class HomeService : BaseService<Sysrole>, IHomeService
{
private static readonly Serilog.ILogger Logger = Serilog.Log.ForContext("SourceContext", typeof(HomeService));
#region 微信支付配置
// 小程序ID
public readonly static string _appid = "wx1234567890abcdef";
// 小程序App/Secret
public readonly static string _secret = "1234567890abcdef1234567890abcdef";
// 商户号
public readonly static string _mch_id = "1234567890";
// API证书序列号
public readonly static string serial_no = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
// APIv3密钥(32字节)
public readonly static string _apiV3Key = "1234567890abcdef1234567890abcdef";
// 证书私钥(PKCS#8格式)
public readonly static string privateKey = @"-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
-----END PRIVATE KEY-----";
#endregion
#region 支付回调处理
/// <summary>
/// 支付回调处理
/// </summary>
/// <param name="wechatpayTimestamp">微信支付时间戳</param>
/// <param name="wechatpayNonce">随机字符串</param>
/// <param name="wechatpaySignature">签名</param>
/// <param name="wechatpaySerial">证书序列号</param>
/// <param name="notifyJson">通知JSON</param>
/// <returns></returns>
public async Task<APIResult> ProcessPaidOrderAsync(
StringValues wechatpayTimestamp,
StringValues wechatpayNonce,
StringValues wechatpaySignature,
StringValues wechatpaySerial,
string notifyJson)
{
try
{
// 1. 验证签名
if (!await VerifySignature(
wechatpayTimestamp,
wechatpayNonce,
wechatpaySignature,
wechatpaySerial,
notifyJson))
{
Logger.Error("签名验证失败");
return APIResult.Error("签名验证失败");
}
// 2. 解析并解密数据
using var doc = JsonDocument.Parse(notifyJson);
var resource = doc.RootElement.GetProperty("resource");
var decryptData = DecryptResource(
resource.GetProperty("associated_data").GetString(),
resource.GetProperty("nonce").GetString(),
resource.GetProperty("ciphertext").GetString());
// 3. 处理业务逻辑
var notifyData = System.Text.Json.JsonSerializer.Deserialize<WeChatPayNotifyData>(decryptData);
if (notifyData.TradeState == "SUCCESS")
{
//回调成功这里处理自己的业务逻辑
}
}
catch (Exception ex)
{
Logger.Information($"支付成功回调错误:{ex.Message}");
}
return APIResult.Success();
}
/// <summary>
/// 验证微信支付回调签名
/// </summary>
/// <param name="timestamp">时间戳</param>
/// <param name="nonce">随机字符串</param>
/// <param name="signature">签名</param>
/// <param name="wechatpaySerial">微信平台证书序列号</param>
/// <param name="body">请求体</param>
/// <returns></returns>
private async Task<bool> VerifySignature(
string timestamp,
string nonce,
string signature,
string wechatpaySerial,
string body)
{
try
{
// 获取微信平台证书公钥
var certPublicKey = await GetWechatPlatformCertificate(wechatpaySerial);
if (string.IsNullOrEmpty(certPublicKey))
{
Logger.Error($"未找到序列号为 {wechatpaySerial} 的微信平台证书");
return false;
}
// 构建待签名字符串
var message = $"{timestamp}\n{nonce}\n{body}\n";
// 从证书获取RSA公钥并验证签名
byte[] certBytes = Encoding.UTF8.GetBytes(certPublicKey);
using var cert = new X509Certificate2(certBytes);
using var rsa = cert.GetRSAPublicKey();
var signatureBytes = Convert.FromBase64String(signature);
var data = Encoding.UTF8.GetBytes(message);
return rsa.VerifyData(data, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
catch (Exception ex)
{
Logger.Error(ex, "签名验证异常");
return false;
}
}
/// <summary>
/// 获取微信平台证书公钥(带缓存)
/// </summary>
/// <param name="serialNo">证书序列号</param>
/// <returns>证书公钥(PEM格式)</returns>
private async Task<string> GetWechatPlatformCertificate(string serialNo)
{
try
{
// 先从Redis缓存获取
var cacheKey = $"wechatpay:cert:{serialNo}";
var cachedCert = await App.Cache.GetAsync<string>(cacheKey);
if (!string.IsNullOrEmpty(cachedCert))
{
return cachedCert;
}
// 从微信平台获取证书列表
var certList = await FetchWechatPlatformCertificatesAsync();
if (certList != null && certList.TryGetValue(serialNo, out var cert))
{
// 缓存证书(微信平台证书更新频率较低,缓存1天)
await App.Cache.SetAsync(cacheKey, cert, TimeSpan.FromDays(1));
return cert;
}
return null;
}
catch (Exception ex)
{
Logger.Error(ex, "获取微信平台证书异常");
return null;
}
}
/// <summary>
/// 从微信平台API获取证书列表
/// </summary>
/// <returns>证书序列号对应的公钥字典</returns>
private async Task<Dictionary<string, string>> FetchWechatPlatformCertificatesAsync()
{
try
{
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Guid.NewGuid().ToString("N");
// 完整的API地址
var url = "https://api.mch.weixin.qq.com/v3/certificates";
// 请求路径(用于签名)
var urlPath = "/v3/certificates";
// 构建签名串(GET请求)
var signStr = $"GET\n{urlPath}\n{timestamp}\n{nonce}\n\n";
// 使用商户私钥签名
using var rsa = RSA.Create();
rsa.ImportFromPem(privateKey);
var signature = rsa.SignData(Encoding.UTF8.GetBytes(signStr), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var signatureBase64 = Convert.ToBase64String(signature);
// 构建Authorization头
var authorization = $"WECHATPAY2-SHA256-RSA2048 mchid=\"{_mch_id}\",nonce_str=\"{nonce}\",signature=\"{signatureBase64}\",timestamp=\"{timestamp}\",serial_no=\"{serial_no}\"";
var headers = new Dictionary<string, string>
{
["Accept"] = "application/json",
["User-Agent"] = "WeChatPay Aos/1.0",
["Authorization"] = authorization
};
var response = await HttpHelper.HttpGetAsync(url, headers);
if (string.IsNullOrEmpty(response))
{
Logger.Error("获取微信平台证书列表失败:响应为空");
return null;
}
// 解析响应获取证书列表
using var doc = JsonDocument.Parse(response);
// 检查是否有错误码
if (doc.RootElement.TryGetProperty("code", out var codeElement))
{
var errorCode = doc.RootElement.GetProperty("code").GetString();
var errorMessage = doc.RootElement.TryGetProperty("message", out var msgElement) ? msgElement.GetString() : "未知错误";
Logger.Error($"获取微信平台证书失败: code={errorCode}, message={errorMessage}");
return null;
}
var data = doc.RootElement.GetProperty("data");
var certDict = new Dictionary<string, string>();
foreach (var item in data.EnumerateArray())
{
var serial = item.GetProperty("serial_no").GetString();
var encryptCert = item.GetProperty("encrypt_certificate");
// 获取密文和密钥
var associatedData = encryptCert.GetProperty("associated_data").GetString();
var nonceStr = encryptCert.GetProperty("nonce").GetString();
var ciphertext = encryptCert.GetProperty("ciphertext").GetString();
// 解密证书(微信使用AEAD_AES_256_GCM)
var decryptedCert = DecryptCert(associatedData, nonceStr, ciphertext);
if (!string.IsNullOrEmpty(decryptedCert))
{
certDict[serial] = decryptedCert;
}
}
return certDict;
}
catch (Exception ex)
{
Logger.Error(ex, "获取微信平台证书列表异常");
return null;
}
}
/// <summary>
/// 解密微信平台证书
/// </summary>
/// <param name="associatedData">附加数据</param>
/// <param name="nonce">随机字符串</param>
/// <param name="ciphertext">密文</param>
/// <returns>解密后的证书PEM</returns>
private string DecryptCert(string associatedData, string nonce, string ciphertext)
{
try
{
var keyBytes = Encoding.UTF8.GetBytes(_apiV3Key);
var associatedDataBytes = Encoding.UTF8.GetBytes(associatedData);
var nonceBytes = Encoding.UTF8.GetBytes(nonce);
var ciphertextBytes = Convert.FromBase64String(ciphertext);
if (ciphertextBytes.Length < 16)
throw new ArgumentException("无效的密文长度");
var tag = new byte[16];
var actualCiphertext = new byte[ciphertextBytes.Length - tag.Length];
Array.Copy(ciphertextBytes, ciphertextBytes.Length - 16, tag, 0, 16);
Array.Copy(ciphertextBytes, 0, actualCiphertext, 0, ciphertextBytes.Length - 16);
using var aesGcm = new AesGcm(keyBytes, 16);
var plainText = new byte[actualCiphertext.Length];
aesGcm.Decrypt(nonceBytes, actualCiphertext, tag, plainText, associatedDataBytes);
return Encoding.UTF8.GetString(plainText);
}
catch (Exception ex)
{
Logger.Error(ex, "解密证书异常");
return null;
}
}
/// <summary>
/// 解密微信支付回调资源
/// </summary>
/// <param name="associatedData">附加数据</param>
/// <param name="nonce">随机字符串</param>
/// <param name="ciphertext">密文</param>
/// <returns>解密后的JSON字符串</returns>
private string DecryptResource(string associatedData, string nonce, string ciphertext)
{
try
{
var keyBytes = Encoding.UTF8.GetBytes(_apiV3Key); // APIv3密钥(32字节)
var associatedDataBytes = Encoding.UTF8.GetBytes(associatedData);
var nonceBytes = Encoding.UTF8.GetBytes(nonce);
var ciphertextBytes = Convert.FromBase64String(ciphertext);
// 分离密文和认证标签(微信使用AES-GCM模式)
if (ciphertextBytes.Length < 16)
throw new ArgumentException("无效的密文长度");
var tag = new byte[16];
var actualCiphertext = new byte[ciphertextBytes.Length - tag.Length];
Buffer.BlockCopy(ciphertextBytes, actualCiphertext.Length, tag, 0, tag.Length);
Buffer.BlockCopy(ciphertextBytes, 0, actualCiphertext, 0, actualCiphertext.Length);
// 使用AES-GCM解密
var plaintextBytes = new byte[actualCiphertext.Length];
using var aesGcm = new AesGcm(keyBytes);
aesGcm.Decrypt(nonceBytes, actualCiphertext, tag, plaintextBytes, associatedDataBytes);
return Encoding.UTF8.GetString(plaintextBytes);
}
catch (Exception ex)
{
Logger.Error(ex, "解密微信支付资源失败");
throw new Exception("解密失败,请检查参数和密钥", ex);
}
}
#endregion
}
}
微信支付回调数据DTO
csharp
/// <summary>
/// 微信支付回调通知数据
/// </summary>
public class WeChatPayNotifyData
{
/// <summary>
/// 商户订单号
/// </summary>
public string OutTradeNo { get; set; }
/// <summary>
/// 微信支付订单号
/// </summary>
public string TransactionId { get; set; }
/// <summary>
/// 交易类型
/// </summary>
public string TradeType { get; set; }
/// <summary>
/// 交易状态
/// </summary>
public string TradeState { get; set; }
/// <summary>
/// 交易状态描述
/// </summary>
public string TradeStateDesc { get; set; }
/// <summary>
/// 用户支付金额(单位:分)
/// </summary>
public int Amount { get; set; }
/// <summary>
/// 用户实付金额
/// </summary>
public int PayerTotal { get; set; }
/// <summary>
/// 货币类型
/// </summary>
public string Currency { get; set; }
/// <summary>
/// 用户支付币种
/// </summary>
public string PayerCurrency { get; set; }
/// <summary>
/// 支付完成时间
/// </summary>
public string SuccessTime { get; set; }
/// <summary>
/// 支付者OpenId
/// </summary>
public string OpenId { get; set; }
}
处理流程图
┌─────────────────────────────────────────────────────────────────┐
│ 微信支付服务器回调 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. HomeController.HandleNotify() │
│ - 获取 Headers: Wechatpay-Timestamp/Nonce/Signature/Serial │
│ - 读取 Request.Body │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. HomeService.ProcessPaidOrderAsync() │
│ - 调用 VerifySignature() 验签 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. VerifySignature() │
│ - GetWechatPlatformCertificate() 获取微信平台证书公钥 │
│ - FetchWechatPlatformCertificatesAsync() 调用V3 API获取证书 │
│ - DecryptCert() 解密证书 │
│ - 使用公钥验证签名 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. 验签通过后 │
│ - DecryptResource() 解密回调数据 │
│ - 反序列化为 WeChatPayNotifyData │
│ - 处理业务逻辑(更新订单状态等) │
└─────────────────────────────────────────────────────────────────┘
注意事项
- Body重复读取问题 :由于审计过滤器会读取Body,需要在Controller中读取后设置
Request.Body.Position = 0,或在接口上添加[NotAudit]特性 - 证书缓存:微信平台证书会定期更新,建议缓存1天后再重新获取
- 日志记录:关键步骤都添加了日志,方便排查问题
- 微信平台证书与商户证书的区别 :
- 商户证书:用于调用微信API时签名
- 微信平台证书:用于验证微信回调的真实性