写在前面
现在到处有微信支付的身影,作为一个后端开发者,跟我一起来看看微信支付到底怎么应用于自己的项目中吧
如果你还不了微信第三方服务生态的,请先阅读一下微信与阿里云第三方服务的一些概念流程梳理,相信读过后你会对微信第三方服务生态有了一定的了解,下面可以按照要求准备开通微信支付的必备条件,并开通APIv3证书,获取一些必备的参数。
如果你对密码学的常识不够了解,最好先阅读一下开发过程中那些不得不知道的密码学基础。基于这些常识,理解这篇文章将会事半功倍。
熟悉官方文档
如果你在之前已经有尝试过浏览微信支付的官方文档/SDK,你或许会和我一样一头雾水。因此,我会带着大家熟悉一下文档。
以Native支付为例,先是开发指引。在这里,微信支付高屋建瓴的总结了实现微信支付的流程。
首先说明了微信支付接口基于APIv3(贴张官方图说明一下)
一言以蔽之,APIv3采用JSON作为数据交互格式,接口遵循REST风格,使用高效安全的SHA256-RSA签名算法保证数据完整性和请求者身份。不需要HTTPS客户端证书,仅凭借证书序列号换取公钥和签名内容,使用AES-256-GCM对称加密保证数据私密性。
作为开发者,两个东西需要保管好,极为重要,不可泄漏
商户证书中封装了公钥和签名内容,用于发送请求和签名验证
商户申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件apiclient_key.pem
中。私钥用于获取AES加密口令,并解密获取加密内容
其次,在开发准备中提供了JAVA,PHP,GO三个语言版本的SDK,封装了签名生成、签名验证、敏感信息加解密、媒体文件上传等功能,方便我们直接使用,而不用自己手写这一系列的操作。若您对官方SDK不放心,可以自己实现。实现思路官方也给出了:
也有相应的快速测试方法:
然后,在快速接入中提供了业务流程图和相应功能(下单、查单、关单、回调支付)的实现逻辑
忽略官方示意图的一些细节,我画了一个更易于理解流程的时序图:
下面,我们要做的是就一目了然了:了解怎么使用SDK封装一系列的安全保障过程,即构建自动化的HttpClient,然后具体实现每个API的请求就可以了!
看懂SDK文档
做好准备
这里以Java为例,剖析官方SDK的README文档
文档地址 在这里建议大家下载源码到本地,更方便的阅读源码,对实现细节做了解
我写这篇文章的时间是
1 2 3 4 5 6 7
| 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依赖为
1 2 3 4 5
| <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
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| wxpay: mch-id: 1******42 mch-serial-no: 你的API证书序列号 private-key-path: C:\Users\cheung0\Desktop\apiclient_key.pem api-v3-key: w*************x appid: wx3*********46 domain: https://api.mch.weixin.qq.com notify-domain: http://maiqu.sh1.k9s.run:2271
|
其中notify-domain
是回调通知时为微信服务器请求的地址。若要做本地测试,请用内网穿透工具开通隧道。这里推荐我使用的SuiDao
随后配置到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @Data @Slf4j @Component @ConfigurationProperties(prefix = "wxpay") public class WxPayConfig {
private String mchId;
private String mchSerialNo;
private String privateKeyPath;
private String apiV3Key;
private String appid;
private String domain;
private String notifyDomain; }
|
这里推荐添加POM依赖,对配置和实体之间更好的依赖:
1 2 3 4 5 6
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>
|
下面,可以自己写一个测试看看有没有配置上
接下来开始注册Bean
由于多处需要加载私钥,便注册一个返回私钥内容的Bean
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@Bean public PrivateKey getPrivateKey() throws IOException {
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提供的工具类,它可以帮助我们读取私钥:
1 2 3 4 5 6 7 8 9 10 11 12
| 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("无效的密钥格式"); } }
|
获取验签器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
|
@Bean public Verifier getVerifier(PrivateKey merchantPrivateKey) throws IOException, NotFoundException {
log.info("加载证书管理器实例");
CertificatesManager certificatesManager = CertificatesManager.getInstance();
log.info("向证书管理器增加商户信息,并开启自动更新"); try { 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
方法中可见:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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为空"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@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));
CloseableHttpClient httpClient = builder.build();
log.info("构造httpClient成功"); return httpClient; }
|
获取支付回调请求处理器
1 2 3 4 5 6 7 8 9 10
|
@Bean public NotificationHandler notificationHandler(Verifier verifier) { return new NotificationHandler(verifier,apiV3Key.getBytes(StandardCharsets.UTF_8)); }
|
启动服务做测试
1 2 3 4 5 6 7 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实例已启动,下载更新证书线程也启动了,一切准备完毕,静候差遣
集中封装一些枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @AllArgsConstructor @Getter public enum PayType {
WXPAY("微信"),
ALIPAY("支付宝");
private final String type; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @AllArgsConstructor @Getter public enum OrderStatus {
NOTPAY("未支付"),
SUCCESS("支付成功"),
CLOSED("超时已关闭"),
CANCEL("用户已取消"),
REFUND_PROCESSING("退款中"),
REFUND_SUCCESS("已退款"),
REFUND_ABNORMAL("退款异常");
private final String type; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| @AllArgsConstructor @Getter public enum WxApiType {
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; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @AllArgsConstructor @Getter public enum WxNotifyType {
NATIVE_NOTIFY("/api/wx-pay/native/notify"),
REFUND_NOTIFY("/api/wx-pay/refunds/notify");
private final String type; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @AllArgsConstructor @Getter public enum WxRefundStatus {
SUCCESS("SUCCESS"),
CLOSED("CLOSED"),
PROCESSING("PROCESSING"),
ABNORMAL("ABNORMAL");
private final String type; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @AllArgsConstructor @Getter public enum WxTradeState {
SUCCESS("SUCCESS"),
NOTPAY("NOTPAY"),
CLOSED("CLOSED"),
REFUND("REFUND");
private final String type; }
|
枚举类中的值在业务经常会被用到,封装成枚举类,更为优雅
封装响应消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| @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)
注解可使得该类方法可以链式调用:
1
| return R.ok().setMessage("下单成功!")
|
Native下单
Native下单API字典告知了我们必要的参数,并提供了请求示例:
1 2 3 4 5 6 7 8 9 10 11
| { "mchid": "1900006XXX", "out_trade_no": "native12177525012014070332333", "appid": "wxdace645e0bc2cXXX", "description": "Image形象店-深圳腾大-QQ公仔", "notify_url": "https://weixin.qq.com/", "amount": { "total": 1, "currency": "CNY" } }
|
返回示例:
1 2 3
| { "code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz" }
|
如果成功,就能拿到二维码链接
按照示例,封装我们自己的请求体:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
|
@Transactional(rollbackFor = Exception.class) @Override public Map<String, Object> nativePay(Long productId) throws Exception {
log.info("生成订单");
log.info("调用统一下单API");
HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType())); httpPost.addHeader("Accept", "application/json"); httpPost.addHeader("Content-type", "application/json; charset=utf-8");
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())); Map<String, Object> amountMap = new HashMap<>(); amountMap.put("total", orderInfo.getTotalFee()); amountMap.put("currency", "CNY"); paramsMap.put("amount", amountMap);
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) { log.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:
1 2 3 4 5 6 7 8
| { "code": 0, "message": "成功", "data": { "codeUrl": "weixin://wxpay/bizpayurl?pr=HVPisQfzz", "orderNo": "ORDER_20220311155916957" } }
|
codeUrl就是我们二维码的链接,后端可以采用Zxing工具来解析成二维码图片二进制流返回前端。我这里交给前端同学自行做优化处理。
在这里使用QRcode测试一下该codeUrl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <!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字典告诉了我们需要的参数:
1 2 3
| { "mchid": "1230000109" }
|
请求体中放商户号,订单号拼接在URL中即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
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");
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字典
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
|
@Override public String queryOrder(String orderNo) throws Exception {
log.info("查单接口调用 ===> {}", orderNo);
String 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) { log.info("成功"); } else { log.info("查单接口调用,响应码 = " + statusCode + ",返回结果 = " + bodyAsString); throw new IOException("request failed"); }
return bodyAsString;
} finally { response.close(); }
}
|
API测试:
返回JSON:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| { "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提供的做法,这里我如法炮制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
|
@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"); 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 {
notification = notificationHandler.parse(wxRequest); } catch (Exception e) { log.error("通知验签失败"); response.setStatus(500); map.put("code", "ERROR"); map.put("message", "通知验签失败"); return JSON.toJSONString(map); }
String 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中主体内容的工具类,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| public class HttpUtils {
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中查看,相关源码位置我也写在注释中了
简单说明一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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为空"); } }
|
在这里进行获取证书换取公钥签名
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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内容是被加密过的
1 2 3 4 5 6 7
| 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 : 通知验签成功
|
解密内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "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请求,省略繁琐的安全验证过程,得到返回结果…大家自己摸索,多说无益
需要完整实例源码的可私信我