前言

前段时间研究了下微信支付-小程序支付的功能。但微信支付文档中关于.net C#的语言的sdk没有,只有java go 和php版本的,当然社区也有很多已经集成好的微信支付.net core sdk,比如盛派家的(Senparc.Weixin)DotNetCore.SKIT.FlurlHttpClient.Wechat , paylink等,但他们的框架又都太臃肿了(功能太多太强大集成度也复杂),我只需要个小程序微信支付即可,而且用他们的sdk还得先学习下他们的文档,拿来主义固然好,主要的是我希望更清晰的了解微信支付的流程和集成方式,使用他们的sdk等以后对微信支付流程有更清晰的了解后再用也不迟,所以打算自己实现。在此整理下以防自己忘了。

接入前准备这里就不提了,参考官方文档,一步一步来就可以了。

实现思路

java和C# 基本上差不多,照着Java的sdk来就可以了。

这里涉及到几个关键配置。

  • 商户号 mchid
  • 商户注册申请和商户apiv3证书(APIv3证书私钥) private.key 调用微信api时签名使用
  • 商户apiv3证书密钥 merchant_serial_no
  • 微信平台证书的公钥 public.key 验证微信响应时验签使用
  • 小程序的 appid
  • 对应小程序的支付用户的openid

小程序支付流程

发起支付

发起支付的文档参考小程序下单

这里的小程序下单并不会直接发起支付,而是生成一个预订单号,prepay_id,当有了这个id后然后再调用小程序发起支付接口进行实际支付。

以下是代码小程序下单的C#版

/// <summary>
/// 下单对应的请求DTO
/// </summary>
public class WeiXinPayOrderRequestDTO
{
/// <summary>
/// 应用id
/// </summary>
public string appid { get; set; } /// <summary>
/// 商户id
/// </summary>
public string mchid { get; set; } /// <summary>
/// 商品描述
/// </summary>
public string description { get; set; } /// <summary>
/// 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。
/// </summary>
public string out_trade_no { get; set; } /// <summary>
/// 订单失效时间,遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,
/// T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒,TIMEZONE表示时区
/// (+08:00表示东八区时间,领先UTC8小时,即北京时间)。
/// 例如:2015-05-20T13:29:35+08:00表示,北京时间2015年5月20日 13点29分35秒
/// </summary>
public string time_expire { get; set; } /// <summary>
/// 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用,实际情况下只有支付完成状态才会返回该字段。
/// 本系统存放的是订单id
/// </summary>
public string attach { get; set; } /// <summary>
/// 通知URL必须为直接可访问的URL,不允许携带查询串,要求必须为https地址。
/// </summary>
public string notify_url { get; set; } /// <summary>
/// 是否开通发票
/// </summary>
public bool support_fapiao { get; private set; } = false; /// <summary>
/// 订单金额
/// </summary>
public WeiXinPayOrderAmout amount { get; set; } /// <summary>
/// 支付人
/// </summary>
public WeiXinPayOrderPayer payer { get; set; } /// <summary>
/// 结算信息
/// </summary>
public WeiXinPayOrderSettleInfo settle_info { get; private set; } = new WeiXinPayOrderSettleInfo
{
profit_sharing = false,
};
} public class WeiXinPayOrderAmout
{
/// <summary>
///总金额
/// </summary>
public int total { get; set; } /// <summary>
/// CNY:人民币,境内商户号仅支持人民币。
/// </summary>
public string currency { get; private set; } = "CNY";
}
public class WeiXinPayOrderPayer
{
/// <summary>
/// 小程序对应的用户openid
/// </summary>
public string openid { get; set; }
} public class WeiXinPayOrderSettleInfo
{
/// <summary>
/// 是否指定分账
/// </summary>
public bool profit_sharing { get; set; }
}

