花了一周左右的时间使用微信支付APIV3实现微信支付,以前也没做过微信支付,现在项目上线有点时间就把知识记录分享出来。
第三代微信支付 apiv3
1. 前言
apiV3之前的版本:
微信支付有五个基础步骤:
1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API】
2、商户server调用支付统一下单,api参见公共api【统一下单API】
3、商户server调用再次签名,api参见公共api【再次签名】
4、商户server接收支付通知,api参见公共api【支付结果通知API】
5、商户server查询支付结果,api参见公共api【查询订单API】
2. 介绍
2.1 描述
微信官方介绍:
为了在保证支付安全的前提下,带给商户简单、一致且易用的开发体验,我们推出了全新的微信支付API v3。
相较于的之前微信支付API,主要区别是:
- 遵循统一的Restful的设计风格
- 使用JSON作为数据交互的格式,不再使用XML
- 使用基于非对称密钥的SHA256-RSA的数字签名算法,不再使用MD5或HMAC-SHA256
- 不再要求HTTPS客户端证书
- 使用AES-256-GCM,对回调中的关键信息进行加密保护
2.2 整体结构
开发指引:
apiV3实现微信支付:
步骤一 用户下单发起支付,商户可通过微信支付《APP下单 、 JSAPI支付、Native下单、H5下单》创建支付订单。
步骤二 商户通过小程序《APP调起支付、JS调起支付、Native调起支付、H5调起支付》调起微信支付,发起支付请求。
步骤三 用户支付成功后,商户可接收到微信支付支付结果通知《支付结果通知》。
步骤四 商户也可主动调用《查询订单》查询支付结果。
2.3 官方SDK和工具(可选)
工具的作用是协助你完成固定的工作。但是有个前提,你得花时间去学习这个工具。
2.3.1 SDK
官方开源了两套(Java、Php)协助商户工程师接入微信支付平台的工具包。里面代码并不多,完全可以阅读拿下,并且可以更规范签名、加密、解密等一系列操作,避免不必要的时间消耗。我个人是更推荐大家去看一遍自己语言对应的sdk再进行开发,哪怕你不用。
- wechatpay-apache-httpclient,适用于使用Apache HttpClient处理HTTP的Java开发者。
- wechatpay-guzzle-middleware,适用于PHP开发者。
2.3.2 工具
如果开发者使用过Postman
API的调试,我们推荐在正式开发之前,使用Postman签名脚本 进行接口体验
另外,我们提供的微信支付平台证书下载工具,可以协助开发者完成证书下载。同时,它也是个很好的Java示例程序。
2.3.3 第三方工具或库
如果自行实现验证平台签名逻辑的话,需要注意以下事项:
- 该微信支付没有审核或者控制以下的第三方工具和库,不能保证它们的安全性和可靠性。
3. 开发
微信支付基础知识(必会)
什么是密钥/证书:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay7_0.shtml
懂得一些基础知识,会让你开发的思路更顺畅。
3.1 准备材料
- 商户证书文件:apiclient_cert.p12
- ApiV3 密钥:商户需先在【商户平台】->【API安全】的页面设置该密钥,请求才能通过微信支付的签名校验。密钥的长度为32个字节
- 商户号mchid
- 公众号appid
3.2 步骤一:下单
3.2.1 思路
- 自己商城平台的订单创建,产生"商户订单编号";
- **(难点)**打包参数,请求微信支付平台下单接口,并成功获得预下单编号;
下单api:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml - 打包小程序调起支付需要的参数,主要:用户openid、预下单编号、商户编号;
调起支付参数:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml - 返回前端;
3.2.2 微信下单代码:
// 微信支付下单 begin
Map<String,Object> vxParm = new HashMap<>();
vxParm.put("appid",payWxPayAppid);
vxParm.put("mchid",payWxpayMchid);
vxParm.put("description",virtualGoods.getGdName());//商品描述
vxParm.put("out_trade_no",virtualGoodsOdr.getOdrNum());//商户订单号 string[6,32] 商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一 示例值:1217752501201407033233368018
// vxParm.put("time_expire","");//非必填 订单失效时间
// vxParm.put("attach","");//非必填 附加数据,在查询API和支付通知中原样返回,可作为自定义参数使用
vxParm.put("notify_url", serverHostname+notifyUrl);//通知URL必须为直接可访问的URL,不允许携带查询串。
// vxParm.put("goods_tag","");//非必填 订单优惠标记
Map<String,Object> amount = new HashMap<>();//订单金额信息
amount.put("total",((Double)(virtualGoods.getPrice()*100)).intValue());//订单总金额,单位为分。
amount.put("currency","CNY");//非必填 CNY:人民币,境内商户号仅支持人民币。
vxParm.put("amount",amount);
Map<String,Object> payer = new HashMap<>();//支付者信息
payer.put("openid",openId);//用户在直连商户appid下的唯一标识。 示例值:oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
vxParm.put("payer",payer);
WechatPayHttpClientBuilder builder = null;
PrivateKey privateKey = null;
try {
X509Certificate certificate = (X509Certificate) SpringContextUtil.getBean("certificate");
privateKey = (PrivateKey) SpringContextUtil.getBean("privateKey");
builder = WechatPayHttpClientBuilder.create()
.withMerchant(payWxpayMchid, certificate.getSerialNumber().toString(16).toUpperCase(), privateKey)
.withValidator(new WechatPay2Validator(autoUpdateCertificatesVerifier));
} catch (Exception e) {
e.printStackTrace();
log.info("【{}】读取证书失败:{}", logTitle, e.getMessage());
}
JSONObject responseMap = JSON.parseObject(HttpUtil.vxPayPost(placeOrderUrl, JSON.toJSONString(vxParm), builder.build()));
//预支付交易会话标识。用于后续接口调用中使用,该值有效期为2小时 示例值:wx201410272009395522657a690389285100
String prepay_id;
if((prepay_id = (String)(responseMap).get("prepay_id"))==null){
log.info("【{}】下单失败:{}", logTitle,responseMap.get("message"));
throw new BusinessException("下单失败,"+responseMap.get("message"));
}
// 微信支付下单 end
小结:
构建好下单请求的签名,获得prepay_id预下单编号。
签名和认证信息比较麻烦。
3.2.3 构建前端支付参数
// 微信支付参数 begin
Map<String,Object> payParm = new HashMap<>();
rsl.put("payParm", payParm);
payParm.put("appId", payWxPayAppid);
payParm.put("timeStamp", System.currentTimeMillis()/1000+"");
payParm.put("nonceStr", UUID.randomUUID().toString().replaceAll("-","")); // .toUpperCase()
payParm.put("package", "prepay_id="+prepay_id);
payParm.put("signType", "RSA");
StringBuffer paySign = new StringBuffer(payWxPayAppid+"\n")
.append(payParm.get("timeStamp")+"\n")
.append(payParm.get("nonceStr")+"\n")
.append(payParm.get("package")+"\n");
try {
// PrivateKeySigner privateKeySigner = new PrivateKeySigner(certificate.getSerialNumber().toString(16).toUpperCase(), privateKey);
// privateKeySigner.sign()
//私钥签名并Base64编码
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(privateKey);
sign.update(paySign.toString().getBytes());
payParm.put("paySign", Base64.getEncoder().encodeToString(sign.sign()));
} catch (Exception e) {
e.printStackTrace();
}
// 微信支付参数 end
小结:这里的参数是前端真实调起支付时用的参数。
需要特别注意的是 appId 等参数的大小写,一定要对照官网说明的格式,不少人在这里苦找半天。
3.3 步骤二:支付通知
通知API:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml
3.3.1 思路
- (麻烦)验证通知接口是否来自微信支付平台;
- 使用微信支付平台公钥解析通知报文;
- 修改商户平台订单的状态为已支付;
3.3.2 验证通知
从头部获取序列号、签名、请求主体、时间戳、随机字符串
tring wechatpaySerial = request.getHeader("Wechatpay-Serial");
String wechatpaySingature = request.getHeader("Wechatpay-Signature");
String noticeBody = SysContexts.getRequestParameterMap().get("BODY").toString();
String wechatpayTimestamp = request.getHeader("Wechatpay-Timestamp");
String wechatpayNonce = request.getHeader("Wechatpay-Nonce");
StringBuffer message = new StringBuffer(wechatpayTimestamp+"\n")
.append(wechatpayNonce+"\n")
.append(noticeBody+"\n");
// 通知验签
if(autoUpdateCertificatesVerifier.verify(wechatpaySerial, message.toString().getBytes(), wechatpaySingature)){
}
说明:autoUpdateCertificatesVerifier是微信支付平台提供的sdk,可以自动获取平台证书并提供验证的入口
小结:
特别注意的是获取请求主体,因为推送过来的请求是通过post方法,参数以json形式传送,所以使用流读取的body。也就是说只有第一次读取时才有值,后面再怎么get都是空,这将直接导致验签失败。
3.3.3 解析报文
noticeBody为报文主体。
具体参数我封装在对象里。可以参考官方api:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_5.shtml
//-------------------- 解析通知 begin
VxNoticeVo vxNoticeVo = JSON.parseObject(noticeBody, VxNoticeVo.class);
VxNoticeVo.Resource resource = vxNoticeVo.getResource();
String Ciphertext = "";
//目前微信提供的只有AEAD_AES_256_GCM算法
if("AEAD_AES_256_GCM".equals(resource.getAlgorithm())){
WxAPIV3AesUtil wxAPIV3AesUtil = new WxAPIV3AesUtil(apiV3Key.getBytes());
try {
Ciphertext = wxAPIV3AesUtil.decryptToString(resource.getAssociated_data().getBytes(), resource.getNonce().getBytes(), resource.getCiphertext());
} catch (GeneralSecurityException | IOException e) {
log.info("【{}】vxNotice():Ciphertext解析失败。通知类型:{},通知数据类型:{},通知内容:{}", logTitle, vxNoticeVo.getEvent_type(),vxNoticeVo.getResource_type(), vxNoticeVo.getSummary());
e.printStackTrace();
}
}
//-------------------- 解析通知 end
小结
这里是整个支付过程最简单的部分了。解析后的内容格式为json,可以直接转对象。
3.4 查询订单、申请退款、退款通知
下面这三块功能暂时没有业务需要,后续我实现了再补充具体感受。应该跟上面的操作大同小异了。
官方api:
查询订单:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_2.shtml
申请退款:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_9.shtml
退款通知:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_11.shtml