- 电商收付通的修改结算帐号API是否支持二级商户?
微信支付已经明确指明3种商户类型的却别: 普通商户、特约商户和二级商户有什么区别?普通商户:商户自行申请入驻微信支付,无服务商协助; 特约商户:由普通服务商,协助完成入驻的商户; 二级商户:由开通了“电商工具箱”的服务商(别称 电商平台),协助完成入驻的商户。 (来自)https://kf.qq.com/faq/200316bYRf63200316eqUziQ.html 但电商收付通的文档指明接口【修改结算帐号API】只针对特约商户,但这个文档是跟二级商户的文档丢一块的,所以有个疑问: 该接口是否支持修改二级商户结算账户??? 如果支持,请注明支持;如果不支持,也请注明不支持。。 接口文档:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/ecommerce/applyments/chapter3_4.shtml [图片] 【补充】 在这个页面显示又说是支持二级商户。。。点击进去又说是特约商户 https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/pages/ecommerce.shtml [图片]
2020-10-21 - .Net Core(C#)微信支付签名和验签最全代码共享
总所周知,C#早已经进入.NetCore跨平台时代了,我们已经在Linux上运行.NetCore好几年了。。。但微信支付的官方技术文档,在C#方面特别落后。很多代码并不兼容Linux,例如签名生成 https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-sheng-cheng 为了方便大伙,我特地把我的C#代码共享出来,亲测Cent OS完美运行。 1)首先,在C#里,无论公钥还是私钥,都是XML格式的,所以很多时候,需要将C#的公钥/私钥格式和JAVA的公钥/私钥格式互转,所以先来个RSAKeyConvert.cs /// <summary> /// RSA密钥转换器 /// </summary> public abstract class RSAKeyConvert { /// <summary> /// RSA私钥格式转换,java->.net /// </summary> /// <param name="privateKey">java生成的RSA私钥</param> /// <returns></returns> public static string RSAPrivateKeyJava2DotNet(string privateKey) { var privateKeyParam =(RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey)); return string.Format( "<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>", Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned())); } /// <summary> /// RSA私钥格式转换,.net->java /// </summary> /// <param name="privateKey">.net生成的私钥</param> /// <returns></returns> public static string RSAPrivateKeyDotNet2Java(string privateKey) { XmlDocument doc = new XmlDocument(); doc.LoadXml(privateKey); BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText)); BigInteger exp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText)); BigInteger d = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("D")[0].InnerText)); BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("P")[0].InnerText)); BigInteger q = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Q")[0].InnerText)); BigInteger dp = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DP")[0].InnerText)); BigInteger dq = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("DQ")[0].InnerText)); BigInteger qinv = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("InverseQ")[0].InnerText)); RsaPrivateCrtKeyParameters privateKeyParam = new RsaPrivateCrtKeyParameters(m, exp, d, p, q, dp, dq, qinv); PrivateKeyInfo privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(privateKeyParam); byte[] serializedPrivateBytes = privateKeyInfo.ToAsn1Object().GetEncoded(); return Convert.ToBase64String(serializedPrivateBytes); } /// <summary> /// RSA公钥格式转换,java->.net /// </summary> /// <param name="publicKey">java生成的公钥</param> /// <returns></returns> public static string RSAPublicKeyJava2DotNet(string publicKey) { RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey)); return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>", Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()), Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned())); } /// <summary> /// RSA公钥格式转换,.net->java /// </summary> /// <param name="publicKey">.net生成的公钥</param> /// <returns></returns> public static string RSAPublicKeyDotNet2Java(string publicKey) { XmlDocument doc = new XmlDocument(); doc.LoadXml(publicKey); BigInteger m = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Modulus")[0].InnerText)); BigInteger p = new BigInteger(1, Convert.FromBase64String(doc.DocumentElement.GetElementsByTagName("Exponent")[0].InnerText)); RsaKeyParameters pub = new RsaKeyParameters(false, m, p); SubjectPublicKeyInfo publicKeyInfo = SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(pub); byte[] serializedPublicBytes = publicKeyInfo.ToAsn1Object().GetDerEncoded(); return Convert.ToBase64String(serializedPublicBytes); } } 2)根据官方提供的技术文档进行修改重写:HttpHandler.cs // 使用方法 // HttpClient client = new HttpClient(new HttpHandler("{商户号}", "{商户证书序列号}","{商户私钥(私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----,亦不包括结尾的-----END PRIVATE KEY-----)}")); // ... // var response = client.GetAsync("https://api.mch.weixin.qq.com/v3/certificates"); public class HttpHandler : DelegatingHandler { private readonly string merchantId; private readonly string serialNo; private readonly string privateKey; /// <summary> /// HTTP句柄 /// </summary> /// <param name="merchantId">商户号</param> /// <param name="merchantSerialNo">商户证书序列号</param> /// <param name="privateKey">商户私钥(私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----,亦不包括结尾的-----END PRIVATE KEY-----)</param> public HttpHandler(string merchantId, string merchantSerialNo,string privateKey) { InnerHandler = new HttpClientHandler(); this.merchantId = merchantId; this.serialNo = merchantSerialNo; this.privateKey = privateKey; } protected async override Task<HttpResponseMessage> SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { var auth = await BuildAuthAsync(request); string value = $"WECHATPAY2-SHA256-RSA2048 {auth}"; request.Headers.Add("Authorization", value); request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.143 Safari/537.36"); request.Headers.Add("Accept", "application/json"); return await base.SendAsync(request, cancellationToken); } protected async Task<string> BuildAuthAsync(HttpRequestMessage request) { string method = request.Method.ToString(); string body = ""; if (method == "POST" || method == "PUT" || method == "PATCH") { var content = request.Content; body = await content.ReadAsStringAsync(); } string uri = request.RequestUri.PathAndQuery; var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds(); string nonce = Path.GetRandomFileName(); string message = $"{method}\n{uri}\n{timestamp}\n{nonce}\n{body}\n"; string signature = Sign(message); return $"mchid=\"{merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\",signature=\"{signature}\""; } //此方法不支持linux平台 //protected string Sign(string message) //{ // // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY----- // // 亦不包括结尾的-----END PRIVATE KEY----- // //string privateKey = "{你的私钥}"; // byte[] keyData = Convert.FromBase64String(privateKey); // using (CngKey cngKey = CngKey.Import(keyData, CngKeyBlobFormat.Pkcs8PrivateBlob)) // using (RSACng rsa = new RSACng(cngKey)) // { // byte[] data = System.Text.Encoding.UTF8.GetBytes(message); // return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)); // } //} protected string Sign(string message) { // SHA256withRSA //根据需要加签时的哈希算法转化成对应的hash字符节 //byte[] bt = Encoding.GetEncoding("utf-8").GetBytes(str); byte[] bt =Encoding.UTF8.GetBytes(message); var sha256 = new SHA256CryptoServiceProvider(); byte[] rgbHash = sha256.ComputeHash(bt); RSACryptoServiceProvider key = new RSACryptoServiceProvider(); var _privateKey = RSAKeyConvert.RSAPrivateKeyJava2DotNet(privateKey); key.FromXmlString(_privateKey); RSAPKCS1SignatureFormatter formatter = new RSAPKCS1SignatureFormatter(key); formatter.SetHashAlgorithm("SHA256");//此处是你需要加签的hash算法,需要和上边你计算的hash值的算法一致,不然会报错。 byte[] inArray = formatter.CreateSignature(rgbHash); return Convert.ToBase64String(inArray); } } 到这里,签名部分已完成,主要修改了Sign(string message)函数,将JAVA的私钥格式转化为C#的私钥格式(XML),然后再进行签名~搞定! 3)写完签名之后,再写一个验签类:WechatPayCer.cs和HttpSignature.cs /// <summary> /// 微信平台证书 /// </summary> public class WechatPayCer { /// <summary> /// 证书序列号 /// </summary> public string SerialNo { get; private set; } /// <summary> /// 证书内容 /// </summary> public string Data { get;private set; } /// <summary> /// 签名器 /// </summary> private ISigner Signer { get; set; } /// <summary> /// 微信平台证书 /// </summary> /// <param name="serialNo"></param> /// <param name="data"></param> public WechatPayCer(string serialNo,string data) { this.SerialNo = serialNo; this.Data = data.Trim().Replace("-----BEGIN CERTIFICATE-----", "").Replace("-----END CERTIFICATE-----", "").Replace("\r", "").Replace("\n", ""); var bs = Convert.FromBase64String(this.Data); var x509 = new X509Certificate2(bs); var rsa = x509.PublicKey.Key; var publickey = rsa.ToXmlString(false); publickey=RSAKeyConvert.RSAPublicKeyDotNet2Java(publickey); Signer = SignerUtilities.GetSigner("SHA256WithRSA"); var publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publickey)); Signer.Init(false, publicKeyParam); } /// <summary> /// 验证签名是否匹配 /// </summary> /// <param name="message"></param> /// <param name="signature"></param> /// <returns></returns> public bool Verify(string message, string signature) { var signature_bs = Convert.FromBase64String(signature); var message_bs = Encoding.UTF8.GetBytes(message); Signer.BlockUpdate(message_bs, 0, message_bs.Length); var r = Signer.VerifySignature(signature_bs); return r; } } /// <summary> /// 微信支付HTTP签名 /// </summary> public abstract class HttpSignature { /// <summary> /// 验证签名 /// </summary> /// <param name="response">HTTP响应</param> /// <param name="get_cer">根据证书序列获取证书</param> /// <returns>签名验证成功返回(true,主体内容),否则返回(false,null)</returns> public static async Task<(bool result,string body)> VerificateAsync(HttpResponseMessage response,Func<string, WechatPayCer> get_cer) { var ctx = response.Content; string body; switch (response.StatusCode) { case System.Net.HttpStatusCode.OK: body = await ctx.ReadAsStringAsync(); break; case System.Net.HttpStatusCode.NoContent: body = null; break; case System.Net.HttpStatusCode.ServiceUnavailable: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:503 - Service Unavailable,服务不可用,过载保护")); case System.Net.HttpStatusCode.BadGateway: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:502 - Bad Gateway,服务下线,暂时不可用")); case System.Net.HttpStatusCode.InternalServerError: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:500 - Server Error,系统错误")); case System.Net.HttpStatusCode.TooManyRequests: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:429 - Too Many Requests,请求超过频率限制")); case System.Net.HttpStatusCode.NotFound: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:404 - Not Found,请求的资源不存在")); case System.Net.HttpStatusCode.Forbidden: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:403 - Forbidden,权限异常")); case System.Net.HttpStatusCode.Unauthorized: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:401 - Unauthorized,签名验证失败")); case System.Net.HttpStatusCode.BadRequest: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:400 - Bad Request,协议或者参数非法")); default: throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}响应错误:未知响应状态{(int)response.StatusCode}-{response.StatusCode.ToString()}")); }; string timestamp; string nonce; string signature; string serial; WechatPayCer cer; try { timestamp = response.Headers.GetValues("Wechatpay-Timestamp").First(); } catch { return (false,null); } try { nonce = response.Headers.GetValues("Wechatpay-Nonce").First(); } catch { return (false, null); } try { serial = response.Headers.GetValues("Wechatpay-Serial").First(); } catch { return (false, null); } cer= get_cer(serial); if (cer == null) { throw (new Exception($"微信接口{response.RequestMessage.RequestUri.ToString()}发生错误:未找到相应证书(序列号{serial})")); } try { signature = response.Headers.GetValues("Wechatpay-Signature").First(); } catch { return (false, null); } //将响应内容进行签名 var s = $"{timestamp}\n{nonce}\n{body??""}\n"; var r = cer.Verify(s,signature); if (r) { return (true, body); } else { return (false,null); } } } 同样的可以看到,在WechatPayCer类里的也引用了RSAKeyConvert.RSAPublicKeyDotNet2Java方法,将C#的公钥(XML格式)转化为JAVA的公钥格式,然后再进行验签~完美搞定! 其实方法并不难,只是很多人不知道C#的公钥和私钥都是XML格式,而JAVA的格式不是。然后导出套代码,要么就是windows运行成功了,linux报错不支持;要么就是各种报错。 强烈建议将该文档列为官方文档,顶替原有demo~
2020-03-08