.netcore 微信支付 V3 版本回调验签流程

微信支付回调文档

目录

  1. 概述
  2. 涉及对象
  3. 核心配置
  4. 完整代码
  5. 微信支付回调数据DTO
  6. 处理流程图
  7. 注意事项

概述

微信支付 V3 版本回调验签流程:

  1. 列号)、Wechatpay-Timestamp(时间戳)、Wechatpay-Nonce(随机数)、Wechatpay-Signature(签名)微信服务器回调时携带 headers: Wechatpay-Serial(证书序
  2. 根据 Wechatpay-Serial 获取对应的微信平台证书公钥
  3. 使用公钥验签 Wechatpay-Signature
  4. 验签通过后,解密回调数据中的加密内容
  5. 处理业务逻辑

涉及对象

对象 说明
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                             │
│     - 处理业务逻辑(更新订单状态等)                               │
└─────────────────────────────────────────────────────────────────┘

注意事项

  1. Body重复读取问题 :由于审计过滤器会读取Body,需要在Controller中读取后设置 Request.Body.Position = 0,或在接口上添加 [NotAudit] 特性
  2. 证书缓存:微信平台证书会定期更新,建议缓存1天后再重新获取
  3. 日志记录:关键步骤都添加了日志,方便排查问题
  4. 微信平台证书与商户证书的区别
    • 商户证书:用于调用微信API时签名
    • 微信平台证书:用于验证微信回调的真实性