微信支付: API V3支付回调签名验证
微信支付官方文档对于签名验证的介绍:微信支付-开发者文档 (qq.com)
微信支付会在回调的HTTP头部中包括回调报文的签名。商户必须 验证回调的签名,以确保回调是由微信支付发送。
参数说明
一个支付回调的示例:
Headers:
[{"Key":"Connection","Value":["Keep-Alive"]},{"Key":"Pragma","Value":["no-cache"]},{"Key":"Accept","Value":["*/*"]},{"Key":"Host","Value":["www.datatook.com"]},{"Key":"User-Agent","Value":["Mozilla/4.0"]},{"Key":"Wechatpay-Nonce","Value":["lL0M9v8R3WtTa5iusxEWgjXgjbyQeP3D"]},{"Key":"Wechatpay-Serial","Value":["4B771705B6FFCA007AAE05A3512E4EA923BF757E"]},{"Key":"Wechatpay-Signature","Value":["ImfeCH3vkB/iQ+nl+JSBuzFdx5aZf3V9+CAyqSww/xeavzcTGtqCO+bixh9Lylk0ZZ2/UJOfJd/KWL4BCqw+GMumAHVWMzb34a0kAdHimVuUt36dNz2ZI58mM/1RN75AMYuNcCptXPTgHzdGBl3+bE2VO+dnoK5q/X+lskFDcxb0LGvKVA7V/bVA0edhSBCaUeiEogajXVXDmHcaHk/4/fUmDD2NCJwjVh0CdynC5p6IQN+mkyITwlJQgASgN6+truAGFGnN50FunTVPXGLze/VbFbZg87flawWce+/KpmFqfSvhxyUOQ/axPMvZkSvMxIWCp7Y0kw6MfMXMCCP5Ow=="]},{"Key":"Wechatpay-Timestamp","Value":["1622016489"]}]
Body:
{"id":"ebd1442a-156f-5e1f-8ca4-d43bf679a603","create_time":"2021-05-26T16:08:09+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"vocEJn2O7MWb9d21Zk48EiHuZfQMapbAjgHPPsvYisfHkZKs4jF15joWbrKb37HYbDuYVYgPoUta4u3i2c1YrcA9VxGWrO043dUO3Nj2QHURCEnIE9j+3G7iY+3PPYiJTI3M1w5vRw6+BDNn0atcLr5GuOy0ajvxpr7bu3GJa6fjuO23pbWw/rEfALaH3igl1gQeeSm+zkVmNeA5uXFrlmX8gMQkADDs+YVIKC84rBslyJZfhvX9rqpI+zPGtXJPs9lRM4qsbtVsqWMK+nEnB7wfj9qTa3fHh/lBtNXzCB29tlodzBPKQS+Pxa9GCMtf3HeIf8YognqF496eqq3kPlGEcF3BdqVu1VqzdW1e+9qzPu68GfexTkml8GliCdFdaO7eq9BUOoebulOLq/p58ZxORxoeaCKR3NfItmSVyHIPbS3wlulJgUDWFo0Umt4hkSmxRp4s+4P40Pc+w/tfAkQsQUYln7V1c1XR8IpWrQn22YGIOKGqB86Fs8Bw4F8S7QzKVhCbD075AoiRhGciXpFD6XNYiMF0gk7gB6eUooZj3aU1bkJzzmf29xUJktcK6G/IeLipE/KzKVeOJJ9t/bqjJRtbAqfEUnTsf7xgwZ2MjQ9gnFn4XG1TELypAyJnajnXfjBcwmEU/3oMxKDw35w9Oc+5V501JFmN/58iKSWYzFK9e48C6iRgZ5cOuU0ImAyF2rjF0KhofJwy7gDV19SteyJkzH+m+116ZiEa8iki","associated_data":"transaction","nonce":"C8EMTkPpAvH5"}}
在回调签名验证中需要用到几个参数:
serialNo
:平台证书(公钥)序列号,验证签名需要用到平台公钥 PublicKey
,通过 获取平台证书列表(https://api.mch.weixin.qq.com/v3/certificates) 接口获取,PublicKey
下载下来后保存在自己的服务器中和 serialNo
是一对,通过 serialNo
找到公钥,如果和保存的公钥 serialNo
不匹配,代表微信平台证书已经更新,需要重新下载平台公钥。在 Header 中获得,参数为:Wechatpay-Serial
示例中值为 4B771705B6FFCA007AAE05A3512E4EA923BF757E
timestamp
:签名时间戳,在 Header 中获得参数为 Wechatpay-Timestamp
示例中值为 1622016489
nonce
:签名随机字符串,在 Header 中获得,参数为 Wechatpay-Nonce
示例中值为 lL0M9v8R3WtTa5iusxEWgjXgjbyQeP3D
signature
:签名,在 Header 中获得,参数为 Wechatpay-Signature
示例中值为 ImfeCH3vkB/iQ+nl+JSBuzFdx5aZf3V9+CAyqSww/xeavzcTGtqCO+bixh9Lylk0ZZ2/UJOfJd/KWL4BCqw+GMumAHVWMzb34a0kAdHimVuUt36dNz2ZI58mM/1RN75AMYuNcCptXPTgHzdGBl3+bE2VO+dnoK5q/X+lskFDcxb0LGvKVA7V/bVA0edhSBCaUeiEogajXVXDmHcaHk/4/fUmDD2NCJwjVh0CdynC5p6IQN+mkyITwlJQgASgN6+truAGFGnN50FunTVPXGLze/VbFbZg87flawWce+/KpmFqfSvhxyUOQ/axPMvZkSvMxIWCp7Y0kw6MfMXMCCP5Ow==
body
:post请求的主体 content
参数值获取代码
// 平台证书序列号 string serialNo = Request.Headers.GetValues("Wechatpay-Serial").FirstOrDefault(); // 签名时间戳 string timestamp = Request.Headers.GetValues("Wechatpay-Timestamp").FirstOrDefault(); // 签名随机字符串 string nonce = Request.Headers.GetValues("Wechatpay-Nonce").FirstOrDefault(); // 验签值 string signature = Request.Headers.GetValues("Wechatpay-Signature").FirstOrDefault(); Request.Content.ReadAsStreamAsync().Result.Seek(0, System.IO.SeekOrigin.Begin); // POST请求主体 string body = Request.Content.ReadAsStringAsync().Result;
签名验证
1. 需要构建待签名字符串
签名时间戳\n
签名随机串\n
Post报文主体\n
string message = $"{timestamp}\n{nonce}\n{body}\n";
示例中结果是:
1622016489
lL0M9v8R3WtTa5iusxEWgjXgjbyQeP3D
{"id":"ebd1442a-156f-5e1f-8ca4-d43bf679a603","create_time":"2021-05-26T16:08:09+08:00","resource_type":"encrypt-resource","event_type":"TRANSACTION.SUCCESS","summary":"支付成功","resource":{"original_type":"transaction","algorithm":"AEAD_AES_256_GCM","ciphertext":"vocEJn2O7MWb9d21Zk48EiHuZfQMapbAjgHPPsvYisfHkZKs4jF15joWbrKb37HYbDuYVYgPoUta4u3i2c1YrcA9VxGWrO043dUO3Nj2QHURCEnIE9j+3G7iY+3PPYiJTI3M1w5vRw6+BDNn0atcLr5GuOy0ajvxpr7bu3GJa6fjuO23pbWw/rEfALaH3igl1gQeeSm+zkVmNeA5uXFrlmX8gMQkADDs+YVIKC84rBslyJZfhvX9rqpI+zPGtXJPs9lRM4qsbtVsqWMK+nEnB7wfj9qTa3fHh/lBtNXzCB29tlodzBPKQS+Pxa9GCMtf3HeIf8YognqF496eqq3kPlGEcF3BdqVu1VqzdW1e+9qzPu68GfexTkml8GliCdFdaO7eq9BUOoebulOLq/p58ZxORxoeaCKR3NfItmSVyHIPbS3wlulJgUDWFo0Umt4hkSmxRp4s+4P40Pc+w/tfAkQsQUYln7V1c1XR8IpWrQn22YGIOKGqB86Fs8Bw4F8S7QzKVhCbD075AoiRhGciXpFD6XNYiMF0gk7gB6eUooZj3aU1bkJzzmf29xUJktcK6G/IeLipE/KzKVeOJJ9t/bqjJRtbAqfEUnTsf7xgwZ2MjQ9gnFn4XG1TELypAyJnajnXfjBcwmEU/3oMxKDw35w9Oc+5V501JFmN/58iKSWYzFK9e48C6iRgZ5cOuU0ImAyF2rjF0KhofJwy7gDV19SteyJkzH+m+116ZiEa8iki","associated_data":"transaction","nonce":"C8EMTkPpAvH5"}}
2. 校验平台序列号 serialNo
是否一致
对比 Header
中的 平台序列号
( serialNo
) 和 本地平台公钥
的 serialNo
是否一致
一致
:直接返回
本地的平台公钥
不一致
:调用 获取平台证书列表(https://api.mch.weixin.qq.com/v3/certificates) 接口 获取
新的 平台公钥
和 serialNo
, 并 确保 和 Header 中的 serialNo 一致,保存
到本地,返回公钥
public class WXCertHelper { List<WXPlatformPublicKey> CacheData { get; set; } private WXCertHelper() { CacheData = new List<WXPlatformPublicKey>(); } static WXCertHelper _intance; // 单例模式 public static WXCertHelper Intance { get { if (_intance == null) { _intance = new WXCertHelper(); _intance.LoadCacheData(); } return _intance; } } /// <summary> /// 从本地加载平台序列号 /// </summary> void LoadCacheData() { CacheData.Clear(); // 从数据库加载数据 } /// <summary> /// 保存平台公钥到本地 /// </summary> /// <param name="data"></param> void SaveCacheData() { //List<WXPlatformPublicKey> data = this.CacheData; // 刷新本地存储 } public string GetPublicKey(string serialNo) { // 匹配缓存中的公钥 var obj = CacheData.Find(w => w.serialNo == serialNo); if (obj != null) { return obj.PublicKey; } // 缓存中没有,重新下载 else { this.RefreshDownCert(); return GetPublicKey(serialNo); } } /// <summary> /// 重新从API下载平台公钥 /// </summary> void RefreshDownCert() { APIHelper apiHelper = new APIHelper(); CacheData.Clear(); var result = apiHelper.DownCert(); foreach (var o in result.data) { string publicKey = AesGcm.AesGcmDecrypt(o.encrypt_certificate.associated_data, o.encrypt_certificate.nonce, o.encrypt_certificate.ciphertext, ConfigData.Intance.WXPaySetting.wx_aes_key); CacheData.Add(new WXPlatformPublicKey() { serialNo = o.serial_no, PublicKey = publicKey }); } SaveCacheData(); } } public class WXPlatformPublicKey { /// <summary> /// 平台证书序列号 /// </summary> public string serialNo { get; set; } /// <summary> /// 平台证书公钥 /// </summary> public string PublicKey { get; set; } }
3. 校验签名
/// <summary> /// 验证签名 /// </summary> /// <param name="message">签名字符串</param> /// <param name="publickey">公钥</param> /// <param name="signStr">签名</param> /// <returns></returns> public bool VerifySign(string message, string publickey, string signStr) { try { var _publicKeyBytes = Encoding.UTF8.GetBytes(publickey); var x509 = new X509Certificate2(_publicKeyBytes); using (var rsa = (RSACryptoServiceProvider)x509.PublicKey.Key) { using (var sha256 = new SHA256CryptoServiceProvider()) { var b = rsa.VerifyData(Encoding.UTF8.GetBytes(message), sha256, Convert.FromBase64String(signStr)); return b; } } } catch { return false; } }
.NET Core 中使用如下方法
var _publicKeyBytes = Encoding.UTF8.GetBytes(publickey); var x509 = new X509Certificate2(_publicKeyBytes); using (var rsa = (System.Security.Cryptography.RSACng)x509.PublicKey.Key) { rsa.VerifyData(Encoding.UTF8.GetBytes(message), Convert.FromBase64String(signStr), HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); }