写在前面

现在到处有微信支付的身影,作为一个后端开发者,跟我一起来看看微信支付到底怎么应用于自己的项目中吧

如果你还不了微信第三方服务生态的,请先阅读一下微信与阿里云第三方服务的一些概念流程梳理,相信读过后你会对微信第三方服务生态有了一定的了解,下面可以按照要求准备开通微信支付的必备条件,并开通APIv3证书,获取一些必备的参数。

如果你对密码学的常识不够了解,最好先阅读一下开发过程中那些不得不知道的密码学基础。基于这些常识,理解这篇文章将会事半功倍。

熟悉官方文档

如果你在之前已经有尝试过浏览微信支付的官方文档/SDK,你或许会和我一样一头雾水。因此,我会带着大家熟悉一下文档。

以Native支付为例,先是开发指引。在这里,微信支付高屋建瓴的总结了实现微信支付的流程。

跟我一起玩转微信支付-编程知识网

首先说明了微信支付接口基于APIv3(贴张官方图说明一下)

跟我一起玩转微信支付-编程知识网

一言以蔽之,APIv3采用JSON作为数据交互格式,接口遵循REST风格,使用高效安全的SHA256-RSA签名算法保证数据完整性和请求者身份。不需要HTTPS客户端证书,仅凭借证书序列号换取公钥和签名内容,使用AES-256-GCM对称加密保证数据私密性。

作为开发者,两个东西需要保管好,极为重要,不可泄漏

  • 商户API证书

商户证书中封装了公钥和签名内容,用于发送请求和签名验证

  • 商户API私钥

商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem 中。私钥用于获取AES加密口令,并解密获取加密内容

其次,在开发准备中提供了JAVA,PHP,GO三个语言版本的SDK,封装了签名生成、签名验证、敏感信息加解密、媒体文件上传等功能,方便我们直接使用,而不用自己手写这一系列的操作。若您对官方SDK不放心,可以自己实现。实现思路官方也给出了:

跟我一起玩转微信支付-编程知识网

也有相应的快速测试方法:

跟我一起玩转微信支付-编程知识网

然后,在快速接入中提供了业务流程图和相应功能(下单、查单、关单、回调支付)的实现逻辑

忽略官方示意图的一些细节,我画了一个更易于理解流程的时序图:

跟我一起玩转微信支付-编程知识网

下面,我们要做的是就一目了然了:了解怎么使用SDK封装一系列的安全保障过程,即构建自动化的HttpClient,然后具体实现每个API的请求就可以了!

看懂SDK文档

做好准备

这里以Java为例,剖析官方SDK的README文档

文档地址 在这里建议大家下载源码到本地,更方便的阅读源码,对实现细节做了解

我写这篇文章的时间是

mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2022-03-11 14:39:46 |
+---------------------+
1 row in set (0.03 sec)

采用最新版本SDK wechatpay-apache-httpclient 0.4.2

基于JDK1.8+ Maven依赖为

<dependency><groupId>com.github.wechatpay-apiv3</groupId><artifactId>wechatpay-apache-httpclient</artifactId><version>0.4.2</version>
</dependency>

开始

跟我一起玩转微信支付-编程知识网

这里告诉我们凭借商户号、证书序列号、私钥、商户证书等可构建专有的WechatPayHttpClient,帮助我们实现加解密、签名、签名验证等繁琐的过程

跟我一起玩转微信支付-编程知识网

接下来则是使用该HttpClient如果封装请求头和请求体的简单实例

填坑

通过上面的方式,需要手动下载更新证书。在README的后面给予了解决方法

跟我一起玩转微信支付-编程知识网

不同于上面,这里利用CertificatesManager证书管理器实现验签器、证书更新的集中管理

回调方案

跟我一起玩转微信支付-编程知识网

这里则提供了如何使用SDK进行回调签名验证,返回数据的解密。稍微分析一下,可见SDK提供了NotificationRequest和NotificationHandler两个工具实现此功能。

现在感到懵逼不要紧张,下面结合具体实例来说明

