diff --git a/src/main/java/com/yxt/pay/biz/order/OrderRest.java b/src/main/java/com/yxt/pay/biz/order/OrderRest.java index 4efe493..550427d 100644 --- a/src/main/java/com/yxt/pay/biz/order/OrderRest.java +++ b/src/main/java/com/yxt/pay/biz/order/OrderRest.java @@ -6,14 +6,13 @@ import com.yxt.pay.api.order.OrderQuery; import com.yxt.pay.api.order.PayOrderVo; import com.yxt.pay.utils.ApiBaseAction; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.HashMap; +import java.util.Map; /** * @description: @@ -42,9 +41,14 @@ public class OrderRest extends ApiBaseAction { return orderService.pay(query, getClientIp()); } - @PostMapping("/wxPayNotify") + /* @PostMapping("/wxPayNotify") ResultBean wxPayNotify(HttpServletRequest request, HttpServletResponse response) throws IOException { return orderService.wxPayNotify(request, response); + }*/ + + @PostMapping( "/wxPayNotify") + public String payNotify(HttpServletRequest request) { + return orderService.payNotify(request); } @PostMapping("selectOrder") diff --git a/src/main/java/com/yxt/pay/biz/order/OrderService.java b/src/main/java/com/yxt/pay/biz/order/OrderService.java index 05b7e92..7990389 100644 --- a/src/main/java/com/yxt/pay/biz/order/OrderService.java +++ b/src/main/java/com/yxt/pay/biz/order/OrderService.java @@ -1,5 +1,6 @@ package com.yxt.pay.biz.order; +import com.sun.org.apache.bcel.internal.generic.RETURN; import com.yxt.common.base.service.MybatisBaseService; import com.yxt.common.core.result.ResultBean; import com.yxt.pay.api.order.*; @@ -7,10 +8,14 @@ import com.yxt.pay.api.wxpay.WxPayVo; import com.yxt.pay.utils.*; import com.yxt.pay.utils.applet.WechatRefundApiResult; import com.yxt.pay.utils.applet.WechatUtil; +import com.yxt.pay.utils.wx.HttpKit; +import com.yxt.pay.utils.wx.SignType; +import com.yxt.pay.utils.wx.WxPayKit; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.RestTemplate; import javax.servlet.http.HttpServletRequest; @@ -103,7 +108,7 @@ public class OrderService extends MybatisBaseService { //支付金额 parame.put("total_fee", new BigDecimal(payOrder.getTotalTee()).multiply(new BigDecimal(100)).intValue()); // 回调地址 - parame.put("notify_url", urlComponent.getDoMainUrl() + "/wxPay/payNotify"); + parame.put("notify_url", urlComponent.getDoMainUrl() + "order/wxPayNotify"); // 交易类型APP parame.put("trade_type", tradeType); parame.put("spbill_create_ip", ip); @@ -155,7 +160,7 @@ public class OrderService extends MybatisBaseService { return rb; } - public ResultBean wxPayNotify(HttpServletRequest request, HttpServletResponse response) { + /* public ResultBean wxPayNotify(HttpServletRequest request, HttpServletResponse response) { ResultBean rb = ResultBean.fireFail(); try { request.setCharacterEncoding("UTF-8"); @@ -201,7 +206,7 @@ public class OrderService extends MybatisBaseService { } return rb; - } + }*/ public ResultBean selectOrder(OrderQuery query) { ResultBean rb = ResultBean.fireFail(); @@ -290,4 +295,40 @@ public class OrderService extends MybatisBaseService { public List selectOrderList() { return baseMapper.selectOrderList(); } + + public String payNotify(HttpServletRequest request) { + String xmlMsg = HttpKit.readData(request); + Map params = WxPayKit.xmlToMap(xmlMsg); + log.info("微信支付通知=" + params); + String returnCode = params.get("return_code"); + //订单编号 + String out_trade_no = params.get("out_trade_no"); + //根据订单编号查询 + PayOrder payOrder = baseMapper.selectBySn(out_trade_no); + if (payOrder == null) { + return null; + } + payOrder.setState(2);//已支付.state:1待支付,2已支付,3已过期 + payOrder.setModifyTime(new Date()); + payOrder.setPayTypeValue("微信"); + WxPayVo wxPayVo = new WxPayVo(payOrder.getSource()); + // 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态 + // 注意此处签名方式需与统一下单的签名类型一致 + if (WxPayKit.verifyNotify(params, wxPayVo.getSecret(), SignType.MD5)) { + if (WxPayKit.codeIsOk(returnCode)) { + baseMapper.updateById(payOrder); + //调业务的回调地址接口。mainSid传过去。 + ResponseEntity postForEntity = new RestTemplate().getForEntity(payOrder.getReturnUrl() + payOrder.getSid(), ResultBean.class); + // 发送通知等 + Map xml = new HashMap(2); + xml.put("return_code", "SUCCESS"); + xml.put("return_msg", "OK"); + return WxPayKit.toXml(xml); + } else { + log.error("订单" + out_trade_no + "支付失败"); + return null; + } + } + return null; + } } diff --git a/src/main/java/com/yxt/pay/utils/wx/AbstractHttpDelegate.java b/src/main/java/com/yxt/pay/utils/wx/AbstractHttpDelegate.java new file mode 100644 index 0000000..89c6ec4 --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/AbstractHttpDelegate.java @@ -0,0 +1,504 @@ +package com.yxt.pay.utils.wx; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.http.HttpUtil; +import cn.hutool.http.ssl.SSLSocketFactoryBuilder; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import java.io.File; +import java.io.FileInputStream; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.util.Map; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付等常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

Http 代理类

+ * + * @author Javen + */ +public abstract class AbstractHttpDelegate { + + /** + * get 请求 + * + * @param url 请求url + * @return {@link String} 请求返回的结果 + */ + public String get(String url) { + return HttpUtil.get(url); + } + + /** + * get 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @return {@link String} 请求返回的结果 + */ + public String get(String url, Map paramMap) { + return HttpUtil.get(url, paramMap); + } + + /** + * get 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse get(String url, Map paramMap, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = getToResponse(url, paramMap, headers); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * post 请求 + * + * @param url 请求url + * @param data 请求参数 + * @return {@link String} 请求返回的结果 + */ + public String post(String url, String data) { + return HttpUtil.post(url, data); + } + + /** + * post 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @return {@link String} 请求返回的结果 + */ + public String post(String url, Map paramMap) { + return HttpUtil.post(url, paramMap); + } + + /** + * post 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse post(String url, Map paramMap, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = postToResponse(url, headers, paramMap); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * post 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse post(String url, String data, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = postToResponse(url, headers, data); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * patch 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse patch(String url, Map paramMap, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = patchToResponse(url, headers, paramMap); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * patch 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse patch(String url, String data, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = patchToResponse(url, headers, data); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * delete 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse delete(String url, Map paramMap, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = deleteToResponse(url, headers, paramMap); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * delete 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse delete(String url, String data, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = deleteToResponse(url, headers, data); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * put 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse put(String url, Map paramMap, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = putToResponse(url, headers, paramMap); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * put 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param headers 请求头 + * @return {@link IJPayHttpResponse} 请求返回的结果 + */ + public IJPayHttpResponse put(String url, String data, Map headers) { + IJPayHttpResponse response = new IJPayHttpResponse(); + HttpResponse httpResponse = putToResponse(url, headers, data); + response.setBody(httpResponse.body()); + response.setStatus(httpResponse.getStatus()); + response.setHeaders(httpResponse.headers()); + return response; + } + + /** + * 上传文件 + * + * @param url 请求url + * @param data 请求参数 + * @param certPath 证书路径 + * @param certPass 证书密码 + * @param filePath 上传文件路径 + * @param protocol 协议 + * @return {@link String} 请求返回的结果 + */ + public String upload(String url, String data, String certPath, String certPass, String filePath, String protocol) { + try { + File file = FileUtil.newFile(filePath); + return HttpRequest.post(url) + .setSSLSocketFactory(SSLSocketFactoryBuilder + .create() + .setProtocol(protocol) + .setKeyManagers(getKeyManager(certPass, certPath, null)) + .setSecureRandom(new SecureRandom()) + .build() + ) + .header("Content-Type", "multipart/form-data;boundary=\"boundary\"") + .form("file", file) + .form("meta", data) + .execute() + .body(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * 上传文件 + * + * @param url 请求url + * @param data 请求参数 + * @param certPath 证书路径 + * @param certPass 证书密码 + * @param filePath 上传文件路径 + * @return {@link String} 请求返回的结果 + */ + public String upload(String url, String data, String certPath, String certPass, String filePath) { + return upload(url, data, certPath, certPass, filePath, SSLSocketFactoryBuilder.TLSv1); + } + + /** + * post 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param certPath 证书路径 + * @param certPass 证书密码 + * @param protocol 协议 + * @return {@link String} 请求返回的结果 + */ + public String post(String url, String data, String certPath, String certPass, String protocol) { + try { + return HttpRequest.post(url) + .setSSLSocketFactory(SSLSocketFactoryBuilder + .create() + .setProtocol(protocol) + .setKeyManagers(getKeyManager(certPass, certPath, null)) + .setSecureRandom(new SecureRandom()) + .build() + ) + .body(data) + .execute() + .body(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * post 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param certPath 证书路径 + * @param certPass 证书密码 + * @return {@link String} 请求返回的结果 + */ + public String post(String url, String data, String certPath, String certPass) { + return post(url, data, certPath, certPass, SSLSocketFactoryBuilder.TLSv1); + } + + /** + * post 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param certFile 证书文件输入流 + * @param certPass 证书密码 + * @param protocol 协议 + * @return {@link String} 请求返回的结果 + */ + public String post(String url, String data, InputStream certFile, String certPass, String protocol) { + try { + return HttpRequest.post(url) + .setSSLSocketFactory(SSLSocketFactoryBuilder + .create() + .setProtocol(protocol) + .setKeyManagers(getKeyManager(certPass, null, certFile)) + .setSecureRandom(new SecureRandom()) + .build() + ) + .body(data) + .execute() + .body(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + /** + * post 请求 + * + * @param url 请求url + * @param data 请求参数 + * @param certFile 证书文件输入流 + * @param certPass 证书密码 + * @return {@link String} 请求返回的结果 + */ + public String post(String url, String data, InputStream certFile, String certPass) { + return post(url, data, certFile, certPass, SSLSocketFactoryBuilder.TLSv1); + } + + /** + * get 请求 + * + * @param url 请求url + * @param paramMap 请求参数 + * @param headers 请求头 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse getToResponse(String url, Map paramMap, Map headers) { + return HttpRequest.get(url) + .addHeaders(headers) + .form(paramMap) + .execute(); + } + + /** + * post 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param data 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse postToResponse(String url, Map headers, String data) { + return HttpRequest.post(url) + .addHeaders(headers) + .body(data) + .execute(); + } + + /** + * post 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param paramMap 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse postToResponse(String url, Map headers, Map paramMap) { + return HttpRequest.post(url) + .addHeaders(headers) + .form(paramMap) + .execute(); + } + + /** + * patch 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param paramMap 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse patchToResponse(String url, Map headers, Map paramMap) { + return HttpRequest.patch(url) + .addHeaders(headers) + .form(paramMap) + .execute(); + } + + /** + * patch 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param data 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse patchToResponse(String url, Map headers, String data) { + return HttpRequest.patch(url) + .addHeaders(headers) + .body(data) + .execute(); + } + + /** + * delete 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param data 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse deleteToResponse(String url, Map headers, String data) { + return HttpRequest.delete(url) + .addHeaders(headers) + .body(data) + .execute(); + } + + /** + * delete 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param paramMap 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse deleteToResponse(String url, Map headers, Map paramMap) { + return HttpRequest.delete(url) + .addHeaders(headers) + .form(paramMap) + .execute(); + } + + /** + * put 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param data 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse putToResponse(String url, Map headers, String data) { + return HttpRequest.put(url) + .addHeaders(headers) + .body(data) + .execute(); + } + + /** + * put 请求 + * + * @param url 请求url + * @param headers 请求头 + * @param paramMap 请求参数 + * @return {@link HttpResponse} 请求返回的结果 + */ + private HttpResponse putToResponse(String url, Map headers, Map paramMap) { + return HttpRequest.put(url) + .addHeaders(headers) + .form(paramMap) + .execute(); + } + + + private KeyManager[] getKeyManager(String certPass, String certPath, InputStream certFile) throws Exception { + KeyStore clientStore = KeyStore.getInstance("PKCS12"); + if (certFile != null) { + clientStore.load(certFile, certPass.toCharArray()); + } else { + clientStore.load(new FileInputStream(certPath), certPass.toCharArray()); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(clientStore, certPass.toCharArray()); + return kmf.getKeyManagers(); + } +} diff --git a/src/main/java/com/yxt/pay/utils/wx/AesUtil.java b/src/main/java/com/yxt/pay/utils/wx/AesUtil.java new file mode 100644 index 0000000..2be1f5b --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/AesUtil.java @@ -0,0 +1,70 @@ +package com.yxt.pay.utils.wx; + +import cn.hutool.core.codec.Base64; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

工具类 AesUtil

+ * + * @author 微信 + */ +public class AesUtil { + + static final int KEY_LENGTH_BYTE = 32; + static final int TAG_LENGTH_BIT = 128; + private final byte[] aesKey; + + /** + * @param key APIv3 密钥 + */ + public AesUtil(byte[] key) { + if (key.length != KEY_LENGTH_BYTE) { + throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); + } + this.aesKey = key; + } + + /** + * 证书和回调报文解密 + * + * @param associatedData associated_data + * @param nonce nonce + * @param cipherText ciphertext + * @return {String} 平台证书明文 + * @throws GeneralSecurityException 异常 + */ + public String decryptToString(byte[] associatedData, byte[] nonce, String cipherText) throws GeneralSecurityException { + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + + SecretKeySpec key = new SecretKeySpec(aesKey, "AES"); + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce); + + cipher.init(Cipher.DECRYPT_MODE, key, spec); + cipher.updateAAD(associatedData); + + return new String(cipher.doFinal(Base64.decode(cipherText)), StandardCharsets.UTF_8); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException(e); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/yxt/pay/utils/wx/HttpKit.java b/src/main/java/com/yxt/pay/utils/wx/HttpKit.java new file mode 100644 index 0000000..aa290bd --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/HttpKit.java @@ -0,0 +1,87 @@ +package com.yxt.pay.utils.wx; + + +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

Http 工具类

+ * + * @author Javen + */ +public class HttpKit { + + private static AbstractHttpDelegate delegate = new DefaultHttpKit(); + + public static AbstractHttpDelegate getDelegate() { + return delegate; + } + + public static void setDelegate(AbstractHttpDelegate delegate) { + HttpKit.delegate = delegate; + } + + 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(); + } + } + } + } + + /** + * 将同步通知的参数转化为Map + * + * @param request {@link HttpServletRequest} + * @return 转化后的 Map + */ + public static Map toMap(HttpServletRequest request) { + Map params = new HashMap<>(); + Map requestParams = request.getParameterMap(); + for (String name : requestParams.keySet()) { + String[] values = requestParams.get(name); + String valueStr = ""; + for (int i = 0; i < values.length; i++) { + valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ","; + } + params.put(name, valueStr); + } + return params; + } +} + +/** + * 使用 huTool 实现的 Http 工具类 + * + * @author Javen + */ +class DefaultHttpKit extends AbstractHttpDelegate { +} diff --git a/src/main/java/com/yxt/pay/utils/wx/IJPayHttpResponse.java b/src/main/java/com/yxt/pay/utils/wx/IJPayHttpResponse.java new file mode 100644 index 0000000..e785966 --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/IJPayHttpResponse.java @@ -0,0 +1,77 @@ + +package com.yxt.pay.utils.wx; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.map.CaseInsensitiveMap; +import cn.hutool.core.util.StrUtil; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

IJPay Http Response

+ * + * @author Javen + */ +public class IJPayHttpResponse implements Serializable { + private static final long serialVersionUID = 6089103955998013402L; + private String body; + private int status; + private Map> headers; + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public int getStatus() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public Map> getHeaders() { + return headers; + } + + public void setHeaders(Map> headers) { + this.headers = headers; + } + + public String getHeader(String name) { + List values = this.headerList(name); + return CollectionUtil.isEmpty(values) ? null : values.get(0); + } + + private List headerList(String name) { + if (StrUtil.isBlank(name)) { + return null; + } else { + CaseInsensitiveMap> headersIgnoreCase = new CaseInsensitiveMap<>(getHeaders()); + return headersIgnoreCase.get(name.trim()); + } + } + + @Override + public String toString() { + return "IJPayHttpResponse{" + + "body='" + body + '\'' + + ", status=" + status + + ", headers=" + headers + + '}'; + } +} diff --git a/src/main/java/com/yxt/pay/utils/wx/PayKit.java b/src/main/java/com/yxt/pay/utils/wx/PayKit.java new file mode 100644 index 0000000..902c1b2 --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/PayKit.java @@ -0,0 +1,521 @@ +package com.yxt.pay.utils.wx; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ClassPathResource; +import cn.hutool.core.io.resource.Resource; +import cn.hutool.core.lang.Snowflake; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.digest.HmacAlgorithm; +import com.ijpay.core.XmlHelper; +import com.ijpay.core.enums.RequestMethod; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import java.io.File; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.*; +import java.util.*; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

IJPay 工具类

+ * + * @author Javen + */ +public class PayKit { + + /** + * 生成16进制的 sha256 字符串 + * + * @param data 数据 + * @param key 密钥 + * @return sha256 字符串 + */ + public static String hmacSha256(String data, String key) { + return SecureUtil.hmac(HmacAlgorithm.HmacSHA256, key).digestHex(data); + } + + /** + * SHA1加密文件,生成16进制SHA1字符串
+ * + * @param dataFile 被加密文件 + * @return SHA1 字符串 + */ + public static String sha1(File dataFile) { + return SecureUtil.sha1(dataFile); + } + + /** + * SHA1加密,生成16进制SHA1字符串
+ * + * @param data 数据 + * @return SHA1 字符串 + */ + public static String sha1(InputStream data) { + return SecureUtil.sha1(data); + } + + /** + * SHA1加密,生成16进制SHA1字符串
+ * + * @param data 数据 + * @return SHA1 字符串 + */ + public static String sha1(String data) { + return SecureUtil.sha1(data); + } + + /** + * 生成16进制 MD5 字符串 + * + * @param data 数据 + * @return MD5 字符串 + */ + public static String md5(String data) { + return SecureUtil.md5(data); + } + + /** + * AES 解密 + * + * @param base64Data 需要解密的数据 + * @param key 密钥 + * @return 解密后的数据 + */ + public static String decryptData(String base64Data, String key) { + return SecureUtil.aes(md5(key).toLowerCase().getBytes()).decryptStr(base64Data); + } + + /** + * AES 加密 + * + * @param data 需要加密的数据 + * @param key 密钥 + * @return 加密后的数据 + */ + public static String encryptData(String data, String key) { + return SecureUtil.aes(md5(key).toLowerCase().getBytes()).encryptBase64(data.getBytes()); + } + + /** + * 简化的UUID,去掉了横线,使用性能更好的 ThreadLocalRandom 生成UUID + * + * @return 简化的 UUID,去掉了横线 + */ + public static String generateStr() { + return IdUtil.fastSimpleUUID(); + } + + /** + * 雪花算法 + * + * @param workerId 终端ID + * @param dataCenterId 数据中心ID + * @return {@link Snowflake} + */ + public static Snowflake getSnowflake(long workerId, long dataCenterId) { + return IdUtil.getSnowflake(workerId, dataCenterId); + } + + /** + * 把所有元素排序 + * + * @param params 需要排序并参与字符拼接的参数组 + * @return 拼接后字符串 + */ + public static String createLinkString(Map params) { + return createLinkString(params, false); + } + + /** + * @param params 需要排序并参与字符拼接的参数组 + * @param encode 是否进行URLEncoder + * @return 拼接后字符串 + */ + public static String createLinkString(Map params, boolean encode) { + return createLinkString(params, "&", encode); + } + + /** + * @param params 需要排序并参与字符拼接的参数组 + * @param connStr 连接符号 + * @param encode 是否进行URLEncoder + * @return 拼接后字符串 + */ + public static String createLinkString(Map params, String connStr, boolean encode) { + return createLinkString(params, connStr, encode, false); + } + + public static String createLinkString(Map params, String connStr, boolean encode, boolean quotes) { + List keys = new ArrayList<>(params.keySet()); + Collections.sort(keys); + StringBuilder content = new StringBuilder(); + for (int i = 0; i < keys.size(); i++) { + String key = keys.get(i); + String value = params.get(key); + // 拼接时,不包括最后一个&字符 + if (i == keys.size() - 1) { + if (quotes) { + content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"'); + } else { + content.append(key).append("=").append(encode ? urlEncode(value) : value); + } + } else { + if (quotes) { + content.append(key).append("=").append('"').append(encode ? urlEncode(value) : value).append('"').append(connStr); + } else { + content.append(key).append("=").append(encode ? urlEncode(value) : value).append(connStr); + } + } + } + return content.toString(); + } + + + /** + * URL 编码 + * + * @param src 需要编码的字符串 + * @return 编码后的字符串 + */ + public static String urlEncode(String src) { + try { + return URLEncoder.encode(src, CharsetUtil.UTF_8).replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 遍历 Map 并构建 xml 数据 + * + * @param params 需要遍历的 Map + * @param prefix xml 前缀 + * @param suffix xml 后缀 + * @return xml 字符串 + */ + public static StringBuffer forEachMap(Map params, String prefix, String suffix) { + StringBuffer xml = new StringBuffer(); + if (StrUtil.isNotEmpty(prefix)) { + xml.append(prefix); + } + for (Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + String value = entry.getValue(); + // 略过空值 + if (StrUtil.isEmpty(value)) { + continue; + } + xml.append("<").append(key).append(">"); + xml.append(entry.getValue()); + xml.append(""); + } + if (StrUtil.isNotEmpty(suffix)) { + xml.append(suffix); + } + return xml; + } + + /** + * 微信下单 map to xml + * + * @param params Map 参数 + * @return xml 字符串 + */ + public static String toXml(Map params) { + StringBuffer xml = forEachMap(params, "", ""); + return xml.toString(); + } + + /** + * 针对支付的 xml,没有嵌套节点的简单处理 + * + * @param xmlStr xml 字符串 + * @return 转化后的 Map + */ + public static Map xmlToMap(String xmlStr) { + XmlHelper xmlHelper = XmlHelper.of(xmlStr); + return xmlHelper.toMap(); + } + + /** + * 构造签名串 + * + * @param method {@link RequestMethod} GET,POST,PUT等 + * @param url 请求接口 /v3/certificates + * @param timestamp 获取发起请求时的系统当前时间戳 + * @param nonceStr 随机字符串 + * @param body 请求报文主体 + * @return 待签名字符串 + */ + public static String buildSignMessage(RequestMethod method, String url, long timestamp, String nonceStr, String body) { + ArrayList arrayList = new ArrayList<>(); + arrayList.add(method.toString()); + arrayList.add(url); + arrayList.add(String.valueOf(timestamp)); + arrayList.add(nonceStr); + arrayList.add(body); + return buildSignMessage(arrayList); + } + + /** + * 构造签名串 + * + * @param timestamp 应答时间戳 + * @param nonceStr 应答随机串 + * @param body 应答报文主体 + * @return 应答待签名字符串 + */ + public static String buildSignMessage(String timestamp, String nonceStr, String body) { + ArrayList arrayList = new ArrayList<>(); + arrayList.add(timestamp); + arrayList.add(nonceStr); + arrayList.add(body); + return buildSignMessage(arrayList); + } + + /** + * 构造签名串 + * + * @param signMessage 待签名的参数 + * @return 构造后带待签名串 + */ + public static String buildSignMessage(ArrayList signMessage) { + if (signMessage == null || signMessage.size() <= 0) { + return null; + } + StringBuilder sbf = new StringBuilder(); + for (String str : signMessage) { + sbf.append(str).append("\n"); + } + return sbf.toString(); + } + + /** + * v3 接口创建签名 + * + * @param signMessage 待签名的参数 + * @param keyPath key.pem 证书路径 + * @return 生成 v3 签名 + * @throws Exception 异常信息 + */ + public static String createSign(ArrayList signMessage, String keyPath) throws Exception { + return createSign(buildSignMessage(signMessage), keyPath); + } + + /** + * v3 接口创建签名 + * + * @param signMessage 待签名的参数 + * @param privateKey 商户私钥 + * @return 生成 v3 签名 + * @throws Exception 异常信息 + */ + public static String createSign(ArrayList signMessage, PrivateKey privateKey) throws Exception { + return createSign(buildSignMessage(signMessage), privateKey); + } + + + /** + * v3 接口创建签名 + * + * @param signMessage 待签名的参数 + * @param keyPath key.pem 证书路径 + * @return 生成 v3 签名 + * @throws Exception 异常信息 + */ + public static String createSign(String signMessage, String keyPath) throws Exception { + if (StrUtil.isEmpty(signMessage)) { + return null; + } + // 获取商户私钥 + PrivateKey privateKey = PayKit.getPrivateKey(keyPath); + // 生成签名 + return RsaKit.encryptByPrivateKey(signMessage, privateKey); + } + + /** + * v3 接口创建签名 + * + * @param signMessage 待签名的参数 + * @param privateKey 商户私钥 + * @return 生成 v3 签名 + * @throws Exception 异常信息 + */ + public static String createSign(String signMessage, PrivateKey privateKey) throws Exception { + if (StrUtil.isEmpty(signMessage)) { + return null; + } + // 生成签名 + return RsaKit.encryptByPrivateKey(signMessage, privateKey); + } + + /** + * 获取授权认证信息 + * + * @param mchId 商户号 + * @param serialNo 商户API证书序列号 + * @param nonceStr 请求随机串 + * @param timestamp 时间戳 + * @param signature 签名值 + * @param authType 认证类型,目前为WECHATPAY2-SHA256-RSA2048 + * @return 请求头 Authorization + */ + public static String getAuthorization(String mchId, String serialNo, String nonceStr, String timestamp, String signature, String authType) { + Map params = new HashMap<>(5); + params.put("mchid", mchId); + params.put("serial_no", serialNo); + params.put("nonce_str", nonceStr); + params.put("timestamp", timestamp); + params.put("signature", signature); + return authType.concat(" ").concat(createLinkString(params, ",", false, true)); + } + + /** + * 获取商户私钥 + * + * @param keyPath 商户私钥证书路径 + * @return {@link String} 商户私钥 + * @throws Exception 异常信息 + */ + public static String getPrivateKeyStr(String keyPath) throws Exception { + return RsaKit.getPrivateKeyStr(getPrivateKey(keyPath)); + } + + /** + * 获取商户私钥 + * + * @param keyPath 商户私钥证书路径 + * @return {@link PrivateKey} 商户私钥 + * @throws Exception 异常信息 + */ + public static PrivateKey getPrivateKey(String keyPath) throws Exception { + String originalKey = FileUtil.readUtf8String(keyPath); + return getPrivateKeyByKeyContent(originalKey); + } + + /** + * 获取商户私钥 + * + * @param originalKey 私钥文本内容 + * @return {@link PrivateKey} 商户私钥 + * @throws Exception 异常信息 + */ + public static PrivateKey getPrivateKeyByKeyContent(String originalKey) throws Exception { + String privateKey = originalKey + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + return RsaKit.loadPrivateKey(privateKey); + } + + /** + * 获取证书 + * + * @param inputStream 证书文件 + * @return {@link X509Certificate} 获取证书 + */ + public static X509Certificate getCertificate(InputStream inputStream) { + try { + CertificateFactory cf = CertificateFactory.getInstance("X509"); + X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream); + cert.checkValidity(); + return cert; + } catch (CertificateExpiredException e) { + throw new RuntimeException("证书已过期", e); + } catch (CertificateNotYetValidException e) { + throw new RuntimeException("证书尚未生效", e); + } catch (CertificateException e) { + throw new RuntimeException("无效的证书", e); + } + } + + /** + * 公钥加密 + * + * @param data 待加密数据 + * @param certificate 平台公钥证书 + * @return 加密后的数据 + * @throws Exception 异常信息 + */ + public static String rsaEncryptOAEP(String data, X509Certificate certificate) throws Exception { + try { + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); + cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey()); + + byte[] dataByte = data.getBytes(StandardCharsets.UTF_8); + byte[] cipherData = cipher.doFinal(dataByte); + return Base64.encode(cipherData); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("无效的证书", e); + } catch (IllegalBlockSizeException | BadPaddingException e) { + throw new IllegalBlockSizeException("加密原串的长度不能超过214字节"); + } + } + + /** + * 私钥解密 + * + * @param cipherText 加密字符 + * @param privateKey 私钥 + * @return 解密后的数据 + * @throws Exception 异常信息 + */ + public static String rsaDecryptOAEP(String cipherText, PrivateKey privateKey) throws Exception { + try { + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] data = Base64.decode(cipherText); + return new String(cipher.doFinal(data), StandardCharsets.UTF_8); + } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { + throw new RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); + } catch (InvalidKeyException e) { + throw new IllegalArgumentException("无效的私钥", e); + } catch (BadPaddingException | IllegalBlockSizeException e) { + throw new BadPaddingException("解密失败"); + } + } + + /** + * 传入 classPath 静态资源路径返回文件输入流 + * + * @param classPath 静态资源路径 + * @return InputStream + */ + public static InputStream getFileToStream(String classPath) { + Resource resource = new ClassPathResource(classPath); + return resource.getStream(); + } + + /** + * 传入 classPath 静态资源路径返回绝对路径 + * + * @param classPath 静态资源路径 + * @return 绝对路径 + */ + public static String getAbsolutePath(String classPath) { + return new ClassPathResource(classPath).getAbsolutePath(); + } +} \ No newline at end of file diff --git a/src/main/java/com/yxt/pay/utils/wx/RequestMethod.java b/src/main/java/com/yxt/pay/utils/wx/RequestMethod.java new file mode 100644 index 0000000..1679a8e --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/RequestMethod.java @@ -0,0 +1,64 @@ +package com.yxt.pay.utils.wx; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

HTTP 请求的方法

+ * + * @author Javen + */ +public enum RequestMethod { + /** + * 上传实质是 post 请求 + */ + UPLOAD("POST"), + /** + * post 请求 + */ + POST("POST"), + /** + * get 请求 + */ + GET("GET"), + /** + * put 请求 + */ + PUT("PUT"), + /** + * delete 请求 + */ + DELETE("DELETE"), + /** + * options 请求 + */ + OPTIONS("OPTIONS"), + /** + * head 请求 + */ + HEAD("HEAD"), + /** + * trace 请求 + */ + TRACE("TRACE"), + /** + * connect 请求 + */ + CONNECT("CONNECT"); + + private final String method; + + RequestMethod(String method) { + this.method = method; + } + + @Override + public String toString() { + return this.method; + } +} diff --git a/src/main/java/com/yxt/pay/utils/wx/RsaKit.java b/src/main/java/com/yxt/pay/utils/wx/RsaKit.java new file mode 100644 index 0000000..dd8f250 --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/RsaKit.java @@ -0,0 +1,387 @@ +package com.yxt.pay.utils.wx; + +import cn.hutool.core.codec.Base64; +import cn.hutool.core.util.StrUtil; + +import javax.crypto.Cipher; +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.*; +import java.util.HashMap; +import java.util.Map; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

RSA 非对称加密工具类

+ * + * @author Javen + */ +public class RsaKit { + + /** + * RSA最大加密明文大小 + */ + private static final int MAX_ENCRYPT_BLOCK = 117; + + /** + * RSA最大解密密文大小 + */ + private static final int MAX_DECRYPT_BLOCK = 128; + + /** + * 加密算法RSA + */ + private static final String KEY_ALGORITHM = "RSA"; + + /** + * 生成公钥和私钥 + * + * @throws Exception 异常信息 + */ + public static Map getKeys() throws Exception { + KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM); + keyPairGen.initialize(1024); + KeyPair keyPair = keyPairGen.generateKeyPair(); + RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); + RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); + + String publicKeyStr = getPublicKeyStr(publicKey); + String privateKeyStr = getPrivateKeyStr(privateKey); + + Map map = new HashMap(2); + map.put("publicKey", publicKeyStr); + map.put("privateKey", privateKeyStr); + + System.out.println("公钥\r\n" + publicKeyStr); + System.out.println("私钥\r\n" + privateKeyStr); + return map; + } + + /** + * 使用模和指数生成RSA公钥 + * 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA + * /None/NoPadding】 + * + * @param modulus 模 + * @param exponent 公钥指数 + * @return {@link RSAPublicKey} + */ + public static RSAPublicKey getPublicKey(String modulus, String exponent) { + try { + BigInteger b1 = new BigInteger(modulus); + BigInteger b2 = new BigInteger(exponent); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + RSAPublicKeySpec keySpec = new RSAPublicKeySpec(b1, b2); + return (RSAPublicKey) keyFactory.generatePublic(keySpec); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 使用模和指数生成RSA私钥 + * 注意:【此代码用了默认补位方式,为RSA/None/PKCS1Padding,不同JDK默认的补位方式可能不同,如Android默认是RSA + * /None/NoPadding】 + * + * @param modulus 模 + * @param exponent 指数 + * @return {@link RSAPrivateKey} + */ + public static RSAPrivateKey getPrivateKey(String modulus, String exponent) { + try { + BigInteger b1 = new BigInteger(modulus); + BigInteger b2 = new BigInteger(exponent); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + RSAPrivateKeySpec keySpec = new RSAPrivateKeySpec(b1, b2); + return (RSAPrivateKey) keyFactory.generatePrivate(keySpec); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + /** + * 公钥加密 + * + * @param data 需要加密的数据 + * @param publicKey 公钥 + * @return 加密后的数据 + * @throws Exception 异常信息 + */ + public static String encryptByPublicKey(String data, String publicKey) throws Exception { + return encryptByPublicKey(data, publicKey, "RSA/ECB/PKCS1Padding"); + } + + /** + * 公钥加密 + * + * @param data 需要加密的数据 + * @param publicKey 公钥 + * @return 加密后的数据 + * @throws Exception 异常信息 + */ + public static String encryptByPublicKeyByWx(String data, String publicKey) throws Exception { + return encryptByPublicKey(data, publicKey, "RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING"); + } + + /** + * 公钥加密 + * + * @param data 需要加密的数据 + * @param publicKey 公钥 + * @param fillMode 填充模式 + * @return 加密后的数据 + * @throws Exception 异常信息 + */ + public static String encryptByPublicKey(String data, String publicKey, String fillMode) throws Exception { + byte[] dataByte = data.getBytes(StandardCharsets.UTF_8); + byte[] keyBytes = Base64.decode(publicKey); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + Key key = keyFactory.generatePublic(x509KeySpec); + // 对数据加密 + Cipher cipher = Cipher.getInstance(fillMode); + cipher.init(Cipher.ENCRYPT_MODE, key); + int inputLen = dataByte.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段加密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_ENCRYPT_BLOCK) { + cache = cipher.doFinal(dataByte, offSet, MAX_ENCRYPT_BLOCK); + } else { + cache = cipher.doFinal(dataByte, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_ENCRYPT_BLOCK; + } + byte[] encryptedData = out.toByteArray(); + out.close(); + return StrUtil.str(Base64.encode(encryptedData)); + } + + /** + * 私钥签名 + * + * @param data 需要加密的数据 + * @param privateKey 私钥 + * @return 加密后的数据 + * @throws Exception 异常信息 + */ + public static String encryptByPrivateKey(String data, String privateKey) throws Exception { + PKCS8EncodedKeySpec priPkcs8 = new PKCS8EncodedKeySpec(Base64.decode(privateKey)); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + PrivateKey priKey = keyFactory.generatePrivate(priPkcs8); + Signature signature = Signature.getInstance("SHA256WithRSA"); + + signature.initSign(priKey); + signature.update(data.getBytes(StandardCharsets.UTF_8)); + byte[] signed = signature.sign(); + return StrUtil.str(Base64.encode(signed)); + } + + /** + * 私钥签名 + * + * @param data 需要加密的数据 + * @param privateKey 私钥 + * @return 加密后的数据 + * @throws Exception 异常信息 + */ + public static String encryptByPrivateKey(String data, PrivateKey privateKey) throws Exception { + Signature signature = Signature.getInstance("SHA256WithRSA"); + signature.initSign(privateKey); + signature.update(data.getBytes(StandardCharsets.UTF_8)); + byte[] signed = signature.sign(); + return StrUtil.str(Base64.encode(signed)); + } + + /** + * 公钥验证签名 + * + * @param data 需要加密的数据 + * @param sign 签名 + * @param publicKey 公钥 + * @return 验证结果 + * @throws Exception 异常信息 + */ + public static boolean checkByPublicKey(String data, String sign, String publicKey) throws Exception { + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + byte[] encodedKey = Base64.decode(publicKey); + PublicKey pubKey = keyFactory.generatePublic(new X509EncodedKeySpec(encodedKey)); + Signature signature = Signature.getInstance("SHA256WithRSA"); + signature.initVerify(pubKey); + signature.update(data.getBytes(StandardCharsets.UTF_8)); + return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8))); + } + + /** + * 公钥验证签名 + * + * @param data 需要加密的数据 + * @param sign 签名 + * @param publicKey 公钥 + * @return 验证结果 + * @throws Exception 异常信息 + */ + public static boolean checkByPublicKey(String data, String sign, PublicKey publicKey) throws Exception { + Signature signature = Signature.getInstance("SHA256WithRSA"); + signature.initVerify(publicKey); + signature.update(data.getBytes(StandardCharsets.UTF_8)); + return signature.verify(Base64.decode(sign.getBytes(StandardCharsets.UTF_8))); + } + + /** + * 私钥解密 + * + * @param data 需要解密的数据 + * @param privateKey 私钥 + * @return 解密后的数据 + * @throws Exception 异常信息 + */ + public static String decryptByPrivateKey(String data, String privateKey) throws Exception { + return decryptByPrivateKey(data, privateKey, "RSA/ECB/PKCS1Padding"); + } + + /** + * 私钥解密 + * + * @param data 需要解密的数据 + * @param privateKey 私钥 + * @return 解密后的数据 + * @throws Exception 异常信息 + */ + public static String decryptByPrivateKeyByWx(String data, String privateKey) throws Exception { + return decryptByPrivateKey(data, privateKey, "RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING"); + } + + /** + * 私钥解密 + * + * @param data 需要解密的数据 + * @param privateKey 私钥 + * @param fillMode 填充模式 + * @return 解密后的数据 + * @throws Exception 异常信息 + */ + public static String decryptByPrivateKey(String data, String privateKey, String fillMode) throws Exception { + byte[] encryptedData = Base64.decode(data); + byte[] keyBytes = Base64.decode(privateKey); + PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + Key key = keyFactory.generatePrivate(pkcs8KeySpec); + Cipher cipher = Cipher.getInstance(fillMode); + + cipher.init(Cipher.DECRYPT_MODE, key); + int inputLen = encryptedData.length; + ByteArrayOutputStream out = new ByteArrayOutputStream(); + int offSet = 0; + byte[] cache; + int i = 0; + // 对数据分段解密 + while (inputLen - offSet > 0) { + if (inputLen - offSet > MAX_DECRYPT_BLOCK) { + cache = cipher.doFinal(encryptedData, offSet, MAX_DECRYPT_BLOCK); + } else { + cache = cipher.doFinal(encryptedData, offSet, inputLen - offSet); + } + out.write(cache, 0, cache.length); + i++; + offSet = i * MAX_DECRYPT_BLOCK; + } + byte[] decryptedData = out.toByteArray(); + out.close(); + return new String(decryptedData); + } + + /** + * 从字符串中加载公钥 + * + * @param publicKeyStr 公钥数据字符串 + * @throws Exception 异常信息 + */ + public static PublicKey loadPublicKey(String publicKeyStr) throws Exception { + try { + byte[] buffer = Base64.decode(publicKeyStr); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + X509EncodedKeySpec keySpec = new X509EncodedKeySpec(buffer); + return keyFactory.generatePublic(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new Exception("无此算法"); + } catch (InvalidKeySpecException e) { + throw new Exception("公钥非法"); + } catch (NullPointerException e) { + throw new Exception("公钥数据为空"); + } + } + + /** + * 从字符串中加载私钥
+ * 加载时使用的是PKCS8EncodedKeySpec(PKCS#8编码的Key指令)。 + * + * @param privateKeyStr 私钥 + * @return {@link PrivateKey} + * @throws Exception 异常信息 + */ + public static PrivateKey loadPrivateKey(String privateKeyStr) throws Exception { + try { + byte[] buffer = Base64.decode(privateKeyStr); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer); + KeyFactory keyFactory = KeyFactory.getInstance(KEY_ALGORITHM); + return keyFactory.generatePrivate(keySpec); + } catch (NoSuchAlgorithmException e) { + throw new Exception("无此算法"); + } catch (InvalidKeySpecException e) { + throw new Exception("私钥非法"); + } catch (NullPointerException e) { + throw new Exception("私钥数据为空"); + } + } + + public static String getPrivateKeyStr(PrivateKey privateKey) { + return Base64.encode(privateKey.getEncoded()); + } + + public static String getPublicKeyStr(PublicKey publicKey) { + return Base64.encode(publicKey.getEncoded()); + } + + public static void main(String[] args) throws Exception { + Map keys = getKeys(); + String publicKey = keys.get("publicKey"); + String privateKey = keys.get("privateKey"); + String content = "我是Javen,I am Javen"; + String encrypt = encryptByPublicKey(content, publicKey); + String decrypt = decryptByPrivateKey(encrypt, privateKey); + System.out.println("加密之后:" + encrypt); + System.out.println("解密之后:" + decrypt); + + System.out.println("======华丽的分割线========="); + + content = "我是Javen,I am Javen"; + encrypt = encryptByPublicKeyByWx(content, publicKey); + decrypt = decryptByPrivateKeyByWx(encrypt, privateKey); + System.out.println("加密之后:" + encrypt); + System.out.println("解密之后:" + decrypt); + + //OPPO + String sign = encryptByPrivateKey(content, privateKey); + System.out.println("加密之后:" + sign); + System.out.println(checkByPublicKey(content, sign, publicKey)); + } +} \ No newline at end of file diff --git a/src/main/java/com/yxt/pay/utils/wx/SignType.java b/src/main/java/com/yxt/pay/utils/wx/SignType.java new file mode 100644 index 0000000..f1c4459 --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/SignType.java @@ -0,0 +1,44 @@ +package com.yxt.pay.utils.wx; + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

签名方式

+ * + * @author Javen + */ +public enum SignType { + /** + * HMAC-SHA256 加密 + */ + HMACSHA256("HMAC-SHA256"), + /** + * MD5 加密 + */ + MD5("MD5"), + /** + * RSA + */ + RSA("RSA"); + + SignType(String type) { + this.type = type; + } + + private final String type; + + public String getType() { + return type; + } + + @Override + public String toString() { + return type; + } +} diff --git a/src/main/java/com/yxt/pay/utils/wx/WxPayKit.java b/src/main/java/com/yxt/pay/utils/wx/WxPayKit.java new file mode 100644 index 0000000..26a7a14 --- /dev/null +++ b/src/main/java/com/yxt/pay/utils/wx/WxPayKit.java @@ -0,0 +1,695 @@ +package com.yxt.pay.utils.wx; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; +import cn.hutool.json.JSONUtil; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + + +/** + *

IJPay 让支付触手可及,封装了微信支付、支付宝支付、银联支付常用的支付方式以及各种常用的接口。

+ * + *

不依赖任何第三方 mvc 框架,仅仅作为工具使用简单快速完成支付模块的开发,可轻松嵌入到任何系统里。

+ * + *

IJPay 交流群: 723992875

+ * + *

Node.js 版: https://gitee.com/javen205/TNWX

+ * + *

微信支付工具类

+ * + * @author Javen + */ +public class WxPayKit { + private static final String FIELD_SIGN = "sign"; + private static final String FIELD_SIGN_TYPE = "sign_type"; + + public static String hmacSha256(String data, String key) { + return PayKit.hmacSha256(data, key); + } + + public static String md5(String data) { + return PayKit.md5(data); + } + + /** + * AES 解密 + * + * @param base64Data 需要解密的数据 + * @param key 密钥 + * @return 解密后的数据 + */ + public static String decryptData(String base64Data, String key) { + return PayKit.decryptData(base64Data, key); + } + + /** + * AES 加密 + * + * @param data 需要加密的数据 + * @param key 密钥 + * @return 加密后的数据 + */ + public static String encryptData(String data, String key) { + return PayKit.encryptData(data, key); + } + + public static String generateStr() { + return PayKit.generateStr(); + } + + + /** + * 支付异步通知时校验 sign + * + * @param params 参数 + * @param partnerKey 支付密钥 + * @return {boolean} + */ + public static boolean verifyNotify(Map params, String partnerKey) { + String sign = params.get("sign"); + String localSign = createSign(params, partnerKey, SignType.MD5); + return sign.equals(localSign); + } + + /** + * 支付异步通知时校验 sign + * + * @param params 参数 + * @param partnerKey 支付密钥 + * @param signType {@link SignType} + * @return {@link Boolean} 验证签名结果 + */ + public static boolean verifyNotify(Map params, String partnerKey, SignType signType) { + String sign = params.get("sign"); + String localSign = createSign(params, partnerKey, signType); + return sign.equals(localSign); + } + + /** + * 生成签名 + * + * @param params 需要签名的参数 + * @param partnerKey 密钥 + * @param signType 签名类型 + * @return 签名后的数据 + */ + public static String createSign(Map params, String partnerKey, SignType signType) { + if (signType == null) { + signType = SignType.MD5; + } + // 生成签名前先去除sign + params.remove(FIELD_SIGN); + String tempStr = PayKit.createLinkString(params); + String stringSignTemp = tempStr + "&key=" + partnerKey; + if (signType == SignType.MD5) { + return md5(stringSignTemp).toUpperCase(); + } else { + return hmacSha256(stringSignTemp, partnerKey).toUpperCase(); + } + } + + /** + * 生成签名 + * + * @param params 需要签名的参数 + * @param secret 企业微信支付应用secret + * @return 签名后的数据 + */ + public static String createSign(Map params, String secret) { + // 生成签名前先去除sign + params.remove(FIELD_SIGN); + String tempStr = PayKit.createLinkString(params); + String stringSignTemp = tempStr + "&secret=" + secret; + return md5(stringSignTemp).toUpperCase(); + } + + /** + * 构建签名 + * + * @param params 需要签名的参数 + * @param partnerKey 密钥 + * @param signType 签名类型 + * @return 签名后的 Map + */ + public static Map buildSign(Map params, String partnerKey, SignType signType) { + return buildSign(params, partnerKey, signType, true); + } + + /** + * 构建签名 + * + * @param params 需要签名的参数 + * @param partnerKey 密钥 + * @param signType 签名类型 + * @param haveSignType 签名是否包含 sign_type 字段 + * @return 签名后的 Map + */ + public static Map buildSign(Map params, String partnerKey, SignType signType, boolean haveSignType) { + if (haveSignType) { + params.put(FIELD_SIGN_TYPE, signType.getType()); + } + String sign = createSign(params, partnerKey, signType); + params.put(FIELD_SIGN, sign); + return params; + } + + public static StringBuffer forEachMap(Map params, String prefix, String suffix) { + return PayKit.forEachMap(params, prefix, suffix); + } + + /** + * 微信下单 map to xml + * + * @param params Map 参数 + * @return xml 字符串 + */ + public static String toXml(Map params) { + return PayKit.toXml(params); + } + + /** + * 针对支付的 xml,没有嵌套节点的简单处理 + * + * @param xmlStr xml 字符串 + * @return 转化后的 Map + */ + public static Map xmlToMap(String xmlStr) { + return PayKit.xmlToMap(xmlStr); + } + + /** + *

生成二维码链接

+ *

原生支付接口模式一(扫码模式一)

+ * + * @param sign 签名 + * @param appId 公众账号ID + * @param mchId 商户号 + * @param productId 商品ID + * @param timeStamp 时间戳 + * @param nonceStr 随机字符串 + * @return {String} + */ + public static String bizPayUrl(String sign, String appId, String mchId, String productId, String timeStamp, String nonceStr) { + String rules = "weixin://wxpay/bizpayurl?sign=Temp&appid=Temp&mch_id=Temp&product_id=Temp&time_stamp=Temp&nonce_str=Temp"; + return replace(rules, "Temp", sign, appId, mchId, productId, timeStamp, nonceStr); + } + + /** + *

生成二维码链接

+ *

原生支付接口模式一(扫码模式一)

+ * + * @param partnerKey 密钥 + * @param appId 公众账号ID + * @param mchId 商户号 + * @param productId 商品ID + * @param timeStamp 时间戳 + * @param nonceStr 随机字符串 + * @param signType 签名类型 + * @return {String} + */ + public static String bizPayUrl(String partnerKey, String appId, String mchId, String productId, String timeStamp, String nonceStr, SignType signType) { + HashMap map = new HashMap<>(5); + map.put("appid", appId); + map.put("mch_id", mchId); + map.put("time_stamp", StrUtil.isEmpty(timeStamp) ? Long.toString(System.currentTimeMillis() / 1000) : timeStamp); + map.put("nonce_str", StrUtil.isEmpty(nonceStr) ? WxPayKit.generateStr() : nonceStr); + map.put("product_id", productId); + return bizPayUrl(createSign(map, partnerKey, signType), appId, mchId, productId, timeStamp, nonceStr); + } + + /** + *

生成二维码链接

+ *

原生支付接口模式一(扫码模式一)

+ * + * @param partnerKey 密钥 + * @param appId 公众账号ID + * @param mchId 商户号 + * @param productId 商品ID + * @return {String} + */ + public static String bizPayUrl(String partnerKey, String appId, String mchId, String productId) { + String timeStamp = Long.toString(System.currentTimeMillis() / 1000); + String nonceStr = WxPayKit.generateStr(); + HashMap map = new HashMap<>(5); + map.put("appid", appId); + map.put("mch_id", mchId); + map.put("time_stamp", timeStamp); + map.put("nonce_str", nonceStr); + map.put("product_id", productId); + return bizPayUrl(createSign(map, partnerKey, null), appId, mchId, productId, timeStamp, nonceStr); + } + + + /** + * 替换url中的参数 + * + * @param str 原始字符串 + * @param regex 表达式 + * @param args 替换字符串 + * @return {String} + */ + public static String replace(String str, String regex, String... args) { + for (String arg : args) { + str = str.replaceFirst(regex, arg); + } + return str; + } + + /** + * 判断接口返回的 code + * + * @param codeValue code 值 + * @return 是否是 SUCCESS + */ + public static boolean codeIsOk(String codeValue) { + return StrUtil.isNotEmpty(codeValue) && "SUCCESS".equals(codeValue); + } + + /** + *

公众号支付-预付订单再次签名

+ *

注意此处签名方式需与统一下单的签名类型一致

+ * + * @param prepayId 预付订单号 + * @param appId 应用编号 + * @param partnerKey API Key + * @param signType 签名方式 + * @return 再次签名后的 Map + */ + public static Map prepayIdCreateSign(String prepayId, String appId, String partnerKey, SignType signType) { + Map packageParams = new HashMap<>(6); + packageParams.put("appId", appId); + packageParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); + packageParams.put("nonceStr", String.valueOf(System.currentTimeMillis())); + packageParams.put("package", "prepay_id=" + prepayId); + if (signType == null) { + signType = SignType.MD5; + } + packageParams.put("signType", signType.getType()); + String packageSign = WxPayKit.createSign(packageParams, partnerKey, signType); + packageParams.put("paySign", packageSign); + return packageParams; + } + + /** + * JS 调起支付签名 + * + * @param appId 应用编号 + * @param prepayId 预付订单号 + * @param keyPath key.pem 证书路径 + * @return 唤起支付需要的参数 + * @throws Exception 错误信息 + */ + public static Map jsApiCreateSign(String appId, String prepayId, String keyPath) throws Exception { + String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); + String nonceStr = String.valueOf(System.currentTimeMillis()); + String packageStr = "prepay_id=" + prepayId; + Map packageParams = new HashMap<>(6); + packageParams.put("appId", appId); + packageParams.put("timeStamp", timeStamp); + packageParams.put("nonceStr", nonceStr); + packageParams.put("package", packageStr); + packageParams.put("signType", SignType.RSA.toString()); + ArrayList list = new ArrayList<>(); + list.add(appId); + list.add(timeStamp); + list.add(nonceStr); + list.add(packageStr); + String packageSign = PayKit.createSign( + PayKit.buildSignMessage(list), + keyPath + ); + packageParams.put("paySign", packageSign); + return packageParams; + } + + /** + *

APP 支付-预付订单再次签名

+ *

注意此处签名方式需与统一下单的签名类型一致

+ * + * @param appId 应用编号 + * @param partnerId 商户号 + * @param prepayId 预付订单号 + * @param partnerKey API Key + * @param signType 签名方式 + * @return 再次签名后的 Map + */ + public static Map appPrepayIdCreateSign(String appId, String partnerId, String prepayId, String partnerKey, SignType signType) { + Map packageParams = new HashMap<>(8); + packageParams.put("appid", appId); + packageParams.put("partnerid", partnerId); + packageParams.put("prepayid", prepayId); + packageParams.put("package", "Sign=WXPay"); + packageParams.put("noncestr", String.valueOf(System.currentTimeMillis())); + packageParams.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); + if (signType == null) { + signType = SignType.MD5; + } + String packageSign = createSign(packageParams, partnerKey, signType); + packageParams.put("sign", packageSign); + return packageParams; + } + + /** + * App 调起支付签名 + * + * @param appId 应用编号 + * @param partnerId 商户编号 + * @param prepayId 预付订单号 + * @param keyPath key.pem 证书路径 + * @return 唤起支付需要的参数 + * @throws Exception 错误信息 + */ + public static Map appCreateSign(String appId, String partnerId, String prepayId, String keyPath) throws Exception { + String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); + String nonceStr = String.valueOf(System.currentTimeMillis()); + Map packageParams = new HashMap<>(8); + packageParams.put("appid", appId); + packageParams.put("partnerid", partnerId); + packageParams.put("prepayid", prepayId); + packageParams.put("package", "Sign=WXPay"); + packageParams.put("timestamp", timeStamp); + packageParams.put("noncestr", nonceStr); + packageParams.put("signType", SignType.RSA.toString()); + ArrayList list = new ArrayList<>(); + list.add(appId); + list.add(timeStamp); + list.add(nonceStr); + list.add(prepayId); + String packageSign = PayKit.createSign( + PayKit.buildSignMessage(list), + keyPath + ); + packageParams.put("sign", packageSign); + return packageParams; + } + + /** + *

小程序-预付订单再次签名

+ *

注意此处签名方式需与统一下单的签名类型一致

+ * + * @param appId 应用编号 + * @param prepayId 预付订单号 + * @param partnerKey API Key + * @param signType 签名方式 + * @return 再次签名后的 Map + */ + public static Map miniAppPrepayIdCreateSign(String appId, String prepayId, String partnerKey, SignType signType) { + Map packageParams = new HashMap<>(6); + packageParams.put("appId", appId); + packageParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); + packageParams.put("nonceStr", String.valueOf(System.currentTimeMillis())); + packageParams.put("package", "prepay_id=" + prepayId); + if (signType == null) { + signType = SignType.MD5; + } + packageParams.put("signType", signType.getType()); + String packageSign = createSign(packageParams, partnerKey, signType); + packageParams.put("paySign", packageSign); + return packageParams; + } + + /** + * 构建 v3 接口所需的 Authorization + * + * @param method {@link RequestMethod} 请求方法 + * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 + * @param mchId 商户Id + * @param serialNo 商户 API 证书序列号 + * @param keyPath key.pem 证书路径 + * @param body 接口请求参数 + * @param nonceStr 随机字符库 + * @param timestamp 时间戳 + * @param authType 认证类型 + * @return {@link String} 返回 v3 所需的 Authorization + * @throws Exception 异常信息 + */ + public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, + String serialNo, String keyPath, String body, String nonceStr, + long timestamp, String authType) throws Exception { + // 构建签名参数 + String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); + String signature = PayKit.createSign(buildSignMessage, keyPath); + // 根据平台规则生成请求头 authorization + return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); + } + + /** + * 构建 v3 接口所需的 Authorization + * + * @param method {@link RequestMethod} 请求方法 + * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 + * @param mchId 商户Id + * @param serialNo 商户 API 证书序列号 + * @param privateKey 商户私钥 + * @param body 接口请求参数 + * @param nonceStr 随机字符库 + * @param timestamp 时间戳 + * @param authType 认证类型 + * @return {@link String} 返回 v3 所需的 Authorization + * @throws Exception 异常信息 + */ + public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, + String serialNo, PrivateKey privateKey, String body, String nonceStr, + long timestamp, String authType) throws Exception { + // 构建签名参数 + String buildSignMessage = PayKit.buildSignMessage(method, urlSuffix, timestamp, nonceStr, body); + String signature = PayKit.createSign(buildSignMessage, privateKey); + // 根据平台规则生成请求头 authorization + return PayKit.getAuthorization(mchId, serialNo, nonceStr, String.valueOf(timestamp), signature, authType); + } + + /** + * 构建 v3 接口所需的 Authorization + * + * @param method {@link RequestMethod} 请求方法 + * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 + * @param mchId 商户Id + * @param serialNo 商户 API 证书序列号 + * @param keyPath key.pem 证书路径 + * @param body 接口请求参数 + * @return {@link String} 返回 v3 所需的 Authorization + * @throws Exception 异常信息 + */ + public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, + String serialNo, String keyPath, String body) throws Exception { + + long timestamp = System.currentTimeMillis() / 1000; + String authType = "WECHATPAY2-SHA256-RSA2048"; + String nonceStr = PayKit.generateStr(); + + return buildAuthorization(method, urlSuffix, mchId, serialNo, keyPath, body, nonceStr, timestamp, authType); + } + + /** + * 构建 v3 接口所需的 Authorization + * + * @param method {@link RequestMethod} 请求方法 + * @param urlSuffix 可通过 WxApiType 来获取,URL挂载参数需要自行拼接 + * @param mchId 商户Id + * @param serialNo 商户 API 证书序列号 + * @param privateKey key.pem 证书路径 + * @param body 接口请求参数 + * @return {@link String} 返回 v3 所需的 Authorization + * @throws Exception 异常信息 + */ + public static String buildAuthorization(RequestMethod method, String urlSuffix, String mchId, + String serialNo, PrivateKey privateKey, String body) throws Exception { + + long timestamp = System.currentTimeMillis() / 1000; + String authType = "WECHATPAY2-SHA256-RSA2048"; + String nonceStr = PayKit.generateStr(); + + return buildAuthorization(method, urlSuffix, mchId, serialNo, privateKey, body, nonceStr, timestamp, authType); + } + + /** + * 验证签名 + * + * @param map 接口请求返回的 Map + * @param certPath 平台证书路径 + * @return 签名结果 + * @throws Exception 异常信息 + */ + @Deprecated + public static boolean verifySignature(Map map, String certPath) throws Exception { + String signature = (String) map.get("signature"); + String body = (String) map.get("body"); + String nonceStr = (String) map.get("nonceStr"); + String timestamp = (String) map.get("timestamp"); + return verifySignature(signature, body, nonceStr, timestamp, FileUtil.getInputStream(certPath)); + } + + /** + * 验证签名 + * + * @param response 接口请求返回的 {@link IJPayHttpResponse} + * @param certPath 平台证书路径 + * @return 签名结果 + * @throws Exception 异常信息 + */ + public static boolean verifySignature(IJPayHttpResponse response, String certPath) throws Exception { + String timestamp = response.getHeader("Wechatpay-Timestamp"); + String nonceStr = response.getHeader("Wechatpay-Nonce"); + String signature = response.getHeader("Wechatpay-Signature"); + String body = response.getBody(); + return verifySignature(signature, body, nonceStr, timestamp, FileUtil.getInputStream(certPath)); + } + + /** + * 验证签名 + * + * @param response 接口请求返回的 {@link IJPayHttpResponse} + * @param certInputStream 平台证书 + * @return 签名结果 + * @throws Exception 异常信息 + */ + public static boolean verifySignature(IJPayHttpResponse response, InputStream certInputStream) throws Exception { + String timestamp = response.getHeader("Wechatpay-Timestamp"); + String nonceStr = response.getHeader("Wechatpay-Nonce"); + String signature = response.getHeader("Wechatpay-Signature"); + String body = response.getBody(); + return verifySignature(signature, body, nonceStr, timestamp, certInputStream); + } + + /** + * 验证签名 + * + * @param map 接口请求返回的 Map + * @param certInputStream 平台证书输入流 + * @return 签名结果 + * @throws Exception 异常信息 + */ + @Deprecated + public static boolean verifySignature(Map map, InputStream certInputStream) throws Exception { + String signature = (String) map.get("signature"); + String body = (String) map.get("body"); + String nonceStr = (String) map.get("nonceStr"); + String timestamp = (String) map.get("timestamp"); + return verifySignature(signature, body, nonceStr, timestamp, certInputStream); + } + + /** + * 验证签名 + * + * @param signature 待验证的签名 + * @param body 应答主体 + * @param nonce 随机串 + * @param timestamp 时间戳 + * @param publicKey 微信支付平台公钥 + * @return 签名结果 + * @throws Exception 异常信息 + */ + public static boolean verifySignature(String signature, String body, String nonce, String timestamp, String publicKey) throws Exception { + String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); + return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); + } + + /** + * 验证签名 + * + * @param signature 待验证的签名 + * @param body 应答主体 + * @param nonce 随机串 + * @param timestamp 时间戳 + * @param publicKey {@link PublicKey} 微信支付平台公钥 + * @return 签名结果 + * @throws Exception 异常信息 + */ + public static boolean verifySignature(String signature, String body, String nonce, String timestamp, PublicKey publicKey) throws Exception { + String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); + return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); + } + + /** + * 验证签名 + * + * @param signature 待验证的签名 + * @param body 应答主体 + * @param nonce 随机串 + * @param timestamp 时间戳 + * @param certInputStream 微信支付平台证书输入流 + * @return 签名结果 + * @throws Exception 异常信息 + */ + public static boolean verifySignature(String signature, String body, String nonce, String timestamp, InputStream certInputStream) throws Exception { + String buildSignMessage = PayKit.buildSignMessage(timestamp, nonce, body); + // 获取证书 + X509Certificate certificate = PayKit.getCertificate(certInputStream); + PublicKey publicKey = certificate.getPublicKey(); + return RsaKit.checkByPublicKey(buildSignMessage, signature, publicKey); + } + + /** + * v3 支付异步通知验证签名 + * + * @param serialNo 证书序列号 + * @param body 异步通知密文 + * @param signature 签名 + * @param nonce 随机字符串 + * @param timestamp 时间戳 + * @param key api 密钥 + * @param certInputStream 平台证书 + * @return 异步通知明文 + * @throws Exception 异常信息 + */ + public static String verifyNotify(String serialNo, String body, String signature, String nonce, + String timestamp, String key, InputStream certInputStream) throws Exception { + // 获取平台证书序列号 + X509Certificate certificate = PayKit.getCertificate(certInputStream); + String serialNumber = certificate.getSerialNumber().toString(16).toUpperCase(); + System.out.println(serialNumber); + // 验证证书序列号 + if (serialNumber.equals(serialNo)) { + boolean verifySignature = WxPayKit.verifySignature(signature, body, nonce, timestamp, + certificate.getPublicKey()); + if (verifySignature) { + JSONObject resultObject = JSONUtil.parseObj(body); + JSONObject resource = resultObject.getJSONObject("resource"); + String cipherText = resource.getStr("ciphertext"); + String nonceStr = resource.getStr("nonce"); + String associatedData = resource.getStr("associated_data"); + + AesUtil aesUtil = new AesUtil(key.getBytes(StandardCharsets.UTF_8)); + // 密文解密 + return aesUtil.decryptToString( + associatedData.getBytes(StandardCharsets.UTF_8), + nonceStr.getBytes(StandardCharsets.UTF_8), + cipherText + ); + } else { + throw new Exception("签名错误"); + } + } else { + throw new Exception("证书序列号错误"); + } + } + + /** + * v3 支付异步通知验证签名 + * + * @param serialNo 证书序列号 + * @param body 异步通知密文 + * @param signature 签名 + * @param nonce 随机字符串 + * @param timestamp 时间戳 + * @param key api 密钥 + * @param certPath 平台证书路径 + * @return 异步通知明文 + * @throws Exception 异常信息 + */ + public static String verifyNotify(String serialNo, String body, String signature, String nonce, + String timestamp, String key, String certPath) throws Exception { + BufferedInputStream inputStream = FileUtil.getInputStream(certPath); + return verifyNotify(serialNo, body, signature, nonce, timestamp, key, inputStream); + } +}