写在前面

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

如果你还不了微信第三方服务生态的,请先阅读一下微信与阿里云第三方服务的一些概念流程梳理,相信读过后你会对微信第三方服务生态有了一定的了解,下面可以按照要求准备开通微信支付的必备条件,并开通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请求,省略繁琐的安全验证过程,得到返回结果…大家自己摸索,多说无益

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

跟我一起玩转微信支付相关推荐

  1. 微信支付推出“中秋花灯会”新玩法 点亮花灯享大额提现免费券

    9月19日消息,微信支付正式上线"中秋花灯会"活动,解锁节日新玩法.用户打开"微信支付有优惠"小程序即可进入专区参加"中秋花灯会"活动,点亮 ...

  2. vue玩转移动端H5微信支付和支付宝支付

    业务场景介绍: H5移动端支持微信支付 [ 微信支付分为微信内支付(JSAPI支付官方API)和微信外支付(H5支付官方API)] && 支付宝支付 [手机网站支付转 APP 支付 官 ...

  3. 微信支付 postman_微信上线新功能,马化腾都在玩!快查查你多少分?

    尽管在移动支付领域与支付宝打得难解难分,但微信支付在信用体系建设方面的进展却慢了对手不只半拍,当芝麻分已经深入日常生活各种使用场景时,腾讯的"微信支付分"终于在6月3日晚间正式上线 ...

  4. 微信支付 postman_微信收款商业版有什么功能?微信智慧经营2.0原来可以这么玩!...

    实体门店使用微信收款商业版收款码(或收款机具)腾讯云支付系统进行收银,腾讯官方开放平台营销接口,在微信官方平台帮助实体门店商家做推广宣传,我们称之为  微信支付  智慧经营! 案例展示 目前已有越来越 ...

  5. iOS支付宝(Alipay)接入详细流程,比微信支付更简单,项目实战中的问题分析

    最近在项目中接入了微信支付和支付宝支付,总的来说没有那么坑,很多人都说文档不全什么的,确实没有面面 俱到,但是认真一步一步测试下还是妥妥的,再配合懂得后台,效率也是很高的,看了这篇文章,你也只要几分钟 ...

  6. Payment Spring Boot 1.0.2.RELEASE 发布,接入微信支付分、先享卡功能

    Payment Spring Boot 是微信支付V3的Java实现,仅仅依赖Spring内置的一些类库.配置简单方便,可以让开发者快速为Spring Boot应用接入微信支付. 演示例子:https ...

  7. 《玩转微信6.0》一1.2 微信初体验

    本节书摘来异步社区<玩转微信6.0>一书中的第1章,第1.2节,作者: 王璨 , 周聪 , 章佳荣 责编: 陈冀康,更多章节内容可以访问云栖社区"异步社区"公众号查看. ...

  8. 快手小店电脑版_微信PC版更新!支持在小程序中使用微信支付 | 一周资讯

    小程序1. 微信PC版更新,支持在小程序中使用微信支付.12月19日,微信PC端推出内测版2.7.2.73,新版本支持以下新功能:新增看一看精选内容,新的订阅号浏览体验,支持在小程序中使用微信支付.( ...

  9. 微信支付 企业转账 小程序发红包 提现 发红包 企业支付等遇到的问题

    最近公司在开发一个项目,小程序抢红包,抢到的红包用户要提现.商家需要通过微信的企业转账功能打款到微信的钱包里. 开发的时候发现有几个坑,在这里和大家分享下.首先就是微信支付的开通条件. 第一个,就是个 ...

最新文章

  1. 淘淘经受了一次考验...
  2. Linux简单的颜色设置
  3. 自动驾驶的摩尔定律:无人驾驶的最终实现时间或在2035年丨厚势汽车
  4. 数据库系统概论:第十一章 并发控制
  5. Python数据类型(列表和元组)
  6. Neo4j实战 (数据库技术丛书)pdf
  7. access数据类型百度百科_Day 7 基本数据类型
  8. 为什么要用Mybatis框架---Mybatis学习笔记(一)
  9. JavaFX下的WebView中js调用java注入方法提示undefined?
  10. Php超出高度隐藏,html设置div最小高度,超出的自适应
  11. Makefile初步理解
  12. 蓝桥杯 ALGO-95 算法训练 2的次幂表示
  13. linux 内存清理/释放命令(也可用于openwrt和padavan等系统的路由器)
  14. C语言-顺序栈的基本操作
  15. 【原创】2021-2001中国科技统计年鉴面板数据、中国科技年鉴(830个指标,可直接用)
  16. 共轭梯度法matlab程序精确线搜索,具有精确线性搜索的改进共轭梯度法
  17. Dinic算法的原理与构造
  18. 2019-3-5 梦
  19. C18-PEG-ICG18碳烷基链-聚乙二醇-吲哚菁绿,Cholesterol-PEG-ICG胆固醇-聚乙二醇-吲哚菁绿
  20. 【UWP通用应用开发】开发准备、部分新特性

热门文章

  1. 【专升本计算机】计算机操作系统练习题(选择判断名词解释简答)
  2. 前端面试技巧和注意事项_web前端没有项目经验怎么应对面试?(技巧) -
  3. 攻防世界 happyctf
  4. display:flex 常用
  5. Docker 与虚拟机有何不同?
  6. 山东大学人机交互复习大纲
  7. 物体结构图,快速图解物体内部结构
  8. 鸿蒙系统支持980,稳了!鸿蒙系统升级名单再曝:至少麒麟980机型都能升级
  9. 安装Visio 2013与原本的office冲突的最终解决方案
  10. 第12章 ‘expect’和‘assume’