public class WXPayService : IPayService
{
public static readonly ILogger _logger = LogManager.GetCurrentClassLogger();
private static char[] constant =
{
'0','1','2','3','4','5','6','7','8','9',
'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z'
};
/// <summary>
///
/// </summary>
protected ReceivablePayConfig RecePayCfg { get; set; }
public WXPayService(IOptions<ReceivablePayConfig> recePayCfgOpt)
{
this.RecePayCfg = recePayCfgOpt.Value;
}
//public async Task<PayCallBackOutput>
public string AesGcmDecrypt(string associatedData, string nonce, string ciphertext)
{
GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine());
AeadParameters aeadParameters = new AeadParameters(
new KeyParameter(Encoding.UTF8.GetBytes(this.RecePayCfg.AES_KEY)),
128,
Encoding.UTF8.GetBytes(nonce),
Encoding.UTF8.GetBytes(associatedData));
gcmBlockCipher.Init(false, aeadParameters);
byte[] data = Convert.FromBase64String(ciphertext);
byte[] plaintext = new byte[gcmBlockCipher.GetOutputSize(data.Length)];
int length = gcmBlockCipher.ProcessBytes(data, 0, data.Length, plaintext, 0);
gcmBlockCipher.DoFinal(plaintext, length);
return Encoding.UTF8.GetString(plaintext);
}
private string ToUrl(IEnumerable<KeyValuePair<string, string>> parameters)
{
string buff = "";
foreach (KeyValuePair<string, string> pair in parameters)
{
if (string.IsNullOrEmpty(pair.Value))
{
continue;
}
if (pair.Key != "sign" && pair.Value.ToString() != "")
{
buff += pair.Key + "=" + pair.Value + "&";
}
}
buff = buff.Trim('&');
return buff;
}
private string GenerateRandomNumber(int length)
{
StringBuilder newRandom = new StringBuilder(62);
Random rd = new Random();
for (int i = 0; i < length; i++)
{
newRandom.Append(constant[rd.Next(62)]);
}
return newRandom.ToString();
}
private static long ToUnixEpochDate(DateTime date)
=> (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
/// <summary>
/// 统一下单v3(https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml)
/// </summary>
/// <typeparam name="I"></typeparam>
/// <param name="outData"></param>
/// <param name="parameters"></param>
/// <returns></returns>
public async Task<Dictionary<string, string>> UnifiedOrder<I>(I outData, params KeyValuePair<string, string>[] parameters) where I : new()
{
_logger.Info(new LogInfo() { Method = "UnifiedOrder", Argument = parameters, Description = "统一下单3" });
//判断是否存在参数
if (parameters?.Length <= 0)
{
throw new Exception("未提供参数");
}
string openid = string.Empty;
if (parameters.Count(t => string.Compare(t.Key, "openid", true) == 0 && !string.IsNullOrWhiteSpace(t.Value)) <= 0)
{
if (parameters.Count(t => string.Compare(t.Key, "code", true) == 0 && !string.IsNullOrWhiteSpace(t.Value)) <= 0)
{
throw new Exception("未提供code参数");
}
}
else
{
openid = parameters.FirstOrDefault(t => string.Compare(t.Key, "openid", true) == 0).Value;
}
var newparams = parameters.ToList();
newparams.Add(new KeyValuePair<string, string>("appid", RecePayCfg.appid));
newparams.Add(new KeyValuePair<string, string>("secret", RecePayCfg.secret));
newparams.Add(new KeyValuePair<string, string>("grant_type", RecePayCfg.grant_type));
WXUnifiedOrderReq req = new WXUnifiedOrderReq();
Dictionary<string, string> sParams2 = new Dictionary<string, string>();
//判断是否存在openid
if (!string.IsNullOrWhiteSpace(openid))
{
req.payer = new Payer() { openid = openid };
sParams2.Add("openid", openid);
}
else
{
string url = this.RecePayCfg.url + this.ToUrl(newparams.OrderBy(t => t.Key));
using (HttpClient client = new HttpClient())
{
var response = await client.GetAsync(url);
string contentstr = await response.Content.ReadAsStringAsync();
if (response.IsSuccessStatusCode)
{
WXCodeResult codeResult = null;
try
{
codeResult = JsonConvert.DeserializeObject<WXCodeResult>(contentstr);
}
catch (Exception ex)
{
_logger.Error(ex, "微信支付获取openid失败");
throw new SinoException("获取OpenId失败");
}
if (codeResult == null || string.IsNullOrEmpty(codeResult.openid))
{
_logger.Error(new SinoException(contentstr), "微信支付获取openid失败");
throw new SinoException("获取OpenId失败");
}
req.payer = new Payer() { openid = codeResult.openid };
sParams2.Add("openid", codeResult.openid);
}
else
{
_logger.Error(new SinoException(contentstr), "微信支付获取openid失败");
throw new SinoException("获取OpenId失败");
}
}
}
req.appid = RecePayCfg.appid;
req.mchid = RecePayCfg.mchid;
req.notify_url = RecePayCfg.notify_url;
if (outData is DependOutDataWXPay)
{
var tempData = outData as DependOutDataWXPay;
req.amount = new Amount() { total = tempData.total };
req.description = tempData.description;
req.out_trade_no = tempData.out_trade_no;
}
else
{
throw new Exception("入参类型不正确");
}
var nonceStr = GenerateRandomNumber(32);
using (var httpClient = new HttpClient(new WXPayHttpHandler(RecePayCfg.mchid, RecePayCfg.serial_no, "apiclient_cert.p12", RecePayCfg.mchid, nonceStr)))
{
string postUrl = RecePayCfg.UnifiedOrderUrl;
// POST 方式
//一定要这样传递参数,不然在加密签名的时候获取到的参数就是\u0这种形式的数据了,不是传递的这样的数据了,导致加密的结果不正确
string jsonData = JsonConvert.SerializeObject(req);
_logger.Info($"统一下单参数:{jsonData}");
var bodyJson = new StringContent(jsonData, Encoding.UTF8, "application/json");
httpClient.Timeout = TimeSpan.FromSeconds(10);
HttpResponseMessage unifiedOrderRes = null;
try
{
unifiedOrderRes = await httpClient.PostAsync(postUrl, bodyJson);
}
catch (Exception ex)
{
throw new SinoException("微信统一下单失败", ex);
}
_logger.Info("httpclient请求结束");
// prepay_id。
var postResult = await unifiedOrderRes.Content.ReadAsStringAsync();
_logger.Info($"httpclient获取结果{postResult}");
if (!unifiedOrderRes.IsSuccessStatusCode)
{
_logger.Error(new Exception(postResult), "微信统一下单失败");
throw new SinoException("微信统一下单失败");
}
UnifiedOrderRes unifiedOrder = JsonConvert.DeserializeObject<UnifiedOrderRes>(postResult);
//二次签名
string timestamp = ToUnixEpochDate(DateTime.Now).ToString();
string package = $"prepay_id={unifiedOrder.prepay_id}";
sParams2.Add(nameof(RecePayCfg.appid), RecePayCfg.appid);
sParams2.Add("nonceStr", nonceStr);
sParams2.Add("timeStamp", timestamp);
sParams2.Add("package", package);
sParams2.Add("signType", "RSA");
//需要签名的字符串
string needSignStr = $"{RecePayCfg.appid}
{timestamp}
{nonceStr}
{package}
";
_logger.Info($"paySign签名前:{needSignStr}");
string paySign = needSignStr.WXRSAWithSHA256Sign("apiclient_cert.p12", RecePayCfg.mchid);
_logger.Info($"paySign签名结果:{paySign}");
sParams2.Add("paySign", paySign);
return sParams2;
}
}
/// <summary>
/// 商户订单查询
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="input"></param>
/// <returns></returns>
public async Task<DecryptionCallBack> PayOrderSearch<T>(T input) where T : new()
{
DecryptionCallBack retVal = null;
PayOrderSearch result = null;
if (input is PayOrderSearch)
{
result = input as PayOrderSearch;
}
else
{
throw new Exception("入参类型不正确");
}
var nonceStr = GenerateRandomNumber(32);
using (var httpClient = new HttpClient(new WXPayHttpHandler(RecePayCfg.mchid, RecePayCfg.serial_no, "apiclient_cert.p12", RecePayCfg.mchid, nonceStr)))
{
string getUrl = $"{RecePayCfg.PayResultSearchUrl}{result.out_trade_no}?mchid={RecePayCfg.mchid}";
var httpResponse = await httpClient.GetAsync(getUrl);
string contentstr = await httpResponse.Content.ReadAsStringAsync();
if (httpResponse.IsSuccessStatusCode)
{
retVal = JsonConvert.DeserializeObject<DecryptionCallBack>(contentstr);
}
else
{
_logger.Error(new SinoException(contentstr), "查询微信订单失败");
throw new SinoException("查询微信订单失败");
}
}
return retVal;
}
}
public static class StringExtension
{
public static string RSAWithPrivateKey(this string message,string certPath)
{
//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));
//}
return string.Empty;
//try
//{
// RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
// byte[] priKeyBytes = Convert.FromBase64String(privateKey);
// rsa.ImportCspBlob(priKeyBytes);
// byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
// return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
//}
//catch
//{
// 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));
// }
//}
//NLog.ILogger _logger = NLog.LogManager.GetCurrentClassLogger();
//_logger.Info("进入RSA签名");
//X509Certificate2 cer = new X509Certificate2(certPath);
//if (cer != null)//获取公钥
//{
// RSA rsa = cer.GetRSAPrivateKey();
// //查看在不同平台上的具体类型
// _logger.Info($"RSA类型:{rsa.GetType().FullName}");
// byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
// return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
// //var isSig = pubkey.VerifyData(butys, CCB_ALG, verify);//验证信息
//}
//else
//{
// throw new Exception("证书解析失败");
//}
}
/// <summary>
/// 微信RSA SHA256 签名
/// </summary>
/// <param name="message"></param>
/// <param name="privateKey"></param>
/// <returns></returns>
public static string WXRSAWithSHA256Sign(this string message, string certPath, string certPwd)
{
//NLog.ILogger _logger = NLog.LogManager.GetCurrentClassLogger();
X509Certificate2 cer = new X509Certificate2(certPath, certPwd, X509KeyStorageFlags.Exportable);
if (cer != null)//获取公钥
{
RSA rsa = cer.GetRSAPrivateKey();
//查看在不同平台上的具体类型
//_logger.Info($"RSA类型:{rsa.GetType().FullName}");
byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
return Convert.ToBase64String(rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1));
}
else
{
throw new Exception("证书解析失败");
}
}
}
// 使用方法
// HttpClient client = new HttpClient(new HttpHandler("{商户号}", "{商户证书序列号}"));
// var response = client.GetAsync("https://api.mch.weixin.qq.com/v3/certificates");
public class WXPayHttpHandler : DelegatingHandler
{
private readonly string merchantId;
private readonly string serialNo;
private readonly string certPath;
private readonly string certPwd;
private readonly string nonceStr;
public WXPayHttpHandler(string merchantId, string merchantSerialNo, string certPath, string certPwd, string nonceStr)
{
InnerHandler = new HttpClientHandler();
this.merchantId = merchantId;
this.serialNo = merchantSerialNo;
this.certPath = certPath;
this.certPwd = certPwd;
this.nonceStr = nonceStr;
}
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("Accept", "application/json");//如果缺少这句代码就会导致下单接口请求失败,报400错误(Bad Request)
request.Headers.Add("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.0.3705;)");//如果缺少这句代码就会导致下单接口请求失败,报400错误(Bad Request)
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();//debug的时候在这里打个断点,看看body的值是多少,如果跟你传入的参数不一致,说明是有问题的,一定参考我的方法
}
string uri = request.RequestUri.PathAndQuery;
var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
string nonce = this.nonceStr;
string message = $"{method}
{uri}
{timestamp}
{nonce}
{body}
";
string signature = message.WXRSAWithSHA256Sign(this.certPath, this.certPwd);
return $"mch>;
}
//protected string Sign(string message)
//{
// // NOTE: 私钥不包括私钥文件起始的-----BEGIN PRIVATE KEY-----
// // 亦不包括结尾的-----END PRIVATE KEY-----
// string privateKey = this.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));
// }
//}
}
/// <summary>
/// 应收微信支付配置
/// </summary>
public class ReceivablePayConfig
{
/// <summary>
/// 主机地址
/// </summary>
public string Host { get; set; }
/// <summary>
/// 路由地址
/// </summary>
public string Route { get; set; }
/// <summary>
/// 生成二维码图片的像素大小 ,我这里设置的是5
/// </summary>
public int? PixelsPerModule { get; set; }
/// <summary>
/// 根据code获取微信授权的地址
/// </summary>
public string url { get; set; }
/// <summary>
/// appid
/// </summary>
public string appid { get; set; }
/// <summary>
///
/// </summary>
public string secret { get; set; }
/// <summary>
/// 授权方式
/// </summary>
public string grant_type { get; set; }
/// <summary>
/// 商户号
/// </summary>
public string mchid { get; set; }
/// <summary>
/// 商户序列号
/// </summary>
public string serial_no { get; set; }
/// <summary>
/// 回调通知地址
/// </summary>
public string notify_url { get; set; }
/// <summary>
/// 统一下单url
/// </summary>
public string UnifiedOrderUrl { get; set; }
/// <summary>
/// 商户订单号查询
/// </summary>
public string PayResultSearchUrl { get; set; }
/// <summary>
/// 微信支付私钥
/// </summary>
public string PrivateKey { get; set; }
/// <summary>
/// AES_KEY 微信支付回调使用
/// </summary>
public string AES_KEY { get; set; }
}
public class DependOutDataWXPay
{
/// <summary>
/// 订单总金额,单位为分
/// </summary>
public int total { get; set; }
/// <summary>
/// 商户订单号
/// </summary>
public string out_trade_no { get; set; }
/// <summary>
/// 商品描述
/// </summary>
public string description { get; set; }
}
public class PayOrderSearch
{
/// <summary>
/// 商户订单号
/// </summary>
public string out_trade_no { get; set; }
}
/// <summary>
/// 订单金额
/// </summary>
public class Amount
{
/// <summary>
/// 订单总金额,单位为分
/// </summary>
public int total { get; set; }
/// <summary>
/// CNY:人民币,境内商户号仅支持人民币。
/// </summary>
public string currency { get; set; } = "CNY";
}
/// <summary>
/// 支付者
/// </summary>
public class Payer
{
/// <summary>
/// 用户在直连商户appid下的唯一标识。
/// </summary>
public string openid { get; set; }
}
/// <summary>
/// 微信统一下单JSAPI(v3)
/// </summary>
public class WXUnifiedOrderReq
{
/// <summary>
/// 直连商户申请的公众号appid
/// </summary>
public string appid { get; set; }
/// <summary>
/// 直连商户的商户号,由微信支付生成并下发
/// </summary>
public string mchid { get; set; }
/// <summary>
/// 商品描述
/// </summary>
public string description { get; set; }
/// <summary>
/// 商户订单号
/// </summary>
public string out_trade_no { get; set; }
/// <summary>
/// 通知URL必须为直接可访问的URL,不允许携带查询串,要求必须为https地址。
/// </summary>
public string notify_url { get; set; }
/// <summary>
/// 订单金额
/// </summary>
public Amount amount { get; set; }
/// <summary>
/// 支付者
/// </summary>
public Payer payer { get; set; }
}
public class WXCodeResult
{
[JsonProperty("access_token")]
public string access_token { get; set; }
[JsonProperty("expires_in")]
public int expires_in { get; set; }
[JsonProperty("refresh_token")]
public string refresh_token { get; set; }
[JsonProperty("openid")]
public string openid { get; set; }
[JsonProperty("scope")]
public string scope { get; set; }
}
public class UnifiedOrderRes
{
public string prepay_id { get; set; }
}
{
"ReceivablePayConfig": {
"Host": "", /*跳转前端的域名*/
"Route": "", /* 跳转前端的路由 */
"PixelsPerModule": 5,
"url": "https://api.weixin.qq.com/sns/oauth2/access_token?",
"appid": "",
"secret": "",
"grant_type": "authorization_code",
"mchid": "",
/* 商户证书序列号 */
"serial_no": "",
"notify_url": "" /* 回调通知地址 */,
"UnifiedOrderUrl": "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi" /* 统一下单url */,
"PayResultSearchUrl": "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/", /* 商户订单号查询*/
/* AES_KEY */
"AES_KEY": "DCE2C90F34544BB1901DB0459033FBB0",
/*微信支付私钥*/
"PrivateKey": ""
}
}