From 8709a9c5a75597022741552faa541abc68f8bbb8 Mon Sep 17 00:00:00 2001 From: spvycf <545997765@qq.com> Date: Tue, 19 May 2020 16:25:18 +0800 Subject: [PATCH] =?UTF-8?q?:new:=20#1090=20=E5=A2=9E=E5=8A=A0=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E6=94=AF=E4=BB=98=E5=88=86=E5=92=8C=E5=85=8D=E6=8A=BC?= =?UTF-8?q?=E7=A7=9F=E5=80=9F=E7=9B=B8=E5=85=B3=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pay/config/WxPayAutoConfiguration.java | 7 + .../pay/properties/WxPayProperties.java | 31 +++ weixin-java-pay/pom.xml | 25 +++ .../wxpay/bean/payscore/NotifyData.java | 45 ++++ .../bean/payscore/WxPayScoreRequest.java | 130 ++++++++++++ .../wxpay/bean/payscore/WxPayScoreResult.java | 163 +++++++++++++++ .../binarywang/wxpay/config/WxPayConfig.java | 119 ++++++++++- .../wxpay/service/PayScoreService.java | 164 +++++++++++++++ .../wxpay/service/WxPayService.java | 31 +++ .../service/impl/BaseWxPayServiceImpl.java | 11 +- .../service/impl/PayScoreServiceImpl.java | 193 ++++++++++++++++++ .../impl/WxPayServiceApacheHttpImpl.java | 88 +++++++- .../impl/WxPayServiceJoddHttpImpl.java | 11 + .../binarywang/wxpay/v3/Credentials.java | 11 + .../binarywang/wxpay/v3/SignatureExec.java | 88 ++++++++ .../github/binarywang/wxpay/v3/Validator.java | 8 + .../wxpay/v3/WechatPayHttpClientBuilder.java | 75 +++++++ .../auth/AutoUpdateCertificatesVerifier.java | 161 +++++++++++++++ .../wxpay/v3/auth/CertificatesVerifier.java | 43 ++++ .../wxpay/v3/auth/PrivateKeySigner.java | 37 ++++ .../binarywang/wxpay/v3/auth/Signer.java | 15 ++ .../binarywang/wxpay/v3/auth/Verifier.java | 5 + .../wxpay/v3/auth/WechatPay2Credentials.java | 91 +++++++++ .../wxpay/v3/auth/WechatPay2Validator.java | 55 +++++ .../binarywang/wxpay/v3/util/AesUtils.java | 107 ++++++++++ .../binarywang/wxpay/v3/util/PemUtils.java | 60 ++++++ 26 files changed, 1762 insertions(+), 12 deletions(-) create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/NotifyData.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreRequest.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreResult.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java create mode 100644 weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java index 43b2114e6..ed5da834a 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java @@ -49,6 +49,13 @@ public class WxPayAutoConfiguration { payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId())); payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId())); payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath())); + //以下是apiv3以及支付分相关 + payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId())); + payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl())); + payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath())); + payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath())); + payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo())); + payConfig.setApiv3Key(StringUtils.trimToNull(this.properties.getApiv3Key())); wxPayService.setConfig(payConfig); return wxPayService; diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java index fe8a21565..940cdf591 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java @@ -43,4 +43,35 @@ public class WxPayProperties { * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定. */ private String keyPath; + + /** + * 微信支付分serviceId + */ + private String serviceId; + + /** + * 证书序列号 + */ + private String certSerialNo; + + /** + * apiV3秘钥 + */ + private String apiv3Key; + + /** + * 微信支付分回调地址 + */ + private String payScoreNotifyUrl; + + /** + * apiv3 商户apiclient_key.pem + */ + private String privateKeyPath; + + /** + * apiv3 商户apiclient_cert.pem + */ + private String privateCertPath; + } diff --git a/weixin-java-pay/pom.xml b/weixin-java-pay/pom.xml index 946aabe78..68e7a0576 100644 --- a/weixin-java-pay/pom.xml +++ b/weixin-java-pay/pom.xml @@ -12,6 +12,18 @@ weixin-java-pay WxJava - PAY Java SDK 微信支付 Java SDK + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + @@ -70,6 +82,19 @@ org.projectlombok lombok + + + com.fasterxml.jackson.core + jackson-databind + 2.9.7 + + + com.alibaba + fastjson + 1.2.58 + + + diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/NotifyData.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/NotifyData.java new file mode 100644 index 000000000..b46de1f11 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/NotifyData.java @@ -0,0 +1,45 @@ +package com.github.binarywang.wxpay.bean.payscore; + +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 微信支付分确认订单跟支付回调对象 + * @author doger.wang + * @date 2020/5/14 12:18 + */ +@NoArgsConstructor +@Data +public class NotifyData { + + + /** + * id : EV-2018022511223320873 + * create_time : 20180225112233 + * resource_type : encrypt-resource + * event_type : PAYSCORE.USER_CONFIRM + * resource : {"algorithm":"AEAD_AES_256_GCM","ciphertext":"...","nonce":"...","associated_data":""} + */ + + private String id; + private String create_time; + private String resource_type; + private String event_type; + private Resource resource; + + @NoArgsConstructor + @Data + public static class Resource { + /** + * algorithm : AEAD_AES_256_GCM + * ciphertext : ... + * nonce : ... + * associated_data : + */ + + private String algorithm; + private String ciphertext; + private String nonce; + private String associated_data; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreRequest.java new file mode 100644 index 000000000..7166a31c2 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreRequest.java @@ -0,0 +1,130 @@ +package com.github.binarywang.wxpay.bean.payscore; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; + +/** + * @author doger.wang + * @date 2020/5/12 16:36 + */ +@NoArgsConstructor +@Data +public class WxPayScoreRequest implements Serializable { + + + private static final long serialVersionUID = 364764508076146082L; + /** + * out_order_no : 1234323JKHDFE1243252 + * appid : wxd678efh567hg6787 + * service_id : 500001 + * service_introduction : 某某酒店 + * post_payments : [{"name":"就餐费用服务费","amount":4000,"description":"就餐人均100元服务费:100/小时","count":1}] + * post_discounts : [{"name":"满20减1元","description":"不与其他优惠叠加"}] + * time_range : {"start_time":"20091225091010","end_time":"20091225121010"} + * location : {"start_location":"嗨客时尚主题展餐厅","end_location":"嗨客时尚主题展餐厅"} + * risk_fund : {"name":"ESTIMATE_ORDER_COST","amount":10000,"description":"就餐的预估费用"} + * attach : Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald + * notify_url : https://api.test.com + * openid : oUpF8uMuAJO_M2pxb1Q9zNjWeS6o + * need_user_confirm : true + */ + + private String out_order_no; + private String appid; + private String service_id; + private String service_introduction; + private TimeRange time_range; + private Location location; + private RiskFund risk_fund; + private String attach; + private String notify_url; + private String openid; + private boolean need_user_confirm; + private boolean profit_sharing; + private List post_payments; + private List post_discounts; + private int total_amount; + private String reason; + private String goods_tag; + private String type; + private Detail detail; + + @NoArgsConstructor + @Data + public static class Detail { + private String paid_time; + } + + + + @NoArgsConstructor + @Data + public static class TimeRange { + /** + * start_time : 20091225091010 + * end_time : 20091225121010 + */ + + private String start_time; + private String end_time; + } + + @NoArgsConstructor + @Data + public static class Location { + /** + * start_location : 嗨客时尚主题展餐厅 + * end_location : 嗨客时尚主题展餐厅 + */ + + private String start_location; + private String end_location; + } + + @NoArgsConstructor + @Data + public static class RiskFund { + /** + * name : ESTIMATE_ORDER_COST + * amount : 10000 + * description : 就餐的预估费用 + */ + + private String name; + private int amount; + private String description; + } + + @NoArgsConstructor + @Data + public static class PostPayments { + /** + * name : 就餐费用服务费 + * amount : 4000 + * description : 就餐人均100元服务费:100/小时 + * count : 1 + */ + + private String name; + private int amount; + private String description; + private int count; + } + + @NoArgsConstructor + @Data + public static class PostDiscounts { + /** + * name : 满20减1元 + * description : 不与其他优惠叠加 + */ + + private String name; + private String description; + private int count; + private int amount; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreResult.java new file mode 100644 index 000000000..6f72039bc --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreResult.java @@ -0,0 +1,163 @@ +package com.github.binarywang.wxpay.bean.payscore; + +import com.alibaba.fastjson.annotation.JSONField; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * @author doger.wang + * @date 2020/5/12 17:05 + */ +@NoArgsConstructor +@Data +public class WxPayScoreResult implements Serializable { + + + private static final long serialVersionUID = 8809250065540275770L; + /** + * appid : wxd678efh567hg6787 + * mchid : 1230000109 + * out_order_no : 1234323JKHDFE1243252 + * service_id : 500001 + * service_introduction : 某某酒店 + * state : CREATED + * state_description : MCH_COMPLETE + * post_payments : [{"name":"就餐费用服务费","amount":4000,"description":"就餐人均100元服务费:100/小时","count":1}] + * post_discounts : [{"name":"满20减1元","description":"不与其他优惠叠加"}] + * risk_fund : {"name":" ESTIMATE_ORDER_COST","amount":10000,"description":"就餐的预估费用"} + * time_range : {"start_time":"20091225091010","end_time":"20091225121010"} + * location : {"start_location":"嗨客时尚主题展餐厅","end_location":"嗨客时尚主题展餐厅"} + * attach : Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald + * notify_url : https://api.test.com + * order_id : 15646546545165651651 + * package : DJIOSQPYWDxsjdldeuwhdodwxasd_dDiodnwjh9we + */ + + private String appid; + private String mchid; + private String out_order_no; + private String service_id; + private String service_introduction; + private String state; + private String state_description; + private RiskFund risk_fund; + private TimeRange time_range; + private Location location; + private String attach; + private String notify_url; + private String order_id; + @JSONField(name = "package") + private String packageX; + private List post_payments; + private List post_discounts; + private boolean need_collection; + private Collection collection; + //用于跳转的sign注意区分需确认模式和无需确认模式的数据差别。创单接口会返回,查询请自行组装 + private Map payScoreSignInfo; + + @NoArgsConstructor + @Data + public static class RiskFund { + /** + * name : ESTIMATE_ORDER_COST + * amount : 10000 + * description : 就餐的预估费用 + */ + + private String name; + private int amount; + private String description; + } + + @NoArgsConstructor + @Data + public static class TimeRange { + /** + * start_time : 20091225091010 + * end_time : 20091225121010 + */ + + private String start_time; + private String end_time; + } + + @NoArgsConstructor + @Data + public static class Location { + /** + * start_location : 嗨客时尚主题展餐厅 + * end_location : 嗨客时尚主题展餐厅 + */ + + private String start_location; + private String end_location; + } + + @NoArgsConstructor + @Data + public static class PostPayments { + /** + * name : 就餐费用服务费 + * amount : 4000 + * description : 就餐人均100元服务费:100/小时 + * count : 1 + */ + + private String name; + private int amount; + private String description; + private int count; + } + + @NoArgsConstructor + @Data + public static class PostDiscounts { + /** + * name : 满20减1元 + * description : 不与其他优惠叠加 + */ + + private String name; + private String description; + } + + @NoArgsConstructor + @Data + public static class Collection { + /** + * state : USER_PAID + * total_amount : 3900 + * paying_amount : 3000 + * paid_amount : 900 + * details : [{"seq":1,"amount":900,"paid_type":"NEWTON","paid_time":"20091225091210","transaction_id":"15646546545165651651"}] + */ + + private String state; + private int total_amount; + private int paying_amount; + private int paid_amount; + private List
details; + + @NoArgsConstructor + @Data + public static class Details { + /** + * seq : 1 + * amount : 900 + * paid_type : NEWTON + * paid_time : 20091225091210 + * transaction_id : 15646546545165651651 + */ + + private int seq; + private int amount; + private String paid_type; + private String paid_time; + private String transaction_id; + } + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java index c5b6ecf69..4371afe10 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java @@ -1,15 +1,25 @@ package com.github.binarywang.wxpay.config; import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.v3.WechatPayHttpClientBuilder; +import com.github.binarywang.wxpay.v3.auth.AutoUpdateCertificatesVerifier; +import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner; +import com.github.binarywang.wxpay.v3.auth.WechatPay2Credentials; +import com.github.binarywang.wxpay.v3.auth.WechatPay2Validator; +import com.github.binarywang.wxpay.v3.util.PemUtils; import lombok.Data; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.ssl.SSLContexts; import javax.net.ssl.SSLContext; import java.io.*; import java.net.URL; import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.ArrayList; /** * 微信支付配置 @@ -85,6 +95,39 @@ public class WxPayConfig { */ private String keyPath; + /** + * apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径. + */ + private String privateKeyPath; + /** + * apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径. + */ + private String privateCertPath; + + /** + * apiV3 秘钥值. + */ + private String apiv3Key; + + /** + * apiV3 证书序列号值 + */ + private String certSerialNo; + + + /** + * 微信支付分serviceId + */ + private String serviceId; + + /** + * 微信支付分回调地址 + */ + private String payScoreNotifyUrl; + + private CloseableHttpClient apiv3HttpClient; + + /** * p12证书文件内容的字节数组. */ @@ -185,4 +228,78 @@ public class WxPayConfig { } } -} + + /** + * @Author doger.wang + * @Description 初始化api v3请求头 自动签名验签 + * 方法参照微信官方https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient + * @Date 2020/5/14 10:10 + * @Param [] + * @return org.apache.http.impl.client.CloseableHttpClient + **/ + public CloseableHttpClient initApiV3HttpClient()throws WxPayException { + String privateKeyPath = this.getPrivateKeyPath(); + String privateCertPath = this.getPrivateCertPath(); + String certSerialNo = this.getCertSerialNo(); + String apiv3Key = this.getApiv3Key(); + if (StringUtils.isBlank(privateKeyPath)) { + throw new WxPayException("请确保privateKeyPath已设置"); + } + if (StringUtils.isBlank(privateCertPath)) { + throw new WxPayException("请确保privateCertPath已设置"); + } + if (StringUtils.isBlank(certSerialNo)) { + throw new WxPayException("请确保certSerialNo证书序列号已设置"); + } + if (StringUtils.isBlank(apiv3Key)) { + throw new WxPayException("请确保apiv3Key值已设置"); + } + + + InputStream keyinputStream=null; + InputStream certinputStream=null; + final String prefix = "classpath:"; + if (privateKeyPath.startsWith(prefix)) { + String keypath = StringUtils.removeFirst(privateKeyPath, prefix); + if (!keypath.startsWith("/")) { + keypath = "/" + keypath; + } + keyinputStream = WxPayConfig.class.getResourceAsStream(keypath); + if (keyinputStream == null) { + throw new WxPayException("证书文件【" + this.getPrivateKeyPath() + "】不存在,请核实!"); + } + } + + if (privateCertPath.startsWith(prefix)) { + String certpath = StringUtils.removeFirst(privateCertPath, prefix); + if (!certpath.startsWith("/")) { + certpath = "/" + certpath; + } + certinputStream = WxPayConfig.class.getResourceAsStream(certpath); + if (certinputStream == null) { + throw new WxPayException("证书文件【" + this.getPrivateCertPath() + "】不存在,请核实!"); + } + } + CloseableHttpClient httpClient = null; + try { + WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create(); + PrivateKey merchantPrivateKey = PemUtils.loadPrivateKey(keyinputStream); + X509Certificate x509Certificate = PemUtils.loadCertificate(certinputStream); + ArrayList certificates = new ArrayList<>(); + certificates.add(x509Certificate); + builder.withMerchant(mchId, certSerialNo, merchantPrivateKey); + builder.withWechatpay(certificates); + AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier( + new WechatPay2Credentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)), + apiv3Key.getBytes("utf-8")); + builder.withValidator(new WechatPay2Validator(verifier)); + httpClient = builder.build(); + this.apiv3HttpClient =httpClient; + } catch (Exception e) { + throw new WxPayException("v3请求构造异常", e); + } + return httpClient; + + + } + } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java new file mode 100644 index 000000000..e85df7f88 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java @@ -0,0 +1,164 @@ +package com.github.binarywang.wxpay.service; + +import com.github.binarywang.wxpay.bean.payscore.NotifyData; +import com.github.binarywang.wxpay.bean.payscore.WxPayScoreRequest; +import com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult; +import com.github.binarywang.wxpay.exception.WxPayException; + +import java.net.URISyntaxException; + +/** + *
+ *  支付分相关服务类.
+ *   微信支付分是对个人的身份特质、支付行为、使用历史等情况的综合计算分值,旨在为用户提供更简单便捷的生活方式。
+ *   微信用户可以在具体应用场景中,开通微信支付分。开通后,用户可以在【微信—>钱包—>支付分】中查看分数和使用记录。(即需在应用场景中使用过一次,钱包才会出现支付分入口)
+ *
+ *  Created by doger.wang on 2020/05/12.
+ * 
+ * + * + */ +public interface PayScoreService { + + + + /** + *
+   * 支付分创建订单API.
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter1_1.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_1.shtml
+   * 
+ * + * @param request 请求对象 + * @return WxPayScoreResult + * @throws WxPayException the wx pay exception + */ + WxPayScoreResult createServiceOrder(WxPayScoreRequest request) throws WxPayException; + + + + /** + *
+   * 支付分查询订单API.
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_2.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_2.shtml
+   * 
+ * + * @Author doger.wang + * @Description + * @Date 2020/5/14 15:40 + * @Param out_order_no, query_id选填一个 + * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult + **/ + WxPayScoreResult queryServiceOrder( String out_order_no,String query_id ) throws WxPayException, URISyntaxException; + + + /** + *
+   * 支付分取消订单API.
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_3.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_3.shtml
+   * 
+ * + * @Author doger.wang + * @Description + * @Date 2020/5/14 15:40 + * @Param out_order_no reason + * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult + **/ + WxPayScoreResult cancelServiceOrder(String out_order_no, String reason) throws WxPayException; + + /** + *
+   * 支付分修改订单金额API.
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_4.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_4.shtml
+   * 
+ * + * @Author doger.wang + * @Description + * @Date 2020/5/14 15:40 + * @Param WxPayScoreRequest + * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult + **/ + WxPayScoreResult modifyServiceOrder(WxPayScoreRequest request) throws WxPayException; + + + /** + *
+   * 支付分完结订单API.
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_5.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_5.shtml
+   * 
+ * + * @Author doger.wang + * @Description + * @Date 2020/5/14 15:40 + * @Param WxPayScoreRequest + * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult + **/ + WxPayScoreResult completeServiceOrder(WxPayScoreRequest request) throws WxPayException; + + + /** + *
+   * 支付分订单收款API.
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_6.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_6.shtml
+   * 
+ * + * @Author doger.wang + * @Description + * @Date 2020/5/14 15:40 + * @Param out_order_no + * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult + **/ + WxPayScoreResult payServiceOrder(String out_order_no) throws WxPayException; + + + /** + *
+   * 支付分订单收款API.
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_7.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_7.shtml
+   * 
+ * + * @Author doger.wang + * @Description + * @Date 2020/5/14 15:40 + * @Param WxPayScoreRequest + * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult + **/ + WxPayScoreResult syncServiceOrder(WxPayScoreRequest request) throws WxPayException; + + + /** + *
+   * 支付分回调内容解密方法
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter5_2.shtml
+   * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter5_2.shtml
+   * 
+ * + * @param NotifyData 请求对象 + * @return WxPayScoreResult + */ + WxPayScoreResult decryptNotifyData(NotifyData data) throws WxPayException; + + + + + + + + + + + + + + + + + + +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java index 109ce34b9..ba9de0ffd 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java @@ -12,6 +12,7 @@ import com.github.binarywang.wxpay.constant.WxPayConstants; import com.github.binarywang.wxpay.exception.WxPayException; import java.io.File; +import java.net.URI; import java.util.Date; import java.util.Map; @@ -54,6 +55,27 @@ public interface WxPayService { */ String post(String url, String requestStr, boolean useKey) throws WxPayException; + /** + * 发送post请求,得到响应字符串. + * + * @param url 请求地址 + * @param requestStr 请求信息 + * @return 返回请求结果字符串 string + * @throws WxPayException the wx pay exception + */ + String postV3(String url, String requestStr) throws WxPayException; + + + /** + * 发送get V3请求,得到响应字符串. + * + * @param url 请求地址 + * @param param 请求信息 + * @return 返回请求结果字符串 string + * @throws WxPayException the wx pay exception + */ + String getV3(URI url) throws WxPayException; + /** * 获取企业付款服务类. * @@ -75,6 +97,14 @@ public interface WxPayService { */ ProfitSharingService getProfitSharingService(); + + /** + * 获取支付分服务类. + * + * @return the ent pay service + */ + PayScoreService getPayScoreService(); + /** * 设置企业付款服务类,允许开发者自定义实现类. * @@ -729,4 +759,5 @@ public interface WxPayService { */ WxPayFacepayResult facepay(WxPayFacepayRequest request) throws WxPayException; + } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java index 1f0e52838..6d6dc748c 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java @@ -17,10 +17,7 @@ import com.github.binarywang.wxpay.constant.WxPayConstants; import com.github.binarywang.wxpay.constant.WxPayConstants.SignType; import com.github.binarywang.wxpay.constant.WxPayConstants.TradeType; import com.github.binarywang.wxpay.exception.WxPayException; -import com.github.binarywang.wxpay.service.EntPayService; -import com.github.binarywang.wxpay.service.ProfitSharingService; -import com.github.binarywang.wxpay.service.RedpackService; -import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.*; import com.github.binarywang.wxpay.util.SignUtils; import com.github.binarywang.wxpay.util.XmlConfig; import com.google.common.base.Joiner; @@ -64,6 +61,7 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { private EntPayService entPayService = new EntPayServiceImpl(this); private ProfitSharingService profitSharingService = new ProfitSharingServiceImpl(this); private RedpackService redpackService = new RedpackServiceImpl(this); + private PayScoreService payScoreService = new PayScoreServiceImpl(this); /** * The Config. @@ -80,6 +78,11 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { return profitSharingService; } + @Override + public PayScoreService getPayScoreService() { + return payScoreService; + } + @Override public RedpackService getRedpackService() { return this.redpackService; diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java new file mode 100644 index 000000000..2756a712c --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java @@ -0,0 +1,193 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.github.binarywang.wxpay.bean.payscore.NotifyData; +import com.github.binarywang.wxpay.bean.payscore.WxPayScoreRequest; +import com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.PayScoreService; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.v3.util.AesUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.client.utils.URIBuilder; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author doger.wang + * @date 2020/5/14 9:43 + */ + +public class PayScoreServiceImpl implements PayScoreService { + private WxPayService payService; + + public PayScoreServiceImpl(WxPayService payService) { + this.payService = payService; + } + + + @Override + public WxPayScoreResult createServiceOrder(WxPayScoreRequest request) throws WxPayException { + boolean need_user_confirm = request.isNeed_user_confirm(); + WxPayConfig config = this.payService.getConfig(); + String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder"; + request.setAppid(config.getAppId()); + request.setService_id(config.getServiceId()); + request.setNotify_url(config.getPayScoreNotifyUrl()); + String result = payService.postV3(url, JSONObject.toJSONString(request)); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class); + + //补充算一下签名给小程序跳转用 + String currentTimeMillis = System.currentTimeMillis() + ""; + Map signMap = new HashMap<>(); + signMap.put("mch_id", config.getMchId()); + if (need_user_confirm){ + signMap.put("package", wxPayScoreCreateResult.getPackageX()); + }else { + signMap.put("service_id", config.getServiceId()); + signMap.put("out_order_no", request.getOut_order_no()); + } + signMap.put("timestamp", currentTimeMillis); + signMap.put("nonce_str", currentTimeMillis); + signMap.put("sign_type", "HMAC-SHA256"); + String sign = AesUtils.createSign(signMap, config.getMchKey()); + signMap.put("sign", sign); + wxPayScoreCreateResult.setPayScoreSignInfo(signMap); + return wxPayScoreCreateResult; + } + + @Override + public WxPayScoreResult queryServiceOrder(String out_order_no, String query_id) throws WxPayException, URISyntaxException { + WxPayConfig config = this.payService.getConfig(); + String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder"; + URIBuilder uriBuilder = new URIBuilder(url); + if (StringUtils.isAllEmpty(out_order_no,query_id) || !StringUtils.isAnyEmpty(out_order_no,query_id)){ + throw new WxPayException("out_order_no,query_id不允许都填写或都不填写"); + } + if (StringUtils.isNotEmpty(out_order_no)){ + uriBuilder.setParameter("out_order_no", out_order_no); + } + if (StringUtils.isNotEmpty(query_id)){ + uriBuilder.setParameter("query_id", query_id); + } + uriBuilder.setParameter("service_id", config.getServiceId()); + uriBuilder.setParameter("appid", config.getAppId()); + URI build = uriBuilder.build(); + String result = payService.getV3(build); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class); + //补充一下加密跳转信息 +/* String currentTimeMillis = System.currentTimeMillis() + ""; + Map signMap = new HashMap(); + signMap.put("mch_id", config.getMchId()); + signMap.put("service_id", config.getServiceId()); + signMap.put("out_order_no", out_order_no); + signMap.put("timestamp", currentTimeMillis); + signMap.put("nonce_str", currentTimeMillis); + signMap.put("sign_type", "HMAC-SHA256"); + String sign = AesUtil.createSign(signMap, config.getMchKey()); + signMap.put("sign", sign); + wxPayScoreCreateResult.setPayScoreSignInfo(signMap);*/ + return wxPayScoreCreateResult; + + } + + + + @Override + public WxPayScoreResult cancelServiceOrder(String out_order_no, String reason) throws WxPayException { + WxPayConfig config = this.payService.getConfig(); + String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/cancel"; + HashMap map = new HashMap<>(); + map.put("appid",config.getAppId()); + map.put("service_id",config.getServiceId()); + map.put("reason",reason); + String result = payService.postV3(url, JSONObject.toJSONString(map)); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class); + return wxPayScoreCreateResult; + + } + + @Override + public WxPayScoreResult modifyServiceOrder(WxPayScoreRequest request) throws WxPayException { + WxPayConfig config = this.payService.getConfig(); + String out_order_no = request.getOut_order_no(); + String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/modify"; + request.setAppid(config.getAppId()); + request.setService_id(config.getServiceId()); + request.setOut_order_no(null); + //request.setNotify_url(config.getPayScoreNotifyUrl()); + String result = payService.postV3(url, JSONObject.toJSONString(request)); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class); + return wxPayScoreCreateResult; + + + } + + @Override + public WxPayScoreResult completeServiceOrder(WxPayScoreRequest request) throws WxPayException { + WxPayConfig config = this.payService.getConfig(); + String out_order_no = request.getOut_order_no(); + String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/complete"; + request.setAppid(config.getAppId()); + request.setService_id(config.getServiceId()); + //request.setNotify_url(config.getPayScoreNotifyUrl()); + request.setOut_order_no(null); + String result = payService.postV3(url, JSONObject.toJSONString(request)); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class); + return wxPayScoreCreateResult; + + } + + @Override + public WxPayScoreResult payServiceOrder(String out_order_no) throws WxPayException { + WxPayConfig config = this.payService.getConfig(); + String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/pay"; + HashMap map = new HashMap<>(); + map.put("appid",config.getAppId()); + map.put("service_id",config.getServiceId()); + String result = payService.postV3(url, JSONObject.toJSONString(map)); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class); + return wxPayScoreCreateResult; + + } + + @Override + public WxPayScoreResult syncServiceOrder(WxPayScoreRequest request) throws WxPayException { + WxPayConfig config = this.payService.getConfig(); + String out_order_no = request.getOut_order_no(); + String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/sync"; + request.setAppid(config.getAppId()); + request.setService_id(config.getServiceId()); + request.setOut_order_no(null); + //request.setNotify_url(config.getPayScoreNotifyUrl()); + String result = payService.postV3(url, JSONObject.toJSONString(request)); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class); + return wxPayScoreCreateResult; + + } + + @Override + public WxPayScoreResult decryptNotifyData(NotifyData data) throws WxPayException{ + NotifyData.Resource resource = data.getResource(); + String ciphertext = resource.getCiphertext(); + String associated_data = resource.getAssociated_data(); + String nonce = resource.getNonce(); + String apiv3Key = this.payService.getConfig().getApiv3Key(); + try { + String s = AesUtils.decryptToString(associated_data, nonce, ciphertext, apiv3Key); + WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(s, WxPayScoreResult.class); + return wxPayScoreCreateResult; + } catch (GeneralSecurityException e) { + throw new WxPayException("解析报文异常",e); + } catch (IOException e) { + throw new WxPayException("解析报文异常",e); + } + + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java index 1703c200f..065a3fc08 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java @@ -1,26 +1,33 @@ package com.github.binarywang.wxpay.service.impl; +import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.net.URI; import java.nio.charset.StandardCharsets; import javax.net.ssl.SSLContext; +import com.alibaba.fastjson.JSONObject; import com.github.binarywang.wxpay.bean.WxPayApiData; import com.github.binarywang.wxpay.bean.request.WxPayQueryCommentRequest; import com.github.binarywang.wxpay.bean.request.WxPayRedpackQueryRequest; import com.github.binarywang.wxpay.bean.result.WxPayCommonResult; import com.github.binarywang.wxpay.bean.result.WxPayRedpackQueryResult; +import com.github.binarywang.wxpay.config.WxPayConfig; import com.github.binarywang.wxpay.exception.WxPayException; import jodd.util.Base64; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; +import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.impl.client.CloseableHttpClient; @@ -90,14 +97,81 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl { } } - private StringEntity createEntry(String requestStr) { - try { - return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); - } catch (UnsupportedEncodingException e) { - //cannot happen - this.log.error(e.getMessage(), e); - return null; + @Override + public String postV3(String url, String requestStr) throws WxPayException { + CloseableHttpClient httpClient = this.createApiV3HttpClient(); + HttpPost httpPost = this.createHttpPost(url, requestStr); + httpPost.addHeader("Accept", "application/json"); + httpPost.addHeader("Content-Type", "application/json"); + try (CloseableHttpResponse response = httpClient.execute(httpPost)){ + //v3已经改为通过状态码判断200 204 成功 + int statusCode = response.getStatusLine().getStatusCode(); + String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + if (HttpStatus.SC_OK==statusCode || HttpStatus.SC_NO_CONTENT==statusCode){ + this.log.info("\n【请求地址】:{}\n【请求数据】:{}\n【响应数据】:{}", url, requestStr, responseString); + return responseString; + }else { + //有错误提示信息返回 + JSONObject jsonObject = JSONObject.parseObject(responseString); + String message = jsonObject.getString("message"); + throw new WxPayException(message); + } + } catch (Exception e) { + this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage()); + throw new WxPayException(e.getMessage(), e); + } finally { + httpPost.releaseConnection(); } + + + + + } + + @Override + public String getV3(URI url) throws WxPayException { + CloseableHttpClient httpClient = this.createApiV3HttpClient(); + HttpGet httpGet = new HttpGet(url); + httpGet.addHeader("Accept", "application/json"); + httpGet.addHeader("Content-Type", "application/json"); + try (CloseableHttpResponse response = httpClient.execute(httpGet)){ + //v3已经改为通过状态码判断200 204 成功 + int statusCode = response.getStatusLine().getStatusCode(); + String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + if (HttpStatus.SC_OK==statusCode || HttpStatus.SC_NO_CONTENT==statusCode){ + this.log.info("\n【请求地址】:{}\n【响应数据】:{}", url , responseString); + return responseString; + }else { + //有错误提示信息返回 + JSONObject jsonObject = JSONObject.parseObject(responseString); + String message = jsonObject.getString("message"); + throw new WxPayException(message); + } + } catch (Exception e) { + this.log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage()); + throw new WxPayException(e.getMessage(), e); + } finally { + httpGet.releaseConnection(); + } + + + } + + private CloseableHttpClient createApiV3HttpClient() throws WxPayException { + CloseableHttpClient apiv3HttpClient = this.getConfig().getApiv3HttpClient(); + if (null==apiv3HttpClient){ + return this.getConfig().initApiV3HttpClient(); + } + return apiv3HttpClient; + } + + + private StringEntity createEntry(String requestStr) { + + return new StringEntity(requestStr, ContentType.create("application/json", "utf-8")); + //return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); + + } private HttpClientBuilder createHttpClientBuilder(boolean useKey) throws WxPayException { diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java index 81d35614d..72dac0171 100644 --- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java @@ -1,5 +1,6 @@ package com.github.binarywang.wxpay.service.impl; +import java.net.URI; import java.nio.charset.StandardCharsets; import javax.net.ssl.SSLContext; @@ -67,6 +68,16 @@ public class WxPayServiceJoddHttpImpl extends BaseWxPayServiceImpl { } } + @Override + public String postV3(String url, String requestStr) throws WxPayException { + return null; + } + + @Override + public String getV3(URI url) throws WxPayException { + return null; + } + private HttpRequest buildHttpRequest(String url, String requestStr, boolean useKey) throws WxPayException { HttpRequest request = HttpRequest .post(url) diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java new file mode 100644 index 000000000..d5102d1fa --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java @@ -0,0 +1,11 @@ +package com.github.binarywang.wxpay.v3; + +import java.io.IOException; +import org.apache.http.client.methods.HttpUriRequest; + +public interface Credentials { + + String getSchema(); + + String getToken(HttpUriRequest request) throws IOException; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java new file mode 100644 index 000000000..a28dfdcd6 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java @@ -0,0 +1,88 @@ +package com.github.binarywang.wxpay.v3; + +import java.io.IOException; +import org.apache.http.HttpEntity; +import org.apache.http.HttpException; +import org.apache.http.StatusLine; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpExecutionAware; +import org.apache.http.client.methods.HttpRequestWrapper; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.methods.RequestBuilder; +import org.apache.http.client.protocol.HttpClientContext; +import org.apache.http.conn.routing.HttpRoute; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.impl.execchain.ClientExecChain; +import org.apache.http.util.EntityUtils; + +public class SignatureExec implements ClientExecChain { + final ClientExecChain mainExec; + final Credentials credentials; + final Validator validator; + + SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) { + this.credentials = credentials; + this.validator = validator; + this.mainExec = mainExec; + } + + protected HttpEntity newRepeatableEntity(HttpEntity entity) throws IOException { + byte[] content = EntityUtils.toByteArray(entity); + ByteArrayEntity newEntity = new ByteArrayEntity(content); + newEntity.setContentEncoding(entity.getContentEncoding()); + newEntity.setContentType(entity.getContentType()); + + return newEntity; + } + + protected void convertToRepeatableResponseEntity(CloseableHttpResponse response) throws IOException { + HttpEntity entity = response.getEntity(); + if (entity != null && !entity.isRepeatable()) { + response.setEntity(newRepeatableEntity(entity)); + } + } + + protected void convertToRepeatableRequestEntity(HttpUriRequest request) throws IOException { + if (request instanceof HttpEntityEnclosingRequestBase) { + HttpEntity entity = ((HttpEntityEnclosingRequestBase) request).getEntity(); + if (entity != null && !entity.isRepeatable()) { + ((HttpEntityEnclosingRequestBase) request).setEntity(newRepeatableEntity(entity)); + } + } + } + + @Override + public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request, + HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException { + if (request.getURI().getHost().endsWith(".mch.weixin.qq.com")) { + return executeWithSignature(route, request, context, execAware); + } else { + return mainExec.execute(route, request, context, execAware); + } + } + + private CloseableHttpResponse executeWithSignature(HttpRoute route, HttpRequestWrapper request, + HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException { + HttpUriRequest newRequest = RequestBuilder.copy(request.getOriginal()).build(); + convertToRepeatableRequestEntity(newRequest); + // 添加认证信息 + newRequest.addHeader("Authorization", + credentials.getSchema() + " " + credentials.getToken(newRequest)); + + // 执行 + CloseableHttpResponse response = mainExec.execute( + route, HttpRequestWrapper.wrap(newRequest), context, execAware); + + // 对成功应答验签 + StatusLine statusLine = response.getStatusLine(); + if (statusLine.getStatusCode() >= 200 && statusLine.getStatusCode() < 300) { + convertToRepeatableResponseEntity(response); + if (!validator.validate(response)) { + throw new HttpException("应答的微信支付签名验证失败"); + } + } + return response; + } + +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java new file mode 100644 index 000000000..cdeb8ac27 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java @@ -0,0 +1,8 @@ +package com.github.binarywang.wxpay.v3; + +import java.io.IOException; +import org.apache.http.client.methods.CloseableHttpResponse; + +public interface Validator { + boolean validate(CloseableHttpResponse response) throws IOException; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java new file mode 100644 index 000000000..0f2732ce9 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java @@ -0,0 +1,75 @@ +package com.github.binarywang.wxpay.v3; + + +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.List; + +import com.github.binarywang.wxpay.v3.auth.CertificatesVerifier; +import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner; +import com.github.binarywang.wxpay.v3.auth.WechatPay2Credentials; +import com.github.binarywang.wxpay.v3.auth.WechatPay2Validator; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.impl.execchain.ClientExecChain; + +public class WechatPayHttpClientBuilder extends HttpClientBuilder { + private Credentials credentials; + private Validator validator; + + static final String os = System.getProperty("os.name") + "/" + System.getProperty("os.version"); + static final String version = System.getProperty("java.version"); + + private WechatPayHttpClientBuilder() { + super(); + + String userAgent = String.format( + "WechatPay-Apache-HttpClient/%s (%s) Java/%s", + getClass().getPackage().getImplementationVersion(), + os, + version == null ? "Unknown" : version); + setUserAgent(userAgent); + } + + public static WechatPayHttpClientBuilder create() { + return new WechatPayHttpClientBuilder(); + } + + public WechatPayHttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) { + this.credentials = + new WechatPay2Credentials(merchantId, new PrivateKeySigner(serialNo, privateKey)); + return this; + } + + public WechatPayHttpClientBuilder withCredentials(Credentials credentials) { + this.credentials = credentials; + return this; + } + + public WechatPayHttpClientBuilder withWechatpay(List certificates) { + this.validator = new WechatPay2Validator(new CertificatesVerifier(certificates)); + return this; + } + + public WechatPayHttpClientBuilder withValidator(Validator validator) { + this.validator = validator; + return this; + } + + @Override + public CloseableHttpClient build() { + if (credentials == null) { + throw new IllegalArgumentException("缺少身份认证信息"); + } + if (validator == null) { + throw new IllegalArgumentException("缺少签名验证信息"); + } + + return super.build(); + } + + @Override + protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) { + return new SignatureExec(this.credentials, this.validator, requestExecutor); + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java new file mode 100644 index 000000000..3c382181b --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java @@ -0,0 +1,161 @@ +package com.github.binarywang.wxpay.v3.auth; + + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.binarywang.wxpay.v3.Credentials; +import com.github.binarywang.wxpay.v3.WechatPayHttpClientBuilder; +import com.github.binarywang.wxpay.v3.util.AesUtils; +import com.github.binarywang.wxpay.v3.util.PemUtils; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 在原有CertificatesVerifier基础上,增加自动更新证书功能 + */ +public class AutoUpdateCertificatesVerifier implements Verifier { + + private static final Logger log = LoggerFactory.getLogger(AutoUpdateCertificatesVerifier.class); + + //证书下载地址 + private static final String CertDownloadPath = "https://api.mch.weixin.qq.com/v3/certificates"; + + //上次更新时间 + private volatile Instant instant; + + //证书更新间隔时间,单位为分钟 + private int minutesInterval; + + private CertificatesVerifier verifier; + + private Credentials credentials; + + private byte[] apiV3Key; + + private ReentrantLock lock = new ReentrantLock(); + + //时间间隔枚举,支持一小时、六小时以及十二小时 + public enum TimeInterval { + OneHour(60), SixHours(60 * 6), TwelveHours(60 * 12); + + private int minutes; + + TimeInterval(int minutes) { + this.minutes = minutes; + } + + public int getMinutes() { + return minutes; + } + } + + public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) { + this(credentials, apiV3Key, TimeInterval.OneHour.getMinutes()); + } + + public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key, int minutesInterval) { + this.credentials = credentials; + this.apiV3Key = apiV3Key; + this.minutesInterval = minutesInterval; + //构造时更新证书 + try { + autoUpdateCert(); + instant = Instant.now(); + } catch (IOException | GeneralSecurityException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean verify(String serialNumber, byte[] message, String signature) { + if (instant == null || Duration.between(instant, Instant.now()).toMinutes() >= minutesInterval) { + if (lock.tryLock()) { + try { + autoUpdateCert(); + //更新时间 + instant = Instant.now(); + } catch (GeneralSecurityException | IOException e) { + log.warn("Auto update cert failed, exception = " + e); + } finally { + lock.unlock(); + } + } + } + return verifier.verify(serialNumber, message, signature); + } + + private void autoUpdateCert() throws IOException, GeneralSecurityException { + CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create() + .withCredentials(credentials) + .withValidator(verifier == null ? (response) -> true : new WechatPay2Validator(verifier)) + .build(); + + HttpGet httpGet = new HttpGet(CertDownloadPath); + httpGet.addHeader("Accept", "application/json"); + + CloseableHttpResponse response = httpClient.execute(httpGet); + int statusCode = response.getStatusLine().getStatusCode(); + String body = EntityUtils.toString(response.getEntity()); + if (statusCode == 200) { + List newCertList = deserializeToCerts(apiV3Key, body); + if (newCertList.isEmpty()) { + log.warn("Cert list is empty"); + return; + } + this.verifier = new CertificatesVerifier(newCertList); + } else { + log.warn("Auto update cert failed, statusCode = " + statusCode + ",body = " + body); + } + } + + + /** + * 反序列化证书并解密 + */ + private List deserializeToCerts(byte[] apiV3Key, String body) + throws GeneralSecurityException, IOException { + AesUtils decryptor = new AesUtils(apiV3Key); + ObjectMapper mapper = new ObjectMapper(); + JsonNode dataNode = mapper.readTree(body).get("data"); + List newCertList = new ArrayList<>(); + if (dataNode != null) { + for (int i = 0, count = dataNode.size(); i < count; i++) { + JsonNode encryptCertificateNode = dataNode.get(i).get("encrypt_certificate"); + //解密 + String cert = decryptor.decryptToString( + encryptCertificateNode.get("associated_data").toString().replaceAll("\"", "") + .getBytes("utf-8"), + encryptCertificateNode.get("nonce").toString().replaceAll("\"", "") + .getBytes("utf-8"), + encryptCertificateNode.get("ciphertext").toString().replaceAll("\"", "")); + + X509Certificate x509Cert = PemUtils + .loadCertificate(new ByteArrayInputStream(cert.getBytes("utf-8"))); + try { + x509Cert.checkValidity(); + } catch (CertificateExpiredException | CertificateNotYetValidException e) { + continue; + } + newCertList.add(x509Cert); + } + } + return newCertList; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java new file mode 100644 index 000000000..9853cf2ee --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java @@ -0,0 +1,43 @@ +package com.github.binarywang.wxpay.v3.auth; + +import java.math.BigInteger; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.HashMap; +import java.util.List; + +public class CertificatesVerifier implements Verifier { + private final HashMap certificates = new HashMap<>(); + + public CertificatesVerifier(List list) { + + for (X509Certificate item : list) { + certificates.put(item.getSerialNumber(), item); + } + } + + private boolean verify(X509Certificate certificate, byte[] message, String signature) { + try { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initVerify(certificate); + sign.update(message); + return sign.verify(Base64.getDecoder().decode(signature)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); + } catch (SignatureException e) { + throw new RuntimeException("签名验证过程发生了错误", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("无效的证书", e); + } + } + + @Override + public boolean verify(String serialNumber, byte[] message, String signature) { + BigInteger val = new BigInteger(serialNumber, 16); + return certificates.containsKey(val) && verify(certificates.get(val), message, signature); + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java new file mode 100644 index 000000000..37ec51cf5 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java @@ -0,0 +1,37 @@ +package com.github.binarywang.wxpay.v3.auth; + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.Base64; + +public class PrivateKeySigner implements Signer { + private String certificateSerialNumber; + + private PrivateKey privateKey; + + public PrivateKeySigner(String serialNumber, PrivateKey privateKey) { + this.certificateSerialNumber = serialNumber; + this.privateKey = privateKey; + } + + @Override + public SignatureResult sign(byte[] message) { + try { + Signature sign = Signature.getInstance("SHA256withRSA"); + sign.initSign(privateKey); + sign.update(message); + + return new SignatureResult( + Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("当前Java环境不支持SHA256withRSA", e); + } catch (SignatureException e) { + throw new RuntimeException("签名计算失败", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("无效的私钥", e); + } + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java new file mode 100644 index 000000000..7255a1b43 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java @@ -0,0 +1,15 @@ +package com.github.binarywang.wxpay.v3.auth; + +public interface Signer { + SignatureResult sign(byte[] message); + + class SignatureResult { + String sign; + String certificateSerialNumber; + + public SignatureResult(String sign, String serialNumber) { + this.sign = sign; + this.certificateSerialNumber = serialNumber; + } + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java new file mode 100644 index 000000000..ed591a4ef --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java @@ -0,0 +1,5 @@ +package com.github.binarywang.wxpay.v3.auth; + +public interface Verifier { + boolean verify(String serialNumber, byte[] message, String signature); +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java new file mode 100644 index 000000000..a0730bdda --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java @@ -0,0 +1,91 @@ +package com.github.binarywang.wxpay.v3.auth; + + +import java.io.IOException; +import java.net.URI; +import java.security.SecureRandom; + +import com.github.binarywang.wxpay.v3.Credentials; +import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WechatPay2Credentials implements Credentials { + private static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class); + + private static final String SYMBOLS = + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final SecureRandom RANDOM = new SecureRandom(); + protected String merchantId; + protected Signer signer; + + public WechatPay2Credentials(String merchantId, Signer signer) { + this.merchantId = merchantId; + this.signer = signer; + } + + public String getMerchantId() { + return merchantId; + } + + protected long generateTimestamp() { + return System.currentTimeMillis() / 1000; + } + + protected String generateNonceStr() { + char[] nonceChars = new char[32]; + for (int index = 0; index < nonceChars.length; ++index) { + nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length())); + } + return new String(nonceChars); + } + + @Override + public final String getSchema() { + return "WECHATPAY2-SHA256-RSA2048"; + } + + @Override + public final String getToken(HttpUriRequest request) throws IOException { + String nonceStr = generateNonceStr(); + long timestamp = generateTimestamp(); + + String message = buildMessage(nonceStr, timestamp, request); + log.debug("authorization message=[{}]", message); + + Signer.SignatureResult signature = signer.sign(message.getBytes("utf-8")); + + String token = "mchid=\"" + getMerchantId() + "\"," + + "nonce_str=\"" + nonceStr + "\"," + + "timestamp=\"" + timestamp + "\"," + + "serial_no=\"" + signature.certificateSerialNumber + "\"," + + "signature=\"" + signature.sign + "\""; + log.debug("authorization token=[{}]", token); + + return token; + } + + protected final String buildMessage(String nonce, long timestamp, HttpUriRequest request) + throws IOException { + URI uri = request.getURI(); + String canonicalUrl = uri.getRawPath(); + if (uri.getQuery() != null) { + canonicalUrl += "?" + uri.getRawQuery(); + } + + String body = ""; + // PATCH,POST,PUT + if (request instanceof HttpEntityEnclosingRequestBase) { + body = EntityUtils.toString(((HttpEntityEnclosingRequestBase) request).getEntity()); + } + + return request.getRequestLine().getMethod() + "\n" + + canonicalUrl + "\n" + + timestamp + "\n" + + nonce + "\n" + + body + "\n"; + } + +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java new file mode 100644 index 000000000..38735c61c --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java @@ -0,0 +1,55 @@ +package com.github.binarywang.wxpay.v3.auth; + + +import java.io.IOException; + +import com.github.binarywang.wxpay.v3.Validator; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class WechatPay2Validator implements Validator { + + private static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class); + + private Verifier verifier; + + public WechatPay2Validator(Verifier verifier) { + this.verifier = verifier; + } + + @Override + public final boolean validate(CloseableHttpResponse response) throws IOException { + Header serialNo = response.getFirstHeader("Wechatpay-Serial"); + Header sign = response.getFirstHeader("Wechatpay-Signature"); + Header timestamp = response.getFirstHeader("Wechatpay-TimeStamp"); + Header nonce = response.getFirstHeader("Wechatpay-Nonce"); + + // todo: check timestamp + if (timestamp == null || nonce == null || serialNo == null || sign == null) { + return false; + } + + String message = buildMessage(response); + return verifier.verify(serialNo.getValue(), message.getBytes("utf-8"), sign.getValue()); + } + + protected final String buildMessage(CloseableHttpResponse response) throws IOException { + String timestamp = response.getFirstHeader("Wechatpay-TimeStamp").getValue(); + String nonce = response.getFirstHeader("Wechatpay-Nonce").getValue(); + + String body = getResponseBody(response); + return timestamp + "\n" + + nonce + "\n" + + body + "\n"; + } + + protected final String getResponseBody(CloseableHttpResponse response) throws IOException { + HttpEntity entity = response.getEntity(); + + return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : ""; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java new file mode 100644 index 000000000..bd68cac1a --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java @@ -0,0 +1,107 @@ +package com.github.binarywang.wxpay.v3.util; + +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class AesUtils { + + static final int KEY_LENGTH_BYTE = 32; + static final int TAG_LENGTH_BIT = 128; + private final byte[] aesKey; + + public AesUtils(byte[] key) { + if (key.length != KEY_LENGTH_BYTE) { + throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节"); + } + this.aesKey = key; + } + + public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext) + throws GeneralSecurityException, IOException { + 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.getDecoder().decode(ciphertext)), "utf-8"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException(e); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException(e); + } + } + + public static String decryptToString(String associatedData, String nonce, String ciphertext,String apiV3Key) + throws GeneralSecurityException, IOException { + try { + Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); + + SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(), "AES"); + GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce.getBytes()); + + cipher.init(Cipher.DECRYPT_MODE, key, spec); + cipher.updateAAD(associatedData.getBytes()); + + return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8"); + } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { + throw new IllegalStateException(e); + } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { + throw new IllegalArgumentException(e); + } + } + + + public static String createSign(Map map, String mchKey) { + Map params = map; + SortedMap sortedMap = new TreeMap<>(params); + + StringBuilder toSign = new StringBuilder(); + for (String key : sortedMap.keySet()) { + String value = params.get(key); + if ("sign".equals(key) || StringUtils.isEmpty(value)) { + continue; + } + toSign.append(key).append("=").append(value).append("&"); + } + toSign.append("key=" + mchKey); + return HMACSHA256(toSign.toString(), mchKey); + + } + + public static String HMACSHA256(String data, String key) { + try { + Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256"); + sha256_HMAC.init(secret_key); + byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte item : array) { + sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3)); + } + return sb.toString().toUpperCase(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + } + + +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java new file mode 100644 index 000000000..bf4d2657b --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java @@ -0,0 +1,60 @@ +package com.github.binarywang.wxpay.v3.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateFactory; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; + +public class PemUtils { + + public static PrivateKey loadPrivateKey(InputStream inputStream) { + try { + ByteArrayOutputStream array = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) != -1) { + array.write(buffer, 0, length); + } + + String privateKey = array.toString("utf-8") + .replace("-----BEGIN PRIVATE KEY-----", "") + .replace("-----END PRIVATE KEY-----", "") + .replaceAll("\\s+", ""); + + KeyFactory kf = KeyFactory.getInstance("RSA"); + return kf.generatePrivate( + new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey))); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("当前Java环境不支持RSA", e); + } catch (InvalidKeySpecException e) { + throw new RuntimeException("无效的密钥格式"); + } catch (IOException e) { + throw new RuntimeException("无效的密钥"); + } + } + + public static X509Certificate loadCertificate(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); + } + } +}