资讯动态

微信支付V3 C#后端接入指南:JSAPI下单签名、回调验签解密实战

发布时间:2026/10/20 12:58:34 来源:尧图企业网站定制
微信支付V3上线到现在很多同学还是停留在V2那套XML报文加MD5签名的旧习惯里。我最早从V2切V3的时候也绕了不少弯子尤其是C#后端这边资料一直比较零散官方文档示例大多是Java和PHP照着抄还得自己翻译成C#的API调用方式。这篇就把我自己跑通的一套微信小程序JSAPI支付V3完整流程整理出来从账号配置、密钥体系、C#端下单签名到小程序调起支付参数生成、回调验签解密一条龙手把手过一遍。适合正在接微信支付、后端用.NET Core/.NET 5的开发者也适合那些刚接触V3协议、搞不清商户私钥和APIv3密钥到底怎么用的朋友。1. 动手前的3件事账号配置、密钥体系与支付流程梳理1.1 你需要的四把钥匙微信支付V3和V2最大的区别就是全面改用RSA非对称签名并且区分了平台证书、商户API私钥、APIv3密钥这三条完全不同的安全链路。很多第一次接V3的人在这里就懵了为什么我调用下单接口时用的是商户API私钥签名回调验签时却要用平台证书的公钥解密回调内容时又冒出一个APIv3密钥其实它们服务于完全不同的三个动作。第一把钥匙是商户号mchid和AppID。商户号是微信支付侧你作为收款方的身份标识AppID是你小程序应用的身份证这两个值在配置关联绑定之后微信支付才知道是哪个商户在哪个小程序里收钱。这个没有什么技术难度唯一要注意的是商户号和小程序AppID的绑定关系要在商户平台后台开通否则JSAPI下单时会报商户号与AppID不匹配。第二把钥匙是商户API证书准确说是证书里的私钥。商户API证书可以在商户平台后台申请同时会得到一本包含证书序列号的CA证书文件但我们在后端代码里实际使用的是配套的私钥文件apiclient_key.pem。V3协议里你发出的每一个HTTP请求Header里的Authorization签名就是用这把私钥对请求方法、请求路径、时间戳、随机串、请求体拼接成的字符串做RSA-SHA256签名得到的。第三把钥匙是APIv3密钥32字节的AES密钥。这把密钥不参与HTTP请求签名而是专门用来解密微信支付回调通知里的敏感字段比如transaction_id、payer信息等以及退款通知里的敏感信息。换句话说这是微信支付平台和你之间约定好的对称加密口令微信侧用这个密钥加密敏感数据你收到后也用这个密钥解密。第四把钥匙是微信支付平台证书。它是微信支付平台的公钥证书你在验证回调通知签名时要用它或者在特殊情况下比如下载账单用它加密密钥。平台证书可以在商户平台下载建议用API接口自动更新或定期手动替换。这四把钥匙的关系我习惯这么类比商户API私钥相当于你的个人签名章所有你主动发出去的业务请求都要盖上它APIv3密钥相当于你们内部公开的保险箱密码微信把需要保密的数据放进去你用同一个密码打开平台证书则是微信方的介绍信微信推消息给你时先盖好自己的章你用它的介绍信来核实这确实微信发的。1.2 从下单到支付成功完整时序微信小程序JSAPI支付的完整调用链拆开看其实只有六步。第一步是小程序前端拿到用户点击的立即支付事件后把订单号或其他业务标识传给自己的后端服务。第二步是后端服务拿到订单号校验订单是否存在、金额是否正确然后调用微信支付V3的下单接口/v3/pay/transactions/jsapi发起统一下单。第三步是微信支付返回预支付交易会话标识prepay_id这个ID相当于微信侧给你开的赊购凭证。第四步是后端用prepay_id和appId、时间戳、随机串、签名类型拼出一组调起支付参数返回给小程序前端。第五步是小程序端通过wx.requestPayment发起拉起收银台动作用户在微信内完成支付。第六步是微信支付服务器向你配置的notify_url发送异步回调通知这笔订单最终支付结果后端收到后需要验签、解密、更新本地订单状态并给微信一个明确的应答。整个链路中核心业务逻辑都在后端前端只负责把后端给的那五个参数原封不动传给微信支付。这也就是为什么很多人说支付参数绝不能在前端拼因为prepay_id和paySign一旦被篡改支付金额和订单内容都无法保证。1.3 V2与V3的根本差异之前用V2的同学应该还记得V2下单后返回的是prepay_id小程序端调起支付时只需要把timeStamp、nonceStr、package、signType和paySign拼好其中paySign用的还是MD5或HMAC-SHA256这类对称签名。这套方案最大的隐患是API密钥一旦泄露任何人都能伪造支付参数。V3全面改用RSA非对称签名后商户私钥和APIv3密钥分离签名密钥和加密密钥互不共用安全边界干净得多。另外V3的报文全部是JSON比V2的XML可读性好太多调试时用Postman肉眼就能看出来请求体里是什么字段。再加上V3接口是平台证书机制商户可以自己下载平台证书并校验回调来源不用再像V2那样把回调服务器的IP白名单和证书扔到一边自欺欺人。如果你是从零开始的新项目我建议直接上V3没有任何理由再去学一套快被淘汰的方案。如果你是V2老项目在迁移重点就是把签名算法从MD5/HMAC换成RSA-SHA256把请求体从XML转成JSON把回调验签从MD5校验换成平台证书验签其余业务流程基本一致。2. C#后台的环境准备与基础数据结构2.1 项目依赖与目标框架我这边用的是.NET 8但整个逻辑在.NET Core 3.1、.NET 6、.NET 7上都能跑。因为核心的加密和HTTP操作都在系统自带命名空间里不需要额外引入第三方微信支付SDK。说实话V3的接口就那么几个自己封装反而比引SDK灵活尤其SDK升级频繁的时候自己写一套还能顺便把签名、解密这些环节吃透。项目中实际用到的几个关键命名空间有System.Net.Http发送HTTP请求构造JSON请求体。System.Security.CryptographyRSA签名、AES-GCM解密、SHA256哈希。System.Text.JsonJSON序列化与反序列化.NET Core 3.1需要加System.Text.Json包.NET 6内置。System.Web或Microsoft.AspNetCore.WebUtilities处理URL编码和回调参数解析。不需要装任何NuGet包除非你用的框架版本特别老。2.2 商户配置类与读取配置我建议放在appsettings.json里但不建议把私钥内容直接写在配置文件中而是把私钥文件路径放进去运行时再读取文件内容。这样至少密钥不会跟着配置满天飞。{ WechatPay: { MchId: 1600000000, AppId: wx0000000000000000, AppSecret: 你的小程序AppSecret不参与支付签名但退款/查询等场景可能用到, ApiV3Key: 32位长的APIv3密钥, MerchantPrivateKeyPath: cert/apiclient_key.pem, MerchantCertificateSerialNo: 证书序列号, PlatformCertificatePath: cert/wechatpay_platform.pem, NotifyUrl: https://你的域名/api/wxpay/notify } }对应的配置类public class WechatPayOptions { public string MchId { get; set; } public string AppId { get; set; } public string AppSecret { get; set; } public string ApiV3Key { get; set; } public string MerchantPrivateKeyPath { get; set; } public string MerchantCertificateSerialNo { get; set; } public string PlatformCertificatePath { get; set; } public string NotifyUrl { get; set; } }在Program.cs或Startup.cs里注册成单例var wechatPayOptions builder.Configuration.GetSection(WechatPay).GetWechatPayOptions(); builder.Services.AddSingleton(wechatPayOptions); builder.Services.AddHttpClient();这里我特别提醒一点不要把私钥硬编码在代码里也不要把apiclient_key.pem放进去Git仓库。我在测试环境习惯用环境变量或者用户机密来覆盖配置项生产环境用密钥管理服务读取私钥内容这样即使代码仓库泄露私钥也不会跟着泄露。2.3 请求与响应的实体模型V3的JSON字段是驼峰命名camelCaseC#属性如果用PascalCase需要在序列化时配置命名策略。我的习惯是直接用特性标注public class JsApiPayRequest { [JsonPropertyName(appid)] public string AppId { get; set; } [JsonPropertyName(mchid)] public string MchId { get; set; } [JsonPropertyName(description)] public string Description { get; set; } [JsonPropertyName(out_trade_no)] public string OutTradeNo { get; set; } [JsonPropertyName(notify_url)] public string NotifyUrl { get; set; } [JsonPropertyName(amount)] public AmountInfo Amount { get; set; } [JsonPropertyName(payer)] public PayerInfo Payer { get; set; } } public class AmountInfo { [JsonPropertyName(total)] public int Total { get; set; } // 单位分 [JsonPropertyName(currency)] public string Currency { get; set; } CNY; } public class PayerInfo { [JsonPropertyName(openid)] public string OpenId { get; set; } }注意金额的单位是分不是元。这个我后面还会再强调因为整数和浮点数的坑几乎每个人都踩过。下单响应的核心字段就是prepay_idpublic class JsApiPayResponse { [JsonPropertyName(prepay_id)] public string PrepayId { get; set; } }3. 核心代码一次完整的JSAPI下单与参数生成3.1 构造请求体下单接口是POST https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi请求体里的字段中description是商品描述out_trade_no是你自己系统的订单号必须保证唯一。notify_url必须是HTTPS地址微信支付服务器会向这个地址推送支付结果。payer.openid是发起支付的小程序用户的openid这个是在小程序前端调wx.login后通过code2Session接口拿到的。一个完整的构造过程如下public async TaskJsApiPayResponse CreateJsapiOrderAsync(string openId, string outTradeNo, int totalFee, string description) { var request new JsApiPayRequest { AppId _options.AppId, MchId _options.MchId, Description description, OutTradeNo outTradeNo, NotifyUrl _options.NotifyUrl, Amount new AmountInfo { Total totalFee, Currency CNY }, Payer new PayerInfo { OpenId openId } }; var json JsonSerializer.Serialize(request, _jsonOptions); var (statusCode, responseBody) await SendRequestAsync( HttpMethod.Post, /v3/pay/transactions/jsapi, json); if (statusCode System.Net.HttpStatusCode.OK || statusCode System.Net.HttpStatusCode.NoContent) { return JsonSerializer.DeserializeJsApiPayResponse(responseBody, _jsonOptions); } // 非2xx时微信返回的错误信息结构是 { code, message, detail } var error JsonSerializer.DeserializeWechatPayError(responseBody, _jsonOptions); throw new WechatPayException(error?.Code, error?.Message); }3.2 签名与Authorization头V3的请求签名是整个流程里最容易翻车的环节也是C#开发者最头疼的部分。构造签名需要先拼一个签名串规则是HTTP请求方法\n 请求路径\n 请求时间戳\n 请求随机串\n 请求报文主体\n注意每个部分之间是换行符\n且是精确格式。请求路径只包含路径部分不包含查询字符串不对——如果URL里带查询参数路径部分要包含完整路径和参数比如/v3/pay/transactions/out-trade-no/123456?mchid12345这样的完整URL不含域名作为签名串的请求路径。对于JSAPI下单这种POST接口通常路径就是/v3/pay/transactions/jsapi。请求体是原始JSON字符串必须是发送HTTP请求时实际发送的body字面值。这里有个隐藏的坑如果你在序列化请求体之后、发请求之前又改了属性值但签名用的是改之前的字符串那签名必然失败。所以代码里一定先用字符串变量保存请求体签名和发送时都引同一个变量。签名算法是RSA-SHA256私钥是商户API私钥输出是Base64编码的签名结果。C#里对应代码private string BuildAuthorization(HttpMethod method, string url, string body, out string timestamp, out string nonce) { timestamp DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); nonce Guid.NewGuid().ToString(N); var message ${method.Method}\n{url}\n{timestamp}\n{nonce}\n{body}\n; using var rsa RSA.Create(); rsa.ImportFromPem(_options.MerchantPrivateKeyPath); // 注意.NET 5 支持ImportFromPem var signature rsa.SignData( Encoding.UTF8.GetBytes(message), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var signatureBase64 Convert.ToBase64String(signature); return $WECHATPAY2-SHA256-RSA2048 mchid\{_options.MchId}\, $nonce_str\{nonce}\, $timestamp\{timestamp}\, $serial_no\{_options.MerchantCertificateSerialNo}\, $signature\{signatureBase64}\; }如果你用的是.NET Core 3.1没有ImportFromPem方法就要先读取pem文件内容去掉头尾标记和换行转成Base64字节数组再ImportRSAPublicKey/ImportRSAPrivateKey。我在.NET 6下就没这个烦恼了直接ImportFromPem一把梭。还要提一下Authorization头的格式它是一整段字符串各部分用逗号拼接WECHATPAY2-SHA256-RSA2048固定的签名类型标识。mchid商户号。nonce_str随机字符串必须和签名串里用的一致。timestampUnix时间戳秒必须和签名串里用的一致。serial_no商户证书序列号这个值不是私钥里的序列号是商户API证书的序列号在商户平台后台能查到。signatureBase64编码的RSA签名结果。发送请求时这段完整字符串放在Header的Authorization字段里。同时Content-Type必须设置为application/json; charsetutf-8。请求发送的完整代码private async Task(HttpStatusCode StatusCode, string Body) SendRequestAsync(HttpMethod method, string url, string jsonBody) { var auth BuildAuthorization(method, url, jsonBody, out _, out _); using var request new HttpRequestMessage(method, new Uri(baseUrl url)); request.Headers.Add(Authorization, auth); request.Headers.Add(Accept, application/json); request.Headers.Add(User-Agent, your-app-id/1.0); if (!string.IsNullOrEmpty(jsonBody)) { request.Content new StringContent(jsonBody, Encoding.UTF8, application/json); } var response await _httpClient.SendAsync(request); var responseBody await response.Content.ReadAsStringAsync(); return (response.StatusCode, responseBody); }注意HttpClient实例建议注册为单例不要每次请求都new一个否则连接池很快会耗尽。3.3 从prepay_id生成小程序调起支付参数拿到prepay_id之后还不能直接把这个ID返回给前端小程序端真正需要的是五个参数其中paySign需要再次用商户私钥签名。参与签名的字符串格式是appIdxxxtimeStampxxxnonceStrxxxpackageprepay_idxxxsignTypeRSA注意package的值是prepay_idxxx这样的整体字符串不是纯粹的一串ID。这个格式跟V2不一样V2时package是一个纯标识V3必须带上前缀。signType固定为RSA。C#生成代码public string BuildPayParams(string prepayId) { var timeStamp DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); var nonceStr Guid.NewGuid().ToString(N); var packageStr $prepay_id{prepayId}; var signType RSA; var message $appId{_options.AppId}\n $timeStamp{timeStamp}\n $nonceStr{nonceStr}\n $package{packageStr}\n $signType{signType}; using var rsa RSA.Create(); rsa.ImportFromPem(_options.MerchantPrivateKeyPath); var signature rsa.SignData( Encoding.UTF8.GetBytes(message), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); var paySign Convert.ToBase64String(signature); return JsonSerializer.Serialize(new Dictionarystring, object { [timeStamp] timeStamp, [nonceStr] nonceStr, [package] packageStr, [signType] signType, [paySign] paySign }, _jsonOptions); }前端拿到这个JSON后直接调用wx.requestPayment({ timeStamp: res.data.timeStamp, nonceStr: res.data.nonceStr, package: res.data.package, signType: res.data.signType, paySign: res.data.paySign, success: function (res) { /* 支付成功 */ }, fail: function (err) { /* 支付失败或取消 */ } });这里有个经验时间戳在服务端生成后传给前端时一定要保证是字符串类型。有些同学喜欢用long类型返回小程序端拿到的是数字类型还有的框架会把它序列化成科学计数法会导致timeStamp解析不正确。所以我在返回前端时全部转成字符串并在接口文档里标注timeStamp必须为string。另外时间戳一定要用服务器当前时间不要用前端传过来的时间。因为签名串里的时间戳会参与微信支付侧的签名校验时间偏差太大标准是5分钟内实际建议控制在3分钟内会被拒绝为时间戳已失效。4. 回调通知验签、解密、更新订单容易翻车的环节4.1 构造验签文本并校验平台证书签名支付完成之后微信支付服务器会向你的notify_url发送一个异步回调。很多同学一开始只调了下单接口以为支付结果会在小程序端success回调里拿到就万事大吉了。这是大忌小程序里的success回调只能代表用户在收银台完成了支付动作并不可靠最终的订单状态必须以后台收到的回调通知为准。回调通知的请求头里有两个关键字段Wechatpay-Timestamp时间戳、Wechatpay-Nonce随机串、Wechatpay-Signature签名值以及Wechatpay-Serial平台证书序列号。请求体是一段JSON整体结构是{ id: 回调通知ID, resource_type: encrypt-resource, event_type: TRANSACTION.SUCCESS, resource: { algorithm: AEAD_AES_256_GCM, ciphertext: 加密的支付结果数据, associated_data: 附加数据通常为空, nonce: 加密时使用的随机串 } }验签目的就是确认这段通知确实是微信支付平台发出的而不是伪装成微信支付的攻击者发的。验签文本的构造顺序是微信支付回调的时间戳\n 微信支付回调的随机串\n 微信支付回调的请求体原文\n同样用\n连接然后用平台证书公钥对这段文本做RSA-SHA256验签把收到的Wechatpay-Signature做Base64解码后比对。C#代码public bool VerifyNotifySignature(string timestamp, string nonce, string body, string signatureBase64) { var message ${timestamp}\n{nonce}\n{body}\n; using var rsa RSA.Create(); var pubKey File.ReadAllText(_options.PlatformCertificatePath); rsa.ImportFromPem(pubKey); var data Encoding.UTF8.GetBytes(message); var signature Convert.FromBase64String(signatureBase64); return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }验签通过后还要检查Wechatpay-Serial是否和当前使用的平台证书序列号一致。如果平台证书更新了但你代码里还用旧证书验签就会失败。所以平台证书的自动更新机制很重要要么写个定时任务查证要么手动更新后重启服务。很多生产环境的支付回调偶尔报验签失败查来查去发现只是平台证书在某天过期或者被重新生成过而代码里的旧证书没替换。4.2 AES-256-GCM解密敏感数据验签通过后resource.ciphertext是AES-256-GCM加密的密文。GCM模式是一种带认证的加密模式解密时不仅需要密钥APIv3密钥还需要nonce和附加数据associated_data并且解出来的明文末尾会附带16字节的认证标签tag用于完整性校验。C#里用AesGcm类解密。构造函数在.NET 6可用.NET 8里还多了支持Span的重载。核心逻辑public string DecryptResource(string ciphertext, string nonce, string associatedData) { var keyBytes Encoding.UTF8.GetBytes(_options.ApiV3Key); var nonceBytes Encoding.UTF8.GetBytes(nonce); var ciphertextBytes Convert.FromBase64String(ciphertext); var associatedDataBytes Encoding.UTF8.GetBytes(associatedData ?? ); // 密文最后16字节是tag var tag ciphertextBytes[^16..]; var encrypted ciphertextBytes[..^16]; var plain new byte[encrypted.Length]; using var aesGcm new AesGcm(keyBytes); aesGcm.Decrypt(nonceBytes, encrypted, tag, plain, associatedDataBytes); return Encoding.UTF8.GetString(plain); }解密出来的明文是一段JSON里面才是完整的支付结果对象包括transaction_id、out_trade_no、trade_state、amount、payer等字段。拿到之后先更新本地订单状态做幂等检查再给微信支付返回一个明确应答。4.3 更新订单与应答微信支付回调通知的应答格式很简单返回HTTP状态码200表示你已经成功处理微信不再重发返回非200或返回的内容不对微信会按一定策略重试间隔逐渐拉长最长约15小时。所以处理回调时一定要考虑幂等同一笔订单可能因为网络原因收到多次通知你的业务处理逻辑必须能正确处理重复通知不能重复加余额或者重复发货。我的处理顺序是先验签验签失败直接返回401或400。验签通过后解密根据out_trade_no查自己的订单如果订单状态已经是支付成功直接返回200不再重复处理。如果是待支付状态则更新订单为已支付清理本地待支付缓存然后返回200。C#控制器里我习惯这样写[HttpPost(api/wxpay/notify)] public async TaskIActionResult Notify() { string body; using (var reader new StreamReader(Request.Body, Encoding.UTF8)) { body await reader.ReadToEndAsync(); } var timestamp Request.Headers[Wechatpay-Timestamp].ToString(); var nonce Request.Headers[Wechatpay-Nonce].ToString(); var signature Request.Headers[Wechatpay-Signature].ToString(); var serial Request.Headers[Wechatpay-Serial].ToString(); var valid _payService.VerifyNotifySignature(timestamp, nonce, body, signature); if (!valid) { return BadRequest(验签失败); } var notifyObject JsonSerializer.DeserializeNotifyEnvelope(body, _jsonOptions); if (notifyObject?.Resource null) { return BadRequest(无资源数据); } var plain _payService.DecryptResource( notifyObject.Resource.Ciphertext, notifyObject.Resource.Nonce, notifyObject.Resource.AssociatedData); var result JsonSerializer.DeserializePayResult(plain, _jsonOptions); var handled await _orderService.HandlePayResultAsync(result); if (!handled) { return StatusCode(500, 处理失败); } // 必须返回微信要求的Success结构 return Content({\code\:\SUCCESS\,\message\:\成功\}, application/json); }注意返回的JSON不是{code:200}而是固定的{code:SUCCESS,message:成功}这个结构。很多同学在这里踩坑返回了自定义的JSON结构结果微信侧一直判定处理失败不断重推回调。5. 我踩过的坑签名失败、回调解密失败的定位思路5.1 签名失败的5个高频原因先拿出现频率最高的签名失败来说。微信支付V3接口返回的错误信息里通常会带code和message字段常见的包括SIGN_ERROR、INVALID_REQUEST、FREQUENCY_LIMITED之类。如果遇到SIGN_ERROR我的排查顺序是第一检查签名串的换行符。签名串要求是\n不是\r\n也不是空格。在Windows环境下从记事本复制粘贴时很容易混入\r\n这种肉眼看不见的差异最阴人。建议用Debug打印签名串的ASCII码或者在代码里用字符串字面量方式构造避免手动拼。第二检查请求体是否和签名时完全一致。有些HttpClient库会在发送时自动格式化JSON比如把冒号后加空格、删掉多余空白这会导致签名用的body和实际发送的body不一致。我记得一个朋友之前就是因为HttpClient的JsonSerializerSettings里加了NullValueHandling.Ignore序列化请求体时把空值属性全删了但签名时用的是没删除的版本结果死活签名不通过。解决方法是存一份body字符串常量序列化完就不要再改。第三检查商户私钥文件是否加载正确。私钥是PKCS#1还是PKCS#8格式ImportFromPem都能处理但文件必须是一整个PEM块不能只有一行。很多人在拼接配置时把私钥的换行符弄丢了就会报私钥格式错误。第四检查证书序列号是不是商户API证书的序列号。在商户平台看到的证书序列号可能显示成带冒号分隔的十六进制字符串但Authorization头里填写时不能带冒号也不能带空格必须是小写十六进制的一串字符长度不超过40位。第五检查时间戳和随机串是否变化过。如果你在调用BuildAuthorization之后又生成新的时间戳和nonce但用来签名的还是旧值Header里带的却是新值服务端验签时肯定是失败的。所以建议把签名过程做成一个原子操作签名时直接输出Header字符串。5.2 小程序端常见报错小程序端wx.requestPayment的fail回调中常见的报错有requestPayment:fail invalid parameter这个基本是参数类型不对。最常见的原因就是timeStamp传成了数字而不是字符串或者package忘了带prepay_id前缀。requestPayment:fail cancel这是用户在收银台主动取消了支付不算技术故障。requestPayment:fail ip is not in whitelist这个一般是后端在调用下单接口时微信校验了商户的API调用来源IP如果不是商户平台配置的IP白名单就会报这个错。在小程序支付场景下如果不配置IP白名单其实不会强制校验但如果配置了那后端服务器的出口IP必须在白名单里。另外一个容易被忽视的是调用wx.requestPayment前必须在小程序的后台配置好支付域名和业务域名。如果你同时做了H5页面跳转还需要单独配置H5支付域名。5.3 回调解密失败的排查AES-256-GCM解密失败通常不是密钥错误就是密文格式错误。先确认APIv3密钥是32字节别把商户API私钥或者证书序列号当成APIv3密钥。我记得曾经遇到一个情况ad同学复制配置时把AppSecret的值贴到了ApiV3Key里解密时虽然AesGcm类不会报密钥长度错误因为AppSecret通常也是32位但解密结果会是乱码或者直接抛AuthenticationTagMismatchException。其次密文取法要注意从Base64字符串Convert.FromBase64String之后密文总长度是加密数据长度 16字节tag。微信文档里说密文内包含了tag所以在解密时需要把最后16字节单独拿出来作为tag传给AesGcm前面部分才是密文主体。如果你直接把完整密文传给AesGcm解密时系统会把最后16字节当成密文的末尾认证标签必然不匹配。另外associated_data在支付回调场景下通常为空字符串但如果你从HttpContext里读取时不小心加了一个null判断并且把null传进去也可能导致解密失败。用空字符串即可。5.4 线上安全建议支付相关代码过审时我始终确定的几个安全底线是私钥和APIv3密钥绝不进代码仓库最好放在环境变量、密钥管理服务或独立配置中心里并用权限控制限制读取日志里绝不能打明文密钥、完整签名串、支付回调明文下单金额必须在后端根据商品原价计算不能信任前端传来的金额回调处理要做好订单状态机的约束支付成功后的订单要设置状态流转规则防止二次发货、重复返利。还有一个细节如果你有多套环境开发、测试、生产建议给每个环境申请独立的商户号或至少独立的APIv3密钥否则测试环境打的脏数据会混进生产环境账单里。最后再分享一个小技巧很多人接支付的时候只会去调下单接口忽略了查询订单接口的价值。实际上微信支付V3提供了按商户订单号和微信支付订单号查询订单的接口GET /v3/pay/transactions/out-trade-no/{out_trade_no}?mchidxxx。当回调因为网络问题迟迟没来或者前端支付成功后用户又刷新了页面时后端可以用这个查询接口主动确认订单状态。我的习惯是在用户支付成功页的后端接口里主动查一次微信侧订单状态做二次确认这样既不依赖回调是否到达也能给用户更及时的状态反馈。这个接口本身也很简单带上签名请求就行返回的数据格式和回调解密出的明文结构几乎一致相当于白拿一个支付状态的补充通道。实测下来在生产环境稳定跑了大半年除了平台证书更新时需要手动替换了一下其余流程没再出过问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