开始干活

再准备一次

相信看过上面对于微信文档和SDK文档的大致分析后,对大概怎么个流程已经心里有数了。下面就开始干活。

开发指引提供了通用的解决思路(如何加载商户私钥、加载平台证书、初始化httpClient),只可惜其中AutoUpdateCertificatesVerifier在最新的SDK中已经弃用

跟我一起玩转微信支付-编程知识网

跟我一起玩转微信支付-编程知识网

虽然但是,开发者提供了更好的解决方法:

跟我一起玩转微信支付-编程知识网

此工具集成了获取证书,下载证书,定期更新证书,获取验签器功能为一体

看看该类的结构:

跟我一起玩转微信支付-编程知识网

显然,这是单例模式的设计。getInstance方法获得唯一实例,可以放入证书,停止下载更新,获取验签器。

注册全局Bean 供业务使用

经过上面的分析,不难得出。拿到CertificatesManager实例,放入证书,开启自动下载更新,取出验签器即可。并在SpringBoot服务中注册全局Bean,静候差遣!

当然,在这之前,最好在yml中配置好需要用到的参数,并读取到WxPayConfig中:

wxpay:# 商户号mch-id: 1******42# API证书序列号mch-serial-no: 你的API证书序列号# 商户私钥文件private-key-path: C:\Users\cheung0\Desktop\apiclient_key.pem# APIv3 密钥api-v3-key: w*************x# APPIDappid: wx3*********46# 微信服务器地址domain: https://api.mch.weixin.qq.com# 接受结果通知地址notify-domain: http://maiqu.sh1.k9s.run:2271   

其中notify-domain是回调通知时为微信服务器请求的地址。若要做本地测试,请用内网穿透工具开通隧道。这里推荐我使用的SuiDao

随后配置到

@Data
@Slf4j
@Component
@ConfigurationProperties(prefix = "wxpay")
public class WxPayConfig {// 商户号private String mchId;// API证书序列号private String mchSerialNo;// 私钥地址private String privateKeyPath;// APIv3 密钥private String apiV3Key;// APPIDprivate String appid;// 微信服务器地址private String domain;// 接收结果通知地址private String notifyDomain;}

这里推荐添加POM依赖,对配置和实体之间更好的依赖:

<!--配置映射-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional>
</dependency>

下面,可以自己写一个测试看看有没有配置上

接下来开始注册Bean

由于多处需要加载私钥,便注册一个返回私钥内容的Bean

    /*** 加载商户私钥 <br>* @return PrivateKey*/@Beanpublic PrivateKey getPrivateKey() throws IOException {// 加载商户私钥(privateKey:私钥字符串)log.info("开始加载私钥,读取内容...");String content = new String(Files.readAllBytes(Paths.get(privateKeyPath)),StandardCharsets.UTF_8 );return PemUtil.loadPrivateKey(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)));}

PemUtil是SDK提供的工具类,它可以帮助我们读取私钥:

public static PrivateKey loadPrivateKey(String privateKey) {privateKey = privateKey.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replaceAll("\\s+", "");try {KeyFactory kf = KeyFactory.getInstance("RSA");return kf.generatePrivate(new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));} catch (NoSuchAlgorithmException var2) {throw new RuntimeException("当前Java环境不支持RSA", var2);} catch (InvalidKeySpecException var3) {throw new RuntimeException("无效的密钥格式");}}

获取验签器

