总所周知,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~