首页
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
Search
1
ElasticSearch ES 安装 Kibana安装 设置密码
421 阅读
2
记一个报错GC overhead limit exceeded解决方法
344 阅读
3
Teamcity + Rancher + 阿里云Code 实现Devops 自动化部署
230 阅读
4
JAVA秒杀系统的简单实现(Redis+RabbitMQ)
209 阅读
5
分布式锁Redisson,完美解决高并发问题
206 阅读
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
登录
/
注册
Search
标签搜索
AOP
支付
小说
docker
SpringBoot
XML
秒杀
K8S
RabbitMQ
工具类
Shiro
多线程
分布式锁
Redisson
接口防刷
Jenkins
Lewis
累计撰写
146
篇文章
累计收到
14
条评论
首页
栏目
JAVA开发
前端相关
Linux相关
电商开发
经验分享
电子书籍
个人随笔
行业资讯
其他
页面
视频
留言
壁纸
直播
下载
友链
统计
推荐
vue
在线工具
搜索到
4
篇与
的结果
2021-11-10
Java中的微信支付(3):API V3对微信服务器响应进行签名验证
1. 前言 微信支付 V3 版本前两篇分别讲了如何对请求做签名和如何获取并刷新微信平台公钥,本篇将继续展开如何对微信支付响应结果的验签。2. 为什么要对响应验签 微信支付会在回调的 HTTP 头部中包括回调报文的签名。商户必须验证响应的签名,保证响应确实来自微信支付服务器,避免中间人攻击。而验证响应签名除了需要微信平台的公钥外还需要从请求头的其它参数。假设以下就是微信支付服务器的响应:HTTP/1.1 200 OK Server: nginx Date: Tue, 02 Apr 2019 12:59:40 GMT Content-Type: application/json; charset=utf-8 Content-Length: 2204 Connection: keep-alive Keep-Alive: timeout=8 Content-Language: zh-CN Request-ID: e2762b10-b6b9-5108-a42c-16fe2422fc8a Wechatpay-Nonce: c5ac7061fccab6bf3e254dcf98995b8c Wechatpay-Signature: CtcbzwtQjN8rnOXItEBJ5aQFSnIXESeV28Pr2YEmf9wsDQ8Nx25ytW6FXBCAFdrr0mgqngX3AD9gNzjnNHzSGTPBSsaEkIfhPF4b8YRRTpny88tNLyprXA0GU5ID3DkZHpjFkX1hAp/D0fva2GKjGRLtvYbtUk/OLYqFuzbjt3yOBzJSKQqJsvbXILffgAmX4pKql+Ln+6UPvSCeKwznvtPaEx+9nMBmKu7Wpbqm/+2ksc0XwjD+xlvlECkCxfD/OJ4gN3IurE0fpjxIkvHDiinQmk51BI7zQD8k1znU7r/spPqB+vZjc5ep6DC5wZUpFu5vJ8MoNKjCu8wnzyCFdA== Wechatpay-Timestamp: 1554209980 Wechatpay-Serial: 5157F09EFDC096DE15EBE81A47057A7232F1B8E1 Cache-Control: no-cache, must-revalidate {"prepay_id":"wx2922034726858082fbd40b511c67630000"}检查平台证书序列号微信支付响应的时候会携带一个微信平台证书序列号,从响应头中的Wechatpay-Serial字段中获取值,用来提示我们要使用该序列号的证书来进行验签,如果不存在就需要我们刷新证书,而上一文我们将平台证书序列号和证书以键值对存在HashMap中,我们只需要检查是否存在即可,不存在就刷新。构造验签名串从响应结果中获取对应下面方法的三个参数就可以构造出验签名串。/** * 构造验签名串. * * @param wechatpayTimestamp HTTP头 Wechatpay-Timestamp 中的应答时间戳。 * @param wechatpayNonce HTTP头 Wechatpay-Nonce 中的应答随机串 * @param body 响应体 * @return the string */ public String responseSign(String wechatpayTimestamp, String wechatpayNonce, String body) { return Stream.of(wechatpayTimestamp, wechatpayNonce, body) .collect(Collectors.joining("\n", "", "\n")); }验证签名待验证的签名从响应头中的Wechatpay-Signature字段中获取,我们使用微信支付平台公钥对验签名串和签名进行SHA256 with RSA签名验证。 // 构造验签名串 final String signatureStr = responseSign(wechatpayTimestamp, wechatpayNonce, body); // 加载SHA256withRSA签名器 Signature signer = Signature.getInstance("SHA256withRSA"); // 用微信平台公钥对签名器进行初始化 signer.initVerify(certificate); // 把我们构造的验签名串更新到签名器中 signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); // 把请求头中微信服务器返回的签名用Base64解码 并使用签名器进行验证 boolean result = signer.verify(Base64Utils.decodeFromString(wechatpaySignature));完整的验签代码/** * 我方对响应验签,和应答签名做比较,使用微信平台证书. * * @param wechatpaySerial response.headers['Wechatpay-Serial'] 当前使用的微信平台证书序列号 * @param wechatpaySignature response.headers['Wechatpay-Signature'] 微信平台签名 * @param wechatpayTimestamp response.headers['Wechatpay-Timestamp'] 微信服务器的时间戳 * @param wechatpayNonce response.headers['Wechatpay-Nonce'] 微信服务器提供的随机串 * @param body response.body 微信服务器的响应体 * @return the boolean */ @SneakyThrows public boolean responseSignVerify(String wechatpaySerial, String wechatpaySignature, String wechatpayTimestamp, String wechatpayNonce, String body) { if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) { refreshCertificate(); } Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial); final String signatureStr = createSign(wechatpayTimestamp, wechatpayNonce, body); Signature signer = Signature.getInstance("SHA256withRSA"); signer.initVerify(certificate); signer.update(signatureStr.getBytes(StandardCharsets.UTF_8)); return signer.verify(Base64Utils.decodeFromString(wechatpaySignature)); }CERTIFICATE_MAP 平台证书容器可参考上一篇文章。3. 总结 验签通过就说明我们请求的响应来自微信服务器就可以针对结果进行对应的逻辑处理了,微信支付 API 无论是 V2 还是 V3 都包含了使用Api 证书对请求进行加签,对响应结果进行验签的流程,十分考验对密码摘要算法的使用,其它无非就是组织参数调用 Http 请求。如果你能够掌握这一能力就会在面试中和工作中占到优势。
2021年11月10日
71 阅读
0 评论
0 点赞
2021-03-17
支付宝支付前后端实现(Vue+Spring Boot)
本文主要总结基于Vue/Spring Boot的支付宝支付实现,兼容H5与电脑端。1、应用创建与配置第一步:登录支付宝开放平台创建应用; 并视情况需要添加“手机网站支付”和“电脑网站支付”能力;手机网站支付可通过浏览器打开支付宝应用程序进行付款,但在微信公众号打开的页面无法通过支付宝进行支付,电脑端的可通过二维码或者登录支付宝账号付款。第二步:密钥配置包括应用私钥、应用公钥和支付宝公钥。可通过支付宝开放平台开发助手生成应用私钥与应用公钥,然后在开放平台中将应用公钥配置到应用中,生成支付宝公钥。最终程序中需要使用到应用私钥与支付宝公钥,其中应用私钥用于对发往支付宝的消息进行加密,而支付宝公钥用于对支付宝返回的数据进行验签。具体配置过程如下:使用支付宝开放平台助手生成应用私钥与公钥切记生成后将私钥与公钥另外保存;登录开放平台配置应用公钥点击接口加签方式中的设置按钮,在弹出窗口中将应用公钥复制进去,然后生成支付宝公钥,将公钥复制下来保存,后续在代码中会需要使用到。生成支付宝公钥后应用公钥基本就用不着了,后续的代码主要使用的是应用私钥和支付宝公钥。2、处理流程概述支付处理步骤如下所示:3、业务的具体实现3.1 前端发起具体支付界面视业务场景而定,用户选择或填写相关信息后,点击确定按钮时调用后端接口生成订单编号,然后将相关信息展示给用户确认,用户确认后再发起付款流程。发起付款流程前端关键代码如下所示:this.$post("/pay/refill", { orderNo: this.orderNo, fee: this.selectedPlan.refillMoney, channel: this.refillChannel === "wx" ? 2 : 1, tbAmount: this.selectedPlan.payMoney, tbSentAmount: this.isVip ? this.selectedPlan.payMoney * 0.5 : 0, }).then((resp) => { this.refillCompleted = true; if (this.refillChannel === "wx") { this.$goPath("/user/pay/wx-pay", resp); } else { // 支付宝 // 打开单独的支付宝页面 this.$goPath("/user/pay/ali-pay", resp); } });注意我的项目支持微信与支付宝两种支付方式,因此前端选择完后会将所选择的方式、金额等信息传给后台生成支付表单3.2 支付表单生成1.依赖包下载:后端需要使用到阿里的包,maven地址:<dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.10.111.ALL</version> </dependency>2.订单生成前端发起订单生成请求后,后端生成订单记录,并根据微信还是支付宝来判断调用微信还是支付宝接口完成支付表单生成。/** * 充值 * * @param refill 充值 * @return 支付相关信息 */ @PreAuthorize("isAuthenticated()") @PostMapping("/refill") public PayDTO refill(@RequestBody @Validated RefillDTO refill) { UserDTO user = this.getLoginUserOrThrow(); if (null == tradeService.findByNo(refill.getOrderNo())) { // 保存订单 TradeDTO tradeDTO = new TradeDTO(); ... tradeService.save(user, tradeDTO); } // 获取支付表单信息 if (refill.getChannel().equals(TradeChannel.WX)) { // 微信 String openId = user.getWxOpenId(); if (refill.getPlatform().equals(1)) { openId = user.getMpOpenId(); } return wxPayService.getPayUrl(openId, refill.getOrderNo(), refill.getFee(), TradeType.REFILL, refill.getPlatform()); } else if (refill.getChannel().equals(TradeChannel.ALIPAY)) { // 支付宝 return aliPayService.getPayForm(refill.getOrderNo(), refill.getFee(), TradeType.REFILL, refill.getPlatform()); } else { throw new UnsupportedOperationException("不支持的支付渠道"); } }3.支付表单生成/** * 获取支付表单 * * @param orderNo 订单号 * @param fee 订单金额 * @param tradeType 交易类型 * @param platform 平台,0:WEB;1:移动端 * @return 下单表单 */ public PayDTO getPayForm(String orderNo, Double fee, TradeType tradeType, Integer platform) { boolean isH5 = Optional.ofNullable(platform).orElse(0).equals(1); AlipayClient alipayClient = new DefaultAlipayClient(API_URL, APP_ID, APP_KEY, "json", "utf-8", ZFB_PUBLIC_KEY, SIGN_TYPE); Map<String, String> params = MapEnhancer.<String, String>create() .put("out_trade_no", orderNo) .put("total_amount", String.format("%.2f", fee)) .put("subject", tradeType.getName()) .put("product_code", "FAST_INSTANT_TRADE_PAY") .get(); String body; try { if (isH5) { if (logger.isDebugEnabled()) { logger.debug("return url: {}", h5ReturnUrl); } AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest(); request.setBizContent(JSONObject.toJSONString(params)); request.setReturnUrl(h5ReturnUrl); request.setNotifyUrl(notifyUrl); body = alipayClient.pageExecute(request).getBody(); } else { AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); request.setBizContent(JSONObject.toJSONString(params)); request.setReturnUrl(returnUrl); request.setNotifyUrl(notifyUrl); body = alipayClient.pageExecute(request).getBody(); } } catch (AlipayApiException e) { logger.error("生成支付宝支付表单失败", e); throw BusinessException.create("生成支付宝支付表单失败"); } if (logger.isDebugEnabled()) { logger.debug("支付表单内容: {}", body); } PayDTO payDTO = new PayDTO(); payDTO.setFee(fee); payDTO.setOrderNo(orderNo); payDTO.setCodeUrl(body); return payDTO; }其中API_URL为支付宝网关地址(https://openapi.alipay.com/gateway.do),APP_ID为支付宝开放平台中的应用id, APP_KEY为前面密钥配置中的应用私钥;ZFB_PUBLIC_KEY为密钥配置中的支付宝公钥。注意需要根据h5及pc网站类型来判断使用哪个API;API关键参数如下:调用接口成功后将请求返回的数据与订单号、交易金额一起返回给前端,由前端进行下一步处理。另外在调用接口时,还需要指定returnUrl与notifyUrl,其中returnUrl是支付宝在用户支付完成后跳转到的页面;notifyUrl是用于接收支付宝支付结果的地址(一般为后端地址,由支付宝调用)。3.3 前端跳转处理前端接收到后端生成的支付表单后,跳转到一个专门的页面,将返回的内容加载到页面中,并自动触发表单提交,处理如下:(ali-pay.vue)<template> <!-- 支付宝支付界面 --> <div class="ali-pay box-shadow mt-4 border-radius"> <div v-html="payInfo.codeUrl" ref="pay"></div> </div> </template> <script> export default { components: {}, props: [], data() { return { payInfo: {}, }; }, mounted() { this.payInfo = this.$route.query; this.$nextTick(() => { this.$refs.pay.children[0].submit(); }); }, methods: {}, }; </script> <style lang="scss"> .ali-pay { } </style>经过以上处理,h5端将会调起支付宝软件进行支付,web端则会显示二维码或账号输入页面。3.4 支付返回页面处理及支付状态更新用户支付完成后,支付宝将会返回生成表单时指定的returnUrl地址,同时会附带订单号在查询参数中;在这个页面中我们可以根据订单号,通过后台调用支付宝接口查询订单状态,然后依此来更新系统订单支付状态。前端页面如下(待完善,可以在trade/status接口返回查询的订单状态,在页面做相应提示):<template> <!-- 支付宝支付界面 --> <div class="ali-pay-success box-shadow mt-4 border-radius p-4"> <div> 支付成功,您可以进入 <el-button type="text" @click="$goPath('/user/account/refill-history')" >充值记录</el-button >查看历史记录 </div> </div> </template> <script> export default { components: {}, props: [], data() { return { payInfo: {}, }; }, mounted() { this.payInfo = this.$route.query; // 更新支付状态 this.$get("/trade/status", { no: this.payInfo.out_trade_no }); }, methods: {}, }; </script><style lang="scss"> .ali-pay-success { } </style>后端/trade/status接口实现如下:/** * 查询交易状态 * * @param no 交易编号 * @return 交易状态,0:失败,1:成功,2:未知 */ @GetMapping("/status") public Integer getTradeStatus(@RequestParam("no") String no) { TradeDTO trade = tradeService.findByNo(no); if (null == trade) { throw BusinessException.create("订单不存在"); } if (trade.getState() == 1 || trade.getState() == 0) { return trade.getState(); } // 交易状态未知时,通过相关支付服务查询状态 if (trade.getChannel().equals(TradeChannel.WX)) { return wxPayService.getTradeStatus(trade); } else if (trade.getChannel().equals(TradeChannel.ALIPAY)) { return aliPayService.getTradeStatus(trade); } return trade.getState(); }这里先在系统里面查询订单状态(状态可能已经变更,因为我们同时会接收支付宝支付成功的通知),如果订单状态未知,那么通过支付宝或微信支付的接口更新订单状态。支付宝支付状态查询接口实现:/** * 获取订单状态 * * @param trade 订单信息 * @return 订单状态 */ public Integer getTradeStatus(TradeDTO trade) { AlipayClient alipayClient = new DefaultAlipayClient(API_URL, APP_ID, APP_KEY, "json", "utf-8", ZFB_PUBLIC_KEY, SIGN_TYPE); AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); Map<String, String> params = MapEnhancer.<String, String>create() .put("out_trade_no", trade.getNo()) .get(); request.setBizContent(JSONObject.toJSONString(params)); AlipayTradeQueryResponse response; try { response = alipayClient.execute(request); } catch (AlipayApiException e) { logger.error("查询订单状态失败", e); return trade.getState(); } if (logger.isDebugEnabled()) { logger.debug("订单信息: {}", JSONObject.toJSONString(response)); } if (response.isSuccess()) { String tradeStatus = response.getTradeStatus(); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { tradeService.tradeSuccess(trade); } else if (tradeStatus.equals("TRADE_CLOSED")) { tradeService.tradeFailed(trade); } } return trade.getState(); }注意查询到结果后,需要更新交易状态,并视情况进行下一步的业务处理,如用户充值的就得增加用户余额等。3.5 支付结果接收至此支付过程基本已经完成。但某些情况用户支付完成后并未返回我们指定的页面,如用户可能直接关闭浏览器等极端情况,因此必须与支付宝推送结果的方式结合,来防止状态不一致。接收支付宝支付结果的接口实现如下:@RequestMapping("ali-notify") public String aliNotify(@RequestParam Map<String, String> params) { if (logger.isDebugEnabled()) { logger.debug("收到支付宝通知: {}", params); } return aliPayService.parseAndSaveTradeResult(params); }具体service方法实现如下:/** * 解析通知结果并保存 * * @param params 通知结果 */ public String parseAndSaveTradeResult(Map<String, String> params) { if (logger.isDebugEnabled()) { logger.debug("接收到支付宝支付结果: {}", params); } // 结果验签 boolean signVerified; try { signVerified = AlipaySignature.rsaCheckV1(params, ZFB_PUBLIC_KEY, "utf-8", SIGN_TYPE); } catch (AlipayApiException e) { logger.error("验签失败", e); return "failure"; } if (!signVerified) { logger.warn("验签失败,通知内容:{}", params); return "failure"; } String orderNo = MapUtils.getString(params, "out_trade_no", ""); if (StringUtils.isBlank(orderNo)) { logger.warn("订单号为空,通知内容:{}", params); return "success"; } // 根据订单号查询订单 TradeDTO trade = tradeService.findByNo(orderNo); if (null == trade) { logger.warn("订单不存在,通知内容:{}", params); return "success"; } if (trade.getState() == 1) { logger.debug("订单已成功"); return "success"; } String tradeStatus = MapUtils.getString(params, "trade_status", ""); if (tradeStatus.equals("TRADE_SUCCESS") || tradeStatus.equals("TRADE_FINISHED")) { trade.setState(1); tradeService.tradeSuccess(trade); } else { trade.setState(0); tradeService.tradeFailed(trade); } return "success"; }注意如果查询到支付状态是成功的,就需要进行下一步的业务处理了,如增加用户余额等。实际交易成功处理,因为同一笔订单可能会有两个进程更新交易状态(通道消息和主动查询的消息),这两个操作肯定只能有一个操作能够生效;如果是单节点情况,可以通过JVM锁来控制;如果多节点的,就需要通过分页式锁来控制了。我的项目里面是采用了Redis实现了个简单的分布式锁处理,具体实现方案网上一大堆,不在此展开。另外,可以看到我的代码里面有很多关于微信支付与支付宝支付的判断,这是因为我同时支持了微信支付与支付宝支付;这两者的支付过程基本类似,本文主要讲的是支付宝支付,微信的未展开,后续再单独对微信支付实现做一个补充。
2021年03月17日
122 阅读
0 评论
0 点赞
2021-03-10
Java中的微信支付(2):API V3 微信平台证书的获取与刷新
1. 前言在Java 中的微信支付(1):API V3 版本签名详解一文中胖哥讲解了微信支付 V3 版本 API 的签名,当我方(你自己的服务器)请求微信支付服务器时需要根据我方的API 证书对参数进行加签,微信服务器会根据我方签名验签以确定请求来自我方服务器。那么同样的道理我方的服务器也要对微信支付服务器的响应进行鉴别来确定响应真的来自微信支付服务器,这就是验签。验签使用的是【微信支付平台证书公钥】,不是商户 API 证书。使用商户 API 证书是验证不过的。今天就来分享一下如何获得微信平台公钥和动态刷新微信平台公钥。2. 获取微信平台证书公钥微信平台证书是微信支付平台自己的证书,我们是管不了的,而且是有效期的。微信服务器会定期更换,所以也要求我方定期获取公钥。而且我们只能通过调用接口/v3/certificates来获得,此接口也需要进行签名(可参考上一篇文章)。你可以获取证书后静态放到服务器上,手动更新静态证书;也可以动态获取一劳永逸。本文采取一劳永逸的办法。平台证书接口文档:https://wechatpay-api.gitbook.io/wechatpay-api-v3/jie-kou-wen-dang/ping-tai-zheng-shu3. 证书和回调报文解密为了保证安全性,微信支付在回调通知和平台证书下载接口中,对关键信息进行了AES-256-GCM加密。也就是说我们拿到响应的信息是被加密的,需要解密后才能获得真正的微信平台证书公钥。响应体大致是这样的,具体根据你调用平台证书接口,应该大差不差是下面这个结构:{ "data": [ { "effective_time": "2020-10-21T14:48:49+08:00", "encrypt_certificate": { // 加密算法 "algorithm": "AEAD_AES_256_GCM", // 附加数据包(可能为空) "associated_data": "certificate", // Base64编码后的密文 "ciphertext": "", // 加密使用的随机串初始化向量) "nonce": "88b4e15a0db9" }, "expire_time": "2025-10-20T14:48:49+08:00", // 证书序列号 "serial_no": "217016F42805DD4D5442059D373F98BFC5252599" } ] }你可以使用各种 JSON 类库取得下面方法的参数进行解密以获取证书,同时这里需要用到APIv3密钥,通用的解密方式为:/** * 解密响应体. * * @param apiV3Key API V3 KEY API v3密钥 商户平台设置的32位字符串 * @param associatedData response.body.data[i].encrypt_certificate.associated_data * @param nonce response.body.data[i].encrypt_certificate.nonce * @param ciphertext response.body.data[i].encrypt_certificate.ciphertext * @return the string * @throws GeneralSecurityException the general security exception */ public String decryptResponseBody(String apiV3Key,String associatedData, String nonce, String ciphertext) { try { Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(StandardCharsets.UTF_8), "AES"); GCMParameterSpec spec = new GCMParameterSpec(128, nonce.getBytes(StandardCharsets.UTF_8)); cipher.init(Cipher.DECRYPT_MODE, key, spec); cipher.updateAAD(associatedData.getBytes(StandardCharsets.UTF_8)); byte[] bytes; try { bytes = cipher.doFinal(Base64Utils.decodeFromString(ciphertext)); } catch (GeneralSecurityException e) { throw new IllegalArgumentException(e); } return new String(bytes, StandardCharsets.UTF_8); } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { throw new IllegalStateException(e); } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { throw new IllegalArgumentException(e); } }回调的请求体也是此方法进行解密。3. 动态刷新然后就能拿到微信平台证书公钥。然后你可以定义个 Map,以证书的序列号为 KEY,以证书为 Value 来动态刷新,关键伪代码:// 定义全局容器 保存微信平台证书公钥 注意线程安全 private static final Map<String, Certificate> CERTIFICATE_MAP = new ConcurrentHashMap<>(); // 下面是刷新方法 refreshCertificate 的核心代码 String publicKey = decryptResponseBody(associatedData, nonce, ciphertext); final CertificateFactory cf = CertificateFactory.getInstance("X509"); ByteArrayInputStream inputStream = new ByteArrayInputStream(publicKey.getBytes(StandardCharsets.UTF_8)); Certificate certificate = null; try { certificate = cf.generateCertificate(inputStream); } catch (CertificateException e) { e.printStackTrace(); } String responseSerialNo = objectNode.get("serial_no").asText(); // 清理HashMap CERTIFICATE_MAP.clear(); // 放入证书 CERTIFICATE_MAP.put(responseSerialNo, certificate);动态刷新的策略就很好写了:// 当证书容器为空 或者 响应提供的证书序列号不在容器中时 就应该刷新了 if (CERTIFICATE_MAP.isEmpty() || !CERTIFICATE_MAP.containsKey(wechatpaySerial)) { refreshCertificate(); } // 然后调用 Certificate certificate = CERTIFICATE_MAP.get(wechatpaySerial);4.总结虽然验签你不做可以拿到其它接口的响应结果,但是从资金安全的角度来说这是十分必要的。同时因为微信平台证书不收我方控制,采取动态刷新也会更加方便,不必再担心过期的问题。本文我们通过调用接口拿到密文并解密获得证书。
2021年03月10日
60 阅读
0 评论
0 点赞
2021-03-10
Java中的微信支付(1):API V3版本签名详解
码农小胖哥服务广大软件开发者。个人博客:https://felord.cn1. 前言最近在折腾微信支付,证书还是比较烦人的,所以有必要分享一些经验,减少你在开发微信支付时的踩坑。目前微信支付的 API 已经发展到V3版本,采用了流行的 Restful 风格。今天来分享微信支付的难点——签名,虽然有很多好用的 SDK 但是如果你想深入了解微信支付还是有帮助的。2. API 证书为了保证资金敏感数据的安全性,确保我们业务中的资金往来交易万无一失。目前微信支付第三方签发的权威的 CA 证书(API 证书)中提供的私钥来进行签名。通过商户平台你可以设置并获取 API 证书。{message type="warning"}切记在第一次设置的时候会提示下载,后面就不再提供下载了,具体参考说明。{/message}设置后找到zip压缩包解压,里面有很多文件,对于 JAVA 开发来说只需要关注apiclient_cert.p12这个证书文件就行了,它包含了公私钥,我们需要把它放在服务端并利用 Java 解析.p12文件获取公钥私钥。{message type="warning"}务必保证证书在服务器端的安全,它涉及到资金安全。{/message}2.解析 API 证书接下来就是证书的解析了,证书的解析有网上很多方法,这里我使用比较“正规”的方法来解析,利用 JDK 安全包的java.security.KeyStore来解析。微信支付 API 证书使用了PKCS12算法,我们通过KeyStore来获取公私钥对的载体KeyPair以及证书序列号serialNumber,我封装了工具类(序列号你自己处理):import org.springframework.core.io.ClassPathResource; import java.security.KeyPair; import java.security.KeyStore; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.X509Certificate; /** * KeyPairFactory * * @author dax * @since 13:41 **/ public class KeyPairFactory { private KeyStore store; private final Object lock = new Object(); /** * 获取公私钥. * * @param keyPath the key path * @param keyAlias the key alias * @param keyPass password * @return the key pair */ public KeyPair createPKCS12(String keyPath, String keyAlias, String keyPass) { ClassPathResource resource = new ClassPathResource(keyPath); char[] pem = keyPass.toCharArray(); try { synchronized (lock) { if (store == null) { synchronized (lock) { store = KeyStore.getInstance("PKCS12"); store.load(resource.getInputStream(), pem); } } } X509Certificate certificate = (X509Certificate) store.getCertificate(keyAlias); certificate.checkValidity(); // 证书的序列号 也有用 String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); // 证书的 公钥 PublicKey publicKey = certificate.getPublicKey(); // 证书的私钥 PrivateKey storeKey = (PrivateKey) store.getKey(keyAlias, pem); return new KeyPair(publicKey, storeKey); } catch (Exception e) { throw new IllegalStateException("Cannot load keys from store: " + resource, e); } } }眼熟的可以看出是胖哥 Spring Security 教程中 JWT 用的公私钥提取方法的修改版本,你可以对比下不同之处。这个方法中有三个参数,这里必须要说明一下:keyPath API 证书apiclient_cert.p12的classpath路径,一般我们会放在resources路径下,当然你可以修改获取证书输入流的方式。keyAlias 证书的别名,这个微信的文档是没有的,胖哥通过加载证书时进行 DEBUG 获取到该值固定为Tenpay Certificate 。keyPass 证书密码,这个默认就是商户号,在其它配置中也需要使用就是mchid,就是你用超级管理员登录微信商户平台在个人资料中的一串数字。3.V3 签名微信支付 V3 版本的签名是我们在调用具体的微信支付的 API 时在 HTTP 请求头中携带特定的编码串供微信支付服务器进行验证请求来源,确保请求是真实可信的。签名格式签名串的具体格式,一共五行一行也不能少,每一行以换行符\n结束。HTTP请求方法\n URL\n 请求时间戳\n 请求随机串\n 请求报文主体\nHTTP 请求方法 你调用的微信支付 API 所要求的请求方法,比如 APP 支付为POST。URL 比如 APP 支付文档中为https://api.mch.weixin.qq.com/v3/pay/transactions/app,除去域名部分得到参与签名的 URL。如果请求中有查询参数,URL 末尾应附加有'?'和对应的查询字符串。这里为/v3/pay/transactions/app。请求时间戳 服务器系统时间戳,保证服务器时间正确并利用System.currentTimeMillis() / 1000获取即可。请求随机串 找个工具类生成类似593BEC0C930BF1AFEB40B4A08C8FB242的字符串就行了。请求报文主体 如果是GET请求直接为空字符"" ;当请求方法为POST或PUT时,请使用真实发送的JSON报文。图片上传 API,请使用meta对应的JSON报文。生成签名然后我们使用商户私钥对按照上面格式的待签名串进行 SHA256 with RSA 签名,并对签名结果进行Base64 编码得到签名值。对应的核心 Java 代码为:/** * V3 SHA256withRSA 签名. * * @param method 请求方法 GET POST PUT DELETE 等 * @param canonicalUrl 例如 https://api.mch.weixin.qq.com/v3/pay/transactions/app?version=1 ——> /v3/pay/transactions/app?version=1 * @param timestamp 当前时间戳 因为要配置到TOKEN 中所以 签名中的要跟TOKEN 保持一致 * @param nonceStr 随机字符串 要和TOKEN中的保持一致 * @param body 请求体 GET 为 "" POST 为JSON * @param keyPair 商户API 证书解析的密钥对 实际使用的是其中的私钥 * @return the string */ @SneakyThrows String sign(String method, String canonicalUrl, long timestamp, String nonceStr, String body, KeyPair keyPair) { String signatureStr = Stream.of(method, canonicalUrl, String.valueOf(timestamp), nonceStr, body) .collect(Collectors.joining("\n", "", "\n")); Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(keyPair.getPrivate()); sign.update(signatureStr.getBytes(StandardCharsets.UTF_8)); return Base64Utils.encodeToString(sign.sign()); }4. 使用签名签名生成后会同一些参数组成一个Token放置到对应 HTTP 请求的Authorization请求头中,格式为: Authorization: WECHATPAY2-SHA256-RSA2048 {Token} Token由以下五部分组成:发起请求的商户(包括直连商户、服务商或渠道商)的商户号mchid商户 API 证书序列号serial_no,用于声明所使用的证书请求随机串nonce_str时间戳timestamp签名值signatureToken生成的核心代码:/** * 生成Token. * * @param mchId 商户号 * @param nonceStr 随机字符串 * @param timestamp 时间戳 * @param serialNo 证书序列号 * @param signature 签名 * @return the string */ String token(String mchId, String nonceStr, long timestamp, String serialNo, String signature) { final String TOKEN_PATTERN = "mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""; // 生成token return String.format(TOKEN_PATTERN, wechatPayProperties.getMchId(), nonceStr, timestamp, serialNo, signature); }将生成的Token按照上述格式放入请求头中即可完成签名的使用。5.总结本文我们对微信支付 V3 版本的难点签名以及签名的使用进行了完整的分析,同时对 API 证书的解析也进行了讲解,相信能够帮助你在支付开发中解决一些具体的问题。
2021年03月10日
62 阅读
0 评论
0 点赞