评论

.Net Core(C#)微信支付签名和验签最全代码共享

C#早已经进入.NetCore跨平台时代了,我们已经在Linux上运行.NetCore好几年了。但微信支付的官方技术文档,在C#方面特别落后。很多代码并不兼容Linux,例如签名生成不支持Linux

总所周知,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 { getset; }

        /// <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~

最后一次编辑于  03-08  
点赞 1
收藏
评论
登录 后发表内容