    /*** 获取平台证书管理器,定时更新证书(默认值为UPDATE_INTERVAL_MINUTE)* <br>* 返回验签器实例,注册为bean,在实际业务中使用** @return*/@Beanpublic Verifier getVerifier(PrivateKey merchantPrivateKey) throws IOException, NotFoundException {log.info("加载证书管理器实例");// 获取证书管理器单例实例CertificatesManager certificatesManager = CertificatesManager.getInstance();// 向证书管理器增加需要自动更新平台证书的商户信息log.info("向证书管理器增加商户信息,并开启自动更新");try {// 该方法底层已实现同步线程更新证书// 详见beginScheduleUpdate()方法certificatesManager.putMerchant(mchId, new WechatPay2Credentials(mchId,new PrivateKeySigner(mchSerialNo, merchantPrivateKey)), apiV3Key.getBytes(StandardCharsets.UTF_8));} catch (GeneralSecurityException | HttpCodeException e) {e.printStackTrace();}log.info("从证书管理器中获取验签器");return certificatesManager.getVerifier(mchId);}

至此,获取验签器的同时也开启了定时下载更新证书,在certificatesManager.putMerchant方法中可见:

public synchronized void putMerchant(String merchantId, Credentials credentials, byte[] apiV3Key) throws IOException, GeneralSecurityException, HttpCodeException {if (merchantId != null && !merchantId.isEmpty()) {if (credentials == null) {throw new IllegalArgumentException("credentials为空");} else if (apiV3Key.length == 0) {throw new IllegalArgumentException("apiV3Key为空");} else {if (this.certificates.get(merchantId) == null) {this.certificates.put(merchantId, new ConcurrentHashMap());}this.initCertificates(merchantId, credentials, apiV3Key);this.credentialsMap.put(merchantId, credentials);this.apiV3Keys.put(merchantId, apiV3Key);if (this.executor == null) {this.beginScheduleUpdate();}}} else {throw new IllegalArgumentException("merchantId为空");}}
private void beginScheduleUpdate() {this.executor = new SafeSingleScheduleExecutor();Runnable runnable = () -> {try {Thread.currentThread().setName("scheduled_update_cert_thread");log.info("Begin update Certificates.Date:{}", Instant.now());this.updateCertificates();log.info("Finish update Certificates.Date:{}", Instant.now());} catch (Throwable var2) {log.error("Update Certificates failed", var2);}};this.executor.scheduleAtFixedRate(runnable, 0L, 1440L, TimeUnit.MINUTES);}

不难看出,当SpringBoot服务启动后,线程池中会创建一个名为"scheduled_update_cert_thread"的线程来定时下载更新证书

获取HttpClient

/*** 通过WechatPayHttpClientBuilder构造HttpClient** @param verifier* @return*/
@Bean(name = "wxPayClient")
public CloseableHttpClient getWxPayClient(Verifier verifier,PrivateKey merchantPrivateKey) throws IOException {log.info("构造httpClient");WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create().withMerchant(mchId, mchSerialNo, merchantPrivateKey).withValidator(new WechatPay2Validator(verifier));// 通过WechatPayHttpClientBuilder构造的HttpClient,会自动的处理签名和验签,并进行证书自动更新CloseableHttpClient httpClient = builder.build();log.info("构造httpClient成功");return httpClient;
}

获取支付回调请求处理器

/*** 构建微信支付回调请求处理器** @param verifier* @return NotificationHandler*/
@Bean
public NotificationHandler notificationHandler(Verifier verifier) {return new NotificationHandler(verifier,apiV3Key.getBytes(StandardCharsets.UTF_8));
}

启动服务做测试

2022-03-11 15:54:27.683  INFO 4944 --- [  restartedMain] t.m.metrichall.wxpay.config.WxPayConfig  : 开始加载私钥,读取内容...
2022-03-11 15:54:27.697  INFO 4944 --- [  restartedMain] t.m.metrichall.wxpay.config.WxPayConfig  : 加载证书管理器实例
2022-03-11 15:54:27.698  INFO 4944 --- [  restartedMain] t.m.metrichall.wxpay.config.WxPayConfig  : 向证书管理器增加商户信息,并开启自动更新
2022-03-11 15:54:28.363  INFO 4944 --- [  restartedMain] t.m.metrichall.wxpay.config.WxPayConfig  : 从证书管理器中获取验签器
2022-03-11 15:54:28.365  INFO 4944 --- [  restartedMain] t.m.metrichall.wxpay.config.WxPayConfig  : 构造httpClient
2022-03-11 15:54:28.364  INFO 4944 --- [ate_cert_thread] c.w.p.c.a.h.cert.CertificatesManager     : Begin update Certificates.Date:2022-03-11T07:54:28.364Z
2022-03-11 15:54:28.367  INFO 4944 --- [  restartedMain] t.m.metrichall.wxpay.config.WxPayConfig  : 构造httpClient成功
2022-03-11 15:54:28.626  INFO 4944 --- [ate_cert_thread] c.w.p.c.a.h.cert.CertificatesManager     : Finish update Certificates.Date:2022-03-11T07:54:28.626Z

可以看到,我们注册Bean实例已启动,下载更新证书线程也启动了,一切准备完毕,静候差遣

集中封装一些枚举类

@AllArgsConstructor
@Getter
public enum PayType {/*** 微信*/WXPAY("微信"),/*** 支付宝*/ALIPAY("支付宝");/*** 类型*/private final String type;
}
@AllArgsConstructor
@Getter
public enum OrderStatus {/*** 未支付*/NOTPAY("未支付"),/*** 支付成功*/SUCCESS("支付成功"),/*** 已关闭*/CLOSED("超时已关闭"),/*** 已取消*/CANCEL("用户已取消"),/*** 退款中*/REFUND_PROCESSING("退款中"),/*** 已退款*/REFUND_SUCCESS("已退款"),/*** 退款异常*/REFUND_ABNORMAL("退款异常");/*** 类型*/private final String type;
}
@AllArgsConstructor
@Getter
public enum WxApiType {/*** Native下单*/NATIVE_PAY("/v3/pay/transactions/native"),/*** 查询订单*/ORDER_QUERY_BY_NO("/v3/pay/transactions/out-trade-no/%s"),/*** 关闭订单*/CLOSE_ORDER_BY_NO("/v3/pay/transactions/out-trade-no/%s/close"),/*** 申请退款*/DOMESTIC_REFUNDS("/v3/refund/domestic/refunds"),/*** 查询单笔退款*/DOMESTIC_REFUNDS_QUERY("/v3/refund/domestic/refunds/%s"),/*** 申请交易账单*/TRADE_BILLS("/v3/bill/tradebill"),/*** 申请资金账单*/FUND_FLOW_BILLS("/v3/bill/fundflowbill");/*** 类型*/private final String type;
}
@AllArgsConstructor
@Getter
public enum WxNotifyType {/*** 支付通知*/NATIVE_NOTIFY("/api/wx-pay/native/notify"),/*** 退款结果通知*/REFUND_NOTIFY("/api/wx-pay/refunds/notify");/*** 类型*/private final String type;
}
@AllArgsConstructor
@Getter
public enum WxRefundStatus {/*** 退款成功*/SUCCESS("SUCCESS"),/*** 退款关闭*/CLOSED("CLOSED"),/*** 退款处理中*/PROCESSING("PROCESSING"),/*** 退款异常*/ABNORMAL("ABNORMAL");/*** 类型*/private final String type;
}
@AllArgsConstructor
@Getter
public enum WxTradeState {/*** 支付成功*/SUCCESS("SUCCESS"),/*** 未支付*/NOTPAY("NOTPAY"),/*** 已关闭*/CLOSED("CLOSED"),/*** 转入退款*/REFUND("REFUND");/*** 类型*/private final String type;
}

枚举类中的值在业务经常会被用到,封装成枚举类,更为优雅

封装响应消息

@Data
@Accessors(chain = true)
public class R {private Integer code; //响应码private String message; //响应消息private Map<String, Object> data = new HashMap<>();public static R ok(){R r = new R();r.setCode(0);r.setMessage("成功");return r;}public static R error(){R r = new R();r.setCode(-1);r.setMessage("失败");return r;}public R data(String key, Object value){this.data.put(key, value);return this;}}

其中@Accessors(chain = true)注解可使得该类方法可以链式调用:

return R.ok().setMessage("下单成功!")

Native下单

Native下单API字典告知了我们必要的参数,并提供了请求示例:

{"mchid": "1900006XXX","out_trade_no": "native12177525012014070332333","appid": "wxdace645e0bc2cXXX","description": "Image形象店-深圳腾大-QQ公仔","notify_url": "https://weixin.qq.com/","amount": {"total": 1,"currency": "CNY"}
}

返回示例:

{"code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz"
}

如果成功,就能拿到二维码链接

按照示例,封装我们自己的请求体:

    /*** 创建订单,调用Native支付接口** @param productId* @return code_url 和 订单号* @throws Exception*/@Transactional(rollbackFor = Exception.class)@Overridepublic Map<String, Object> nativePay(Long productId) throws Exception {log.info("生成订单");//生成订单...//查找二维码链接是否已经存在 ? 直接retun : 往下走 ...log.info("调用统一下单API");//调用统一下单APIHttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));httpPost.addHeader("Accept", "application/json");httpPost.addHeader("Content-type", "application/json; charset=utf-8");// 请求body参数Map<String, Object> paramsMap = new HashMap<>();paramsMap.put("mchid", wxPayConfig.getMchId());paramsMap.put("out_trade_no", orderInfo.getOrderNo());paramsMap.put("appid", wxPayConfig.getAppid());paramsMap.put("description", orderInfo.getTitle());paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));// 组装amountMap<String, Object> amountMap = new HashMap<>();amountMap.put("total", orderInfo.getTotalFee());amountMap.put("currency", "CNY");paramsMap.put("amount", amountMap);//将参数转换成json字符串String jsonParams = JSON.toJSONString(paramsMap);log.info("请求参数 ===> {}" + jsonParams);// 配置请求体httpPost.setEntity(new StringEntity(jsonParams, "UTF-8"));//完成签名并执行请求try (CloseableHttpResponse response = httpClient.execute(httpPost)) {String bodyAsString = EntityUtils.toString(response.getEntity());//响应体int statusCode = response.getStatusLine().getStatusCode();//响应状态码if (statusCode == 200) { //处理成功log.info("成功, 返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {log.info("Native下单失败,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);throw new IOException("request failed");}//响应结果Map<String, String> resultMap = JSON.parseObject(bodyAsString, HashMap.class);//二维码codeUrl = resultMap.get("code_url");//保存二维码链接...//返回二维码Map<String, Object> map = new HashMap<>();map.put("codeUrl", codeUrl);map.put("orderNo", orderInfo.getOrderNo());return map;}}

其中省略部分则大家个性化开发,使用Mybatis/MybatisPlus/JPA等工具进行数据库的增删改查,接下来类似地方同理

API请求看一下:

跟我一起玩转微信支付-编程知识网

返回JSON:

{"code": 0,"message": "成功","data": {"codeUrl": "weixin://wxpay/bizpayurl?pr=HVPisQfzz","orderNo": "ORDER_20220311155916957"}
}

codeUrl就是我们二维码的链接,后端可以采用Zxing工具来解析成二维码图片二进制流返回前端。我这里交给前端同学自行做优化处理。

在这里使用QRcode测试一下该codeUrl

<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><title>支付测试</title><script type="text/javascript" src="https://code.jquery.com/jquery-3.3.1.min.js"></script><script type="text/javascript" src="https://cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js"></script>
</head><body><button onclick="displayDate()">点击支付</button><div id="myQrcode"></div><script>function displayDate() {jQuery('#myQrcode').qrcode({text: 'weixin://wxpay/bizpayurl?pr=HVPisQfzz'});}</script>
</body></html>

跟我一起玩转微信支付-编程知识网

取消订单

取消订单API字典告诉了我们需要的参数:

跟我一起玩转微信支付-编程知识网

{"mchid": "1230000109"
}

请求体中放商户号,订单号拼接在URL中即可

    /*** 关单接口的调用* <p>* API字典: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_3.shtml** @param orderNo*/private HashMap<String, String> closeOrder(String orderNo) throws Exception {log.info("关单接口的调用,订单号 ===> {}", orderNo);//创建远程请求对象String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);url = wxPayConfig.getDomain().concat(url);HttpPost httpPost = new HttpPost(url);httpPost.addHeader("Accept", "application/json");httpPost.addHeader("Content-type", "application/json; charset=utf-8");// 请求body参数Map<String, String> paramsMap = new HashMap<>();paramsMap.put("mchid", wxPayConfig.getMchId());String jsonParams = JSON.toJSONString(paramsMap);log.info("请求参数 ===> {}", jsonParams);StringEntity entity = new StringEntity(jsonParams, "UTF-8");entity.setContentType("application/json");//将请求参数设置到请求对象中httpPost.setEntity(entity);//完成签名并执行请求CloseableHttpResponse response = httpClient.execute(httpPost);HashMap<String, String> res = new HashMap<>();try {if (response.getStatusLine().getStatusCode() == 200 || response.getStatusLine().getStatusCode() == 204 ) {res.put("code", "SUCCESS");res.put("message", "该订单已成功关闭");return res;}String bodyAsString = EntityUtils.toString(response.getEntity());res = JSON.parseObject(bodyAsString,HashMap.class);return res;} catch (IOException | ParseException e) {res.put("code", "ERROR");if (e.toString() != null && !e.toString().equals("")) {res.put("message", e.toString());} else {res.put("message", "发生未知错误");}return res;}}

打印返回体,能得到具体的相关信息。API字典也做出了说明。

若成功,返回体为空,状态码为200或204。若失败,例如:

跟我一起玩转微信支付-编程知识网

查询订单

查询订单API字典

    /*** 可通过“微信支付订单号查询”和“商户订单号查询”两种方式查询订单详情* <p>* 这里通过后者进行查询* <p>* API字典: https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_2.shtml** @param orderNo* @return* @throws Exception*/@Overridepublic String queryOrder(String orderNo) throws Exception {log.info("查单接口调用 ===> {}", orderNo);//拼接请求的第三方APIString url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());HttpGet httpGet = new HttpGet(url);httpGet.setHeader("Accept", "application/json");//完成签名并执行请求CloseableHttpResponse response = httpClient.execute(httpGet);try {String bodyAsString = EntityUtils.toString(response.getEntity());//响应体int statusCode = response.getStatusLine().getStatusCode();//响应状态码if (statusCode == 200) { //处理成功log.info("成功, 返回结果 = " + bodyAsString);} else if (statusCode == 204) { //处理成功,无返回Bodylog.info("成功");} else {log.info("查单接口调用,响应码 = " + statusCode + ",返回结果 = " + bodyAsString);throw new IOException("request failed");}return bodyAsString;} finally {response.close();}}

API测试:

跟我一起玩转微信支付-编程知识网

返回JSON:

{"code": 0,"message": "查询成功","data": {"result": {"mchid": "162****542","out_trade_no": "ORDER_20220311155916957","trade_state": "CLOSED","promotion_detail": [],"appid": "wx32d4*******79746","trade_state_desc": "订单已关闭","attach": "","payer": {}}}
}

支付回调

当用户下单后,微信服务器会请求我们的服务器,告知我们支付结果。但这并不安全,因为我们并不能确定请求服务器来自哪里,万一是黑客的恶意请求呢?于是微信强烈建议我们进行签名验证,确认受否微信支付服务器所请求且数据完整未被中途篡改

支付回调API字典详细解释了Request内容和对Resource解密后的内容,以及我们该如何回应微信服务器。如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。(通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h – 总计 24h4m)

在看懂SDK文档回调方案中我贴上了SDK提供的做法,这里我如法炮制

    /*** 支付通知<br>* 微信支付通过支付通知接口将用户支付成功消息通知给商户<br>* 商户应返回应答<br>* 若商户收到的商户的应答不符合规范或者超时 微信则认为通知失败<br>* 若通知失败 微信会通过一定的策略定期重新发起通知<br>* 加密不能保证通知请求来自微信<br>* 微信会对发送给商户的通知进行签名<br>* 并将签名值放在通知的HTTP头Wechatpay-Signature<br>** @param request* @param response* @return 响应map*/@ApiOperation("支付通知")@PostMapping("/native/notify")public String nativeNotify(HttpServletRequest request, HttpServletResponse response) {// 应答对象Map<String, String> map = new HashMap<>();try {// 处理参数String serialNumber = request.getHeader("Wechatpay-Serial");String nonce = request.getHeader("Wechatpay-Nonce");String timestamp = request.getHeader("Wechatpay-Timestamp");String signature = request.getHeader("Wechatpay-Signature");// 请求头Wechatpay-Signature// 获取请求体String body = HttpUtils.readData(request);// 构造微信请求体NotificationRequest wxRequest = new NotificationRequest.Builder().withSerialNumber(serialNumber).withNonce(nonce).withTimestamp(timestamp).withSignature(signature).withBody(body).build();Notification notification = null;try {/*** 使用微信支付回调请求处理器解析构造的微信请求体* 在这个过程中会进行签名验证,并解密加密过的内容* 签名源码:  com.wechat.pay.contrib.apache.httpclient.cert; 271行开始* 解密源码:  com.wechat.pay.contrib.apache.httpclient.notification 76行*           com.wechat.pay.contrib.apache.httpclient.notification 147行 使用私钥获取AesUtil*           com.wechat.pay.contrib.apache.httpclient.notification 147行 使用Aes对称解密获得原文*/notification = notificationHandler.parse(wxRequest);} catch (Exception e) {log.error("通知验签失败");//失败应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "通知验签失败");return JSON.toJSONString(map);}// 从notification中获取解密报文,并解析为HashMapString plainText = notification.getDecryptData();log.info("通知验签成功");//处理订单wxPayService.processOrder(plainText);//成功应答response.setStatus(200);map.put("code", "SUCCESS");map.put("message", "成功");return JSON.toJSONString(map);} catch (Exception e) {e.printStackTrace();//失败应答response.setStatus(500);map.put("code", "ERROR");map.put("message", "失败");return JSON.toJSONString(map);}}

避坑:serialNumber参数值并不是我们在yml中所配置的,微信会重新发送一个新的证书序列号放在请求头,我们必须拼接这个证书序列号去换取证书实例,换取公钥验签

调试是可以看到:

跟我一起玩转微信支付-编程知识网

HttpUtils是我用来读取HttpServletRequest中主体内容的工具类,源码如下:

public class HttpUtils {/*** 将通知参数转化为字符串* @param request* @return*/public static String readData(HttpServletRequest request) {BufferedReader br = null;try {StringBuilder result = new StringBuilder();br = request.getReader();for (String line; (line = br.readLine()) != null; ) {if (result.length() > 0) {result.append("\n");}result.append(line);}return result.toString();} catch (IOException e) {throw new RuntimeException(e);} finally {if (br != null) {try {br.close();} catch (IOException e) {e.printStackTrace();}}}}
}

对于签名和解密是如何实现感到好奇的朋友可以到SDK中查看,相关源码位置我也写在注释中了

简单说明一下:

public boolean verify(String serialNumber, byte[] message, String signature) {if (!serialNumber.isEmpty() && message.length != 0 && !signature.isEmpty()) {BigInteger serialNumber16Radix = new BigInteger(serialNumber, 16);ConcurrentHashMap<BigInteger, X509Certificate> merchantCertificates = (ConcurrentHashMap)CertificatesManager.this.certificates.get(this.merchantId);X509Certificate certificate = (X509Certificate)merchantCertificates.get(serialNumber16Radix);if (certificate == null) {CertificatesManager.log.error("商户证书为空,serialNumber:{}", serialNumber);return false;} else {try {Signature sign = Signature.getInstance("SHA256withRSA");sign.initVerify(certificate);sign.update(message);return sign.verify(Base64.getDecoder().decode(signature));} catch (NoSuchAlgorithmException var8) {throw new RuntimeException("当前Java环境不支持SHA256withRSA", var8);} catch (SignatureException var9) {throw new RuntimeException("签名验证过程发生了错误", var9);} catch (InvalidKeyException var10) {throw new RuntimeException("无效的证书", var10);}}} else {throw new IllegalArgumentException("serialNumber或message或signature为空");}}

在这里进行获取证书换取公钥签名

private void setDecryptData(Notification notification) throws ParseException {Resource resource = notification.getResource();String getAssociateddData = "";if (resource.getAssociatedData() != null) {getAssociateddData = resource.getAssociatedData();}byte[] associatedData = getAssociateddData.getBytes(StandardCharsets.UTF_8);byte[] nonce = resource.getNonce().getBytes(StandardCharsets.UTF_8);String ciphertext = resource.getCiphertext();AesUtil aesUtil = new AesUtil(this.apiV3Key);String decryptData;try {decryptData = aesUtil.decryptToString(associatedData, nonce, ciphertext);} catch (GeneralSecurityException var10) {throw new ParseException("AES解密失败,resource:" + resource.toString(), var10);}notification.setDecryptData(decryptData);}

凭借私钥获取AES口令,解密ciphertext中的内容

实测一下:

跟我一起玩转微信支付-编程知识网

跟我一起玩转微信支付-编程知识网

resource内容是被加密过的

2022-03-11 17:11:23.379  INFO 1656 --- [nio-8080-exec-1] t.m.m.w.service.impl.WxPayServiceImpl    : 生成订单
2022-03-11 17:11:23.479  INFO 1656 --- [nio-8080-exec-1] t.m.m.w.service.impl.WxPayServiceImpl    : 调用统一下单API
2022-03-11 17:11:23.523  INFO 1656 --- [nio-8080-exec-1] t.m.m.w.service.impl.WxPayServiceImpl    : 请求参数 ===> {}{"amount":{"total":1,"currency":"CNY"},"mchid":"1621810542","out_trade_no":"ORDER_20220311171123529","appid":"wx32d4d97357b79746","description":"GBA游戏测评","notify_url":"http://maiqu.sh1.k9s.run:2271/api/wx-pay/native/notify"}
2022-03-11 17:11:23.926  INFO 1656 --- [nio-8080-exec-1] t.m.m.w.service.impl.WxPayServiceImpl    : 成功, 返回结果 = {"code_url":"weixin://wxpay/bizpayurl?pr=ICd695Azz"}
2022-03-11 17:11:31.783 ERROR 1656 --- [nio-8080-exec-2] t.m.m.s.f.JWTAuthenticationTokenFilter   : Token为空
2022-03-11 17:19:53.337  WARN 1656 --- [l-1 housekeeper] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=8m29s873ms509µs200ns).
2022-03-11 17:19:53.338  INFO 1656 --- [nio-8080-exec-2] t.maiquer.metrichall.wxpay.api.WxPayAPI  : 通知验签成功

解密内容:

{"mchid": "1621***42","appid": "wx32d*****79746","out_trade_no": "ORDER_20220311171123529","transaction_id": "4200001348202203119819934409","trade_type": "NATIVE","trade_state": "SUCCESS","trade_state_desc": "支付成功","bank_type": "OTHERS","attach": "","success_time": "2022-03-11T17:11:31+08:00","payer": {"openid": "o0F3X099H******Spqj5p8D-6TI"},"amount": {"total": 1,"payer_total": 1,"currency": "CNY","payer_currency": "CNY"}
}

跟我一起玩转微信支付-编程知识网

总结

写到这里就告一段落了

相关的数据库表和实体类我没有提供,各位根据业务个性化设计,至于怎么使用微信支付SDK本文已交代的很清楚

后面还有退款、订单超时、下载账单等API。怎么使用都大差不差,无非组装请求体,使用SDK提供的HttpClient请求,省略繁琐的安全验证过程,得到返回结果…大家自己摸索,多说无益

需要完整实例源码的可私信我