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}\n{timestamp}\n{nonceStr}\n{package}\n"; _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}\n{uri}\n{timestamp}\n{nonce}\n{body}\n"; string signature = message.WXRSAWithSHA256Sign(this.certPath, this.certPwd); return $"mchid=\"{merchantId}\",nonce_str=\"{nonce}\",timestamp=\"{timestamp}\",serial_no=\"{serialNo}\",signature=\"{signature}\""; } //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": "" } }