接下来是提交订单

 public async Task<WeiXinPayOrderResponseDTO> SubmitOrderAsync(WeiXinPayOrderRequestDTO request)
{
string nonceStr = StringUtils.RandomNonceString(10);
long timeStamp = DateTimeHelper.NowTimeStamp();
HttpRequestMessage httpRequest = new()
{
Method = HttpMethod.Post
};
request.appid = options.AppId;
request.mchid = options.Mchid;
string json = request.ToJson();
httpRequest.Content = new StringContent(json, Encoding.UTF8, "application/json");
httpRequest.Headers.Add("Accept", "*/*");
httpRequest.Headers.Add("User-Agent", "dotnet/6.0"); httpRequest.RequestUri = new Uri("https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi");
string orderTosignData = BuildSubmitOrderToSignData(httpRequest.Method, httpRequest.RequestUri, timeStamp, nonceStr, json);
string signature = "";
// 创建RSA加密服务提供程序实例
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// 加载私钥
rsa.ImportFromPem(options.ApiPrivateKeyContent); // 将待签名的数据转换为字节数组
byte[] dataBytes = Encoding.UTF8.GetBytes(orderTosignData); // 计算SHA256 with RSA签名
byte[] signatureBytes = rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // 将签名结果进行Base64编码
signature = Convert.ToBase64String(signatureBytes); // 输出签名值
Console.WriteLine("签名值: " + signature);
}
string authrizationValue = $"mchid=\"{options.Mchid}\",serial_no=\"{options.ApiSerialNumber}\",nonce_str=\"{nonceStr}\",timestamp=\"{timeStamp}\",signature=\"{signature}\"";
httpRequest.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("WECHATPAY2-SHA256-RSA2048", authrizationValue);
HttpResponseMessage response = await httpClient.SendAsync(httpRequest);
string responseString = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response: {responseString}");
var orderResult = Newtonsoft.Json.JsonConvert.DeserializeObject<WeiXinPayOrderResponseDTO>(responseString); return orderResult;
}

然后是调用微信支付所需要的requestPayment body

public class WeiXinPayrequestPayment
{
/// <summary>
/// 时间戳
/// </summary>
public string timeStamp { get; set; } /// <summary>
/// 随机字符
/// </summary>
public string nonceStr { get; set; } /// <summary>
/// 支付的预支付号
/// </summary>
public string package { get; set; } /// <summary>
/// 签名类型
/// </summary>
public string signType { get; } = "RSA"; /// <summary>
/// 小程序支付发起的签名
/// </summary>
public string paySign { get; set; } }
public async Task<WeiXinPayrequestPayment> WeiXinPayrequestPayment(string prepayId)
{
string nonceStr = StringUtils.RandomNonceString(16);
string timeStamp = DateTimeHelper.NowTimeStamp().ToString();
string package = $"prepay_id={prepayId}";
string dataToSign = options.AppId + "\n" + timeStamp + "\n" + nonceStr + "\n" + package + "\n";
string sign = "";
// 创建RSA加密服务提供程序实例
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
{
// 加载私钥
rsa.ImportFromPem(options.ApiPrivateKeyContent); // 将待签名的数据转换为字节数组
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign); // 计算SHA256 with RSA签名
byte[] signatureBytes = rsa.SignData(dataBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); // 将签名结果进行Base64编码
sign = Convert.ToBase64String(signatureBytes);
}
return new WeiXinPayrequestPayment
{
nonceStr = nonceStr,
package = package,
paySign = sign,
timeStamp = timeStamp,
};
}

微信异步通知的验签与解密

解密

微信异步通知的body中存在加密部分, 其中的body中ciphertext字段是密文,商户端需要使用apiv3的密钥进行解密,文档参见

如何加密解密敏感信息代码如下:

        private async Task<string> WechatAesGcmDecrypt(string associatedData, string nonce, string ciphertext)
{
GcmBlockCipher gcmBlockCipher = new GcmBlockCipher(new AesEngine()); AeadParameters aeadParameters = new AeadParameters(
new KeyParameter(Encoding.UTF8.GetBytes(options.ApiAESKey)),// 商户APIv3 密钥
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);
}

验签:

由于签名算法中,调用微信api后微信响应的数据(response)和微信主动请求商户地址的数据(wechat request)都是使用微信平台证书的私钥进行签名的。尤为注意,验签这里必须使用微信平台证书中的公钥public_key进行验签操作。

 public async Task<bool> VerifyWeiXinSign(string nonce, long timestamp, string serial, string body, string signature)
{
logger.LogInformation($"开始验证微信签名,\n nonce:{nonce}" + $"\n timestamp:{timestamp}"
+ $"\n serial:{serial}\n body:{body}\n signature:{signature}");
try
{
// 将Unix时间戳转换为DateTime
DateTime dateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(timestamp).ToLocalTime();
// 获取当前时间
DateTime currentTime = DateTime.Now;
// 计算时间差
TimeSpan timeDifference = currentTime.Subtract(dateTime);
if (timeDifference.Minutes > 20)
{
throw new Exception("当前请求已过期");
}
string dataToSign = $"{timestamp}\n{nonce}\n{body}\n";
// 待验签的数据
byte[] dataBytes = Encoding.UTF8.GetBytes(dataToSign); using var reader = new StringReader(options.WechatPlatformPublicKey);
var pemReader = new PemReader(reader);
object obj = pemReader.ReadObject();
RsaKeyParameters publicKeyParams = null;
if (obj is RsaKeyParameters)
{
publicKeyParams = (RsaKeyParameters)obj;
}
else
{
throw new Exception("公钥内容加载失败,Invalid public key format.");
} // 签名
byte[] signatureBytes = Convert.FromBase64String(signature);
var verifier = SignerUtilities.GetSigner("SHA256withRSA");
verifier.Init(false, publicKeyParams);
verifier.BlockUpdate(dataBytes, 0, dataBytes.Length);
bool isVaild = verifier.VerifySignature(signatureBytes);
logger.LogInformation($"微信验证结果 nonce:{nonce} isVaild:{isVaild}");
return isVaild;
}
catch (Exception ex)
{
throw;
}
finally
{
logger.LogInformation($"结束验证微信签名");
}
}

注意事项

  1. 第三方(商户)调用微信api的签名中使用的密钥 都是商户apiv3证书的私钥进行签名的也就是private_key。
  2. 在微信通知到第三方的通知url上或者调用完微信接口后的Response,必须使用微信平台证书的公钥进行验签的,也就是publick_key。
  3. 由于微信调整,微信平台证书可能会定期更换,这里需要定期更新微信平台证书的公钥。具体参考平台证书更换指引由于我只处于研究阶段,所以这里的微信平台证书自动更换就没往下研究。

参考链接

微信支付文档-小程序支付场景

微信支付文档-接入前准备-小程序支付

微信支付文档-签名

微信支付文档-关键概念

.net core 微信支付-微信小程序支付(服务端C#代码)的更多相关文章

  1. uni-app - 支付(app支付、小程序支付、h5(微信端)支付)

    App支付.小程序支付.h5(微信端)支付 APP支付(内置) appPay.js /** * 5+App支付,仅支持支付宝以及微信支付 * * 支付宝Sdk集成,微信sdk未集成 * * @para ...

  2. 微信支付之扫码支付、公众号支付、H5支付、小程序支付相关业务流程分析总结

    前言 很久以来,一直想写一篇微信支付有关的总结文档:一方面是总结自己的一些心得,另一方面也可以帮助别人,但是因种种原因未能完全理解透彻微信支付的几大支付方式,今天有幸做一些总结上的文章,也趁此机会,将 ...

  3. nodejs+koa2微信app支付,小程序支付

    企业付款到零钱文档:https://pay.weixin.qq.com/wiki/doc/api/tools/mch_pay.php?chapter=14_2 1,搞微信支付,先看流程图 https: ...

  4. 微信小程序支付、小程序支付功能、小程序支付方法、微信小程序支付方法

    相信大家在做小程序的时候不可避免的会碰到支付功能小程序的支付和pc的是有区别的小程序的支付方法为 wx.requestPayment wx.requestPayment({ timeStamp: '' ...

  5. Vue+koa2开发一款全栈小程序(5.服务端环境搭建和项目初始化)

    1.微信公众平台小程序关联腾讯云 腾讯云的开发环境是给免费的一个后台,但是只能够用于开发,如果用于生产是需要花钱的,我们先用开发环境吧 1.用小程序开发邮箱账号登录微信公众平台 2.[设置]→[开发者 ...

  6. 多个微信小程序一个服务端架构

    由于某些特定的业务场景,当多个小程序需要一个服务端后台提供数据时,大家可能想到是HTTP路由.是的,实际上我们使用微服务的GateWay网关也是一样的,如下图微服务架构: 网关GateWay的作用在于 ...

  7. 微信小程序post 服务端无法获得参数问题

    header中需要改为 "Content-Type": "application/x-www-form-urlencoded"

  8. 微信小程序支付源码,后台服务端代码

    作者:尹华南,来自原文地址 微信小程序支付绕坑指南 步骤 A:小程序向服务端发送商品详情.金额.openid B:服务端向微信统一下单 C:服务器收到返回信息二次签名发回给小程序 D:小程序发起支付 ...

  9. 微信支付之扫码、APP、小程序支付接入详解

    做电商平台的小伙伴都知道,支付服务是必不可少的一部分,今天我们开始就说说支付服务的接入及实现.目前在国内,几乎90%中小公司的支付系统都离不开微信支付和支付宝支付.那么大家要思考了,为什么微信支付和支 ...

  10. 微信h5支付/jsapi支付/小程序支付

    一. 介绍------------------------------------------------------------------ 微信支付官方开发文档:  https://pay.wei ...

随机推荐

  1. 修复HTTP动词篡改导致的认证旁路问题的方法

    本文于2016年4月完成,发布在个人博客网站上. 考虑个人博客因某种原因无法修复,于是在博客园安家,之前发布的文章逐步搬迁过来. 诡异的问题 分析AppScan扫描报告的时候,发现报告里提示" ...

  2. JDK9的新特性:JVM的xlog

    目录 简介 xlog的使用 selections output decorators 总结 简介 在java程序中,我们通过日志来定位和发现项目中可能出现的问题.在现代java项目中,我们使用log4 ...

  3. 许北林:我为什么加入OpenHarmony生态?又为什么要做“启航KP”开发套件?

    许北林 软通动力 资深项目经理 在全球开源趋势下,中国正逐渐成为全球开源软件的主要使用者和核心贡献者.今天我们来认识一位接触 OpenHarmony 不到一年,便带领团队成功开发出一款"启航 ...

  4. Pdfium.Net.Free 一个免费的Pdfium的 .net包装器--可视化编辑pdf

    Pdfium.Net.Free 支持 .NETFramework 4.0 .NETFramework 4.5 .NETStandard 2.0 .Net8.0 可以和PdfiumViewer.Free ...

  5. HarmonyOS CPU与I/O密集型任务开发指导

      一.CPU密集型任务开发指导 CPU密集型任务是指需要占用系统资源处理大量计算能力的任务,需要长时间运行,这段时间会阻塞线程其它事件的处理,不适宜放在主线程进行.例如图像处理.视频编码.数据分析等 ...

  6. 实验1产品原型设计-YHealth健康APP

    一.实验题目:原型设计 二.实验目的:掌握产品原型设计方法和相应工具使用. 三.实验要求: (1)对比分析墨刀.Axure.Mockplus等原型设计工具的各自的适用领域及优缺点 --墨刀 适用领域: ...

  7. k8s 深入篇———— 编排[八]

    前言 简单整理一下编排. 正文 一个deployment 例子: apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployme ...

  8. 力扣27(java&python)-移除元素(简单)

    题目: 给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度. 不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入 ...

  9. 力扣661(java)-图片平滑器(简单)

    题目: 图像平滑器 是大小为 3 x 3 的过滤器,用于对图像的每个单元格平滑处理,平滑处理后单元格的值为该单元格的平均灰度. 每个单元格的  平均灰度 定义为:该单元格自身及其周围的 8 个单元格的 ...

  10. 第 8章 Python 爬虫框架 Scrapy(下)

    第 8章 Python 爬虫框架 Scrapy(下) 8.1 Scrapy 对接 Selenium 有一种反爬虫策略就是通过 JS 动态加载数据,应对这种策略的两种方法如下:  分析 Ajax 请求 ...