diff --git a/pom.xml b/pom.xml index bd533a6d..1b957806 100644 --- a/pom.xml +++ b/pom.xml @@ -126,11 +126,10 @@ qrcode-utils 1.1 - org.jodd jodd-http - 3.7.1 + 5.2.0 provided @@ -163,7 +162,7 @@ org.apache.commons commons-lang3 - 3.5 + 3.10 org.slf4j 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 43b2114e..241b12e1 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 @@ -50,6 +50,14 @@ public class WxPayAutoConfiguration { 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 fe8a2156..0cad58f7 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,34 @@ 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-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java new file mode 100644 index 00000000..ccb8aece --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java @@ -0,0 +1,23 @@ +package me.chanjar.weixin.common.error; + +/** + * WxJava专用的runtime exception. + * + * @author Binary Wang + * @date 2020-09-26 + */ +public class WxRuntimeException extends RuntimeException { + private static final long serialVersionUID = 4881698471192264412L; + + public WxRuntimeException(Throwable e) { + super(e); + } + + public WxRuntimeException(String msg) { + super(msg); + } + + public WxRuntimeException(String msg, Throwable e) { + super(msg, e); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java index 4f310274..df5cfeb2 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; import me.chanjar.weixin.common.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; @@ -19,6 +18,8 @@ import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; + /** * . @@ -47,7 +48,7 @@ public class JoddHttpMediaDownloadRequestExecutor extends BaseMediaDownloadReque request.withConnectionProvider(requestHttp.getRequestHttpClient()); HttpResponse response = request.send(); - response.charset(StringPool.UTF_8); + response.charset(StandardCharsets.UTF_8.name()); String contentType = response.header("Content-Type"); if (contentType != null && contentType.startsWith("application/json")) { diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java index 3c7122a1..a526772f 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; import me.chanjar.weixin.common.WxType; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxError; @@ -14,6 +13,7 @@ import me.chanjar.weixin.common.util.http.RequestHttp; import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * . @@ -35,7 +35,7 @@ public class JoddHttpMediaUploadRequestExecutor extends MediaUploadRequestExecut request.withConnectionProvider(requestHttp.getRequestHttpClient()); request.form("media", file); HttpResponse response = request.send(); - response.charset(StringPool.UTF_8); + response.charset(StandardCharsets.UTF_8.name()); String responseContent = response.bodyText(); WxError error = WxError.fromJson(responseContent, wxType); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java index c93bd4b1..9706d708 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; import me.chanjar.weixin.common.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; @@ -12,6 +11,7 @@ import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * . @@ -39,7 +39,7 @@ public class JoddHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutorweixin-java-pay WxJava - PAY Java SDK 微信支付 Java SDK + + + + org.apache.maven.plugins + maven-compiler-plugin + + 8 + 8 + + + + diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/BusinessCircleNotifyData.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/BusinessCircleNotifyData.java new file mode 100644 index 00000000..f8f0a1ef --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/BusinessCircleNotifyData.java @@ -0,0 +1,93 @@ +package com.github.binarywang.wxpay.bean.businesscircle; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 智慧商圈回调通知对象 + *
+ *   文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_1.shtml
+ *   https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_3.shtml
+ * 
+ * + * @author thinsstar + */ +@NoArgsConstructor +@Data +public class BusinessCircleNotifyData implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 通知ID + */ + @SerializedName("id") + private String id; + + /** + * 通知创建时间 + */ + @SerializedName("create_time") + private String createTime; + + /** + * 通知类型 + */ + @SerializedName("event_type") + private String eventType; + + /** + * 通知数据类型 + */ + @SerializedName("resource_type") + private String resourceType; + + /** + * 回调摘要 + * summary + */ + @SerializedName("summary") + private String summary; + + /** + * 通知数据 + */ + @SerializedName("resource") + private Resource resource; + + @Data + public static class Resource implements Serializable { + private static final long serialVersionUID = 1L; + /** + * 加密算法类型 + */ + @SerializedName("algorithm") + private String algorithm; + + /** + * 数据密文 + */ + @SerializedName("ciphertext") + private String cipherText; + + /** + * 附加数据 + */ + @SerializedName("associated_data") + private String associatedData; + + /** + * 随机串 + */ + @SerializedName("nonce") + private String nonce; + + /** + * 原始回调类型 + */ + @SerializedName("original_type") + private String originalType; + } +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PaidResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PaidResult.java new file mode 100644 index 00000000..24f1fbde --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PaidResult.java @@ -0,0 +1,112 @@ +package com.github.binarywang.wxpay.bean.businesscircle; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 商圈支付结果通知内容 + *
+ *  文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_1.shtml
+ * 
+ * + * @author thinsstar + */ +@NoArgsConstructor +@Data +public class PaidResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 商户号 + *

+ * 微信支付分配的商户号 + * 示例值:1230000109 + */ + @SerializedName("mchid") + private String mchid; + + /** + * 商圈商户名称 + *

+ * 商圈商户名称 + * 示例值:微信支付 + */ + @SerializedName("merchant_name") + private String merchantName; + + /** + * 门店名称 + *

+ * 门店名称,商圈在商圈小程序上圈店时填写的门店名称 + * 示例值:微信支付 + */ + @SerializedName("shop_name") + private String shopName; + + /** + * 门店编号 + *

+ * 门店编号,商圈在商圈小程序上圈店时填写的门店编号,用于跟商圈自身已有的商户识别码对齐 + * 示例值:123456 + */ + @SerializedName("shop_number") + private String shopNumber; + + /** + * 小程序APPID + *

+ * 顾客授权积分时使用的小程序的appid + * 示例值:wxd678efh567hg6787 + */ + @SerializedName("appid") + private String appid; + + /** + * 用户标识 + *

+ * 顾客授权时使用的小程序上的openid + * 示例值:oUpF8uMuAJ2pxb1Q9zNjWeS6o + */ + @SerializedName("openid") + private String openid; + + /** + * 交易完成时间 + *

+ * 交易完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒毫秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒(需要增加所有跟时间有关的参数的描述) + * 示例值:2015-05-20T13:29:35+08:00 + */ + @SerializedName("time_end") + private String timeEnd; + + /** + * 金额 + *

+ * 用户实际消费金额,单位(分) + * 示例值:200 + */ + @SerializedName("amount") + private Integer amount; + + /** + * 微信支付订单号 + *

+ * 微信支付订单号 + * 示例值:1234567890 + */ + @SerializedName("transaction_id") + private String transactionId; + + /** + * 手动提交积分标记 + *

+ * 手动提交积分标记,自动提交时无该字段,用于区分用户手动申请后推送的积分数据 + * 示例值:oUpF8uMuAJ2pxb1Q9zNjWUHsd + */ + @SerializedName("commit_tag") + private String commitTag; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PointsNotifyRequest.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PointsNotifyRequest.java new file mode 100644 index 00000000..541335ef --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PointsNotifyRequest.java @@ -0,0 +1,154 @@ +package com.github.binarywang.wxpay.bean.businesscircle; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 商圈积分同步 + *

+ *   文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_2.shtml
+ * 
+ * + * @author thinsstar + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PointsNotifyRequest implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + *
+   * 字段名:商圈商户ID
+   * 变量名:sub_mchid
+   * 是否必填:否
+   * 类型:string[1,64]
+   * 描述:
+   *  当以服务商模式管理商圈积分能力时,则要带上商圈商户ID,否则留空
+   *  示例值:1234567890
+   * 
+ */ + @SerializedName(value = "sub_mchid") + private String subMchid; + + /** + *
+   * 字段名:微信订单号
+   * 变量名:transaction_id
+   * 是否必填:是
+   * 类型:string[1,64]
+   * 描述:
+   *  微信支付推送的商圈内交易通知里携带的微信订单号
+   *  示例值:1217752501201407033233368018
+   * 
+ */ + @SerializedName(value = "transaction_id") + private String transactionId; + + /** + *
+   * 字段名:小程序appid
+   * 变量名:appid
+   * 是否必填:是
+   * 类型:string[1,128]
+   * 描述:
+   *  顾客授权积分时使用的小程序的appid
+   *  示例值:wx1234567890abcdef
+   * 
+ */ + @SerializedName(value = "appid") + private String appid; + + /** + *
+   * 字段名:用户标识
+   * 变量名:openid
+   * 是否必填:是
+   * 类型:string[1,64]
+   * 描述:
+   *  顾客授权时使用的小程序上的openid
+   *  示例值:oWmnN4xxxxxxxxxxe92NHIGf1xd8
+   * 
+ */ + @SerializedName(value = "openid") + private String openid; + + /** + *
+   * 字段名:是否获得积分
+   * 变量名:earn_points
+   * 是否必填:是
+   * 类型:boolean
+   * 描述:
+   *  用于标明此单是否获得积分,
+   *  true为获得积分,
+   *  false为未获得
+   *  示例值:true
+   * 
+ */ + @SerializedName(value = "earn_points") + private Boolean earnPoints; + + /** + *
+   * 字段名:订单新增积分值
+   * 变量名:increased_points
+   * 是否必填:是
+   * 类型:int
+   * 描述:
+   *  顾客此笔交易新增的积分值
+   *  示例值:100
+   * 
+ */ + @SerializedName(value = "increased_points") + private Integer increasedPoints; + + /** + *
+   * 字段名:积分更新时间
+   * 变量名:points_update_time
+   * 是否必填:是
+   * 类型:string[1,32]
+   * 描述:
+   *  为顾客此笔交易成功积分的时间
+   *  示例值:2020-05-20T13:29:35.120+08:00
+   * 
+ */ + @SerializedName(value = "points_update_time") + private String pointsUpdateTime; + + /** + *
+   * 字段名:未获得积分的备注信息
+   * 变量名:no_points_remarks
+   * 是否必填:否
+   * 类型:string[1,128]
+   * 描述:
+   *  当未获得积分时,提供未获得积分的原因等备注信息
+   *  示例值:商品不参与积分活动
+   * 
+ */ + @SerializedName(value = "no_points_remarks") + private String noPointsRemarks; + + /** + *
+   * 字段名:顾客积分总额
+   * 变量名:total_points
+   * 是否必填:否
+   * 类型:int
+   * 描述:
+   *  当前顾客积分总额
+   *  示例值:888888
+   * 
+ */ + @SerializedName(value = "total_points") + private Integer totalPoints; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/RefundResult.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/RefundResult.java new file mode 100644 index 00000000..58c92c34 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/RefundResult.java @@ -0,0 +1,121 @@ +package com.github.binarywang.wxpay.bean.businesscircle; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 商圈退款成功通知内容 + *
+ *  文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_3.shtml
+ * 
+ * + * @author thinsstar + */ +@NoArgsConstructor +@Data +public class RefundResult implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 商户号 + *

+ * 微信支付分配的商户号 + * 示例值:1230000109 + */ + @SerializedName("mchid") + private String mchid; + + /** + * 商圈商户名称 + *

+ * 商圈商户名称 + * 示例值:微信支付 + */ + @SerializedName("merchant_name") + private String merchantName; + + /** + * 门店名称 + *

+ * 门店名称,商圈在商圈小程序上圈店时填写的门店名称 + * 示例值:微信支付 + */ + @SerializedName("shop_name") + private String shopName; + + /** + * 门店编号 + *

+ * 门店编号,商圈在商圈小程序上圈店时填写的门店编号,用于跟商圈自身已有的商户识别码对齐 + * 示例值:123456 + */ + @SerializedName("shop_number") + private String shopNumber; + + /** + * 小程序APPID + *

+ * 顾客授权积分时使用的小程序的appid + * 示例值:wxd678efh567hg6787 + */ + @SerializedName("appid") + private String appid; + + /** + * 用户标识 + *

+ * 顾客授权时使用的小程序上的openid + * 示例值:oUpF8uMuAJ2pxb1Q9zNjWeS6o + */ + @SerializedName("openid") + private String openid; + + /** + * 退款完成时间 + *

+ * 退款完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒毫秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒(需要增加所有跟时间有关的参数的描述) + * 示例值:2015-05-20T13:29:35+08:00 + */ + @SerializedName("refund_time") + private String refundTime; + + /** + * 消费金额 + *

+ * 用户实际消费金额,单位(分) + * 示例值:100 + */ + @SerializedName("pay_amount") + private Integer payAmount; + + /** + * 退款金额 + *

+ * 用户退款金额,单位(分) + * 示例值:100 + */ + @SerializedName("refund_amount") + private Integer refundAmount; + + /** + * 微信支付订单号 + *

+ * 微信支付订单号 + * 示例值:1234567890 + */ + @SerializedName("transaction_id") + private String transactionId; + + /** + * 微信支付退款单号 + *

+ * 微信支付退款单号 + * 示例值:1217752501201407033233368999 + */ + @SerializedName("refund_id") + private String refundId; +} diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java new file mode 100644 index 00000000..bd50ac89 --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java @@ -0,0 +1,35 @@ +package com.github.binarywang.wxpay.bean.ecommerce; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 微信通知接口头部信息,需要做签名验证 + * 文档地址: https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-yan-zheng + */ +@Data +@NoArgsConstructor +public class SignatureHeader implements Serializable { + private static final long serialVersionUID = -6958015499416059949L; + /** + * 时间戳 + */ + private String timeStamp; + + /** + * 随机串 + */ + private String nonce; + + /** + * 已签名字符串 + */ + private String signed; + + /** + * 证书序列号 + */ + private String serialNo; +} 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 c0ca2cf5..8fed2745 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,27 @@ package com.github.binarywang.wxpay.config; import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.v3.WxPayV3HttpClientBuilder; +import com.github.binarywang.wxpay.v3.auth.*; +import com.github.binarywang.wxpay.v3.util.PemUtils; +import jodd.util.ResourcesUtil; import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.SneakyThrows; import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.RegExUtils; 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.nio.charset.StandardCharsets; import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Collections; /** * 微信支付配置 @@ -17,8 +29,11 @@ import java.security.KeyStore; * @author Binary Wang (https://github.com/binarywang) */ @Data +@EqualsAndHashCode(exclude = "verifier") public class WxPayConfig { private static final String DEFAULT_PAY_BASE_URL = "https://api.mch.weixin.qq.com"; + private static final String PROBLEM_MSG = "证书文件【%s】有问题,请核实!"; + private static final String NOT_FOUND_MSG = "证书文件【%s】不存在,请核实!"; /** * 微信支付接口请求地址域名部分. @@ -85,6 +100,52 @@ 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 String payScorePermissionNotifyUrl; + + + private CloseableHttpClient apiV3HttpClient; + /** + * 私钥信息 + */ + private PrivateKey privateKey; + + /** + * 证书自动更新时间差(分钟),默认一分钟 + */ + private int certAutoUpdateTime = 60; + /** * p12证书文件内容的字节数组. */ @@ -106,8 +167,15 @@ public class WxPayConfig { private String httpProxyUsername; private String httpProxyPassword; + /** + * v3接口下证书检验对象,通过改对象可以获取到X509Certificate,进一步对敏感信息加密 + * 文档见 https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi + */ + private Verifier verifier; + /** * 返回所设置的微信支付接口请求地址域名. + * * @return 微信支付接口请求地址域名 */ public String getPayBaseUrl() { @@ -118,6 +186,15 @@ public class WxPayConfig { return this.payBaseUrl; } + @SneakyThrows + public Verifier getVerifier() { + if (verifier == null) { + //当改对象为null时,初始化api v3的请求头 + initApiV3HttpClient(); + } + return verifier; + } + /** * 初始化ssl. * @@ -136,40 +213,7 @@ public class WxPayConfig { if (StringUtils.isBlank(this.getKeyPath())) { throw new WxPayException("请确保证书文件地址keyPath已配置"); } - - final String prefix = "classpath:"; - String fileHasProblemMsg = "证书文件【" + this.getKeyPath() + "】有问题,请核实!"; - String fileNotFoundMsg = "证书文件【" + this.getKeyPath() + "】不存在,请核实!"; - if (this.getKeyPath().startsWith(prefix)) { - String path = StringUtils.removeFirst(this.getKeyPath(), prefix); - if (!path.startsWith("/")) { - path = "/" + path; - } - inputStream = WxPayConfig.class.getResourceAsStream(path); - if (inputStream == null) { - throw new WxPayException(fileNotFoundMsg); - } - } else if (this.getKeyPath().startsWith("http://") || this.getKeyPath().startsWith("https://")) { - try { - inputStream = new URL(this.keyPath).openStream(); - if (inputStream == null) { - throw new WxPayException(fileNotFoundMsg); - } - } catch (IOException e) { - throw new WxPayException(fileNotFoundMsg, e); - } - } else { - try { - File file = new File(this.getKeyPath()); - if (!file.exists()) { - throw new WxPayException(fileNotFoundMsg); - } - - inputStream = new FileInputStream(file); - } catch (IOException e) { - throw new WxPayException(fileHasProblemMsg, e); - } - } + inputStream = this.loadConfigInputStream(this.getKeyPath()); } try { @@ -185,4 +229,104 @@ public class WxPayConfig { } } + /** + * 初始化api v3请求头 自动签名验签 + * 方法参照微信官方https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient + * + * @return org.apache.http.impl.client.CloseableHttpClient + * @author doger.wang + **/ + public CloseableHttpClient initApiV3HttpClient() throws WxPayException { + String privateKeyPath = this.getPrivateKeyPath(); + String privateCertPath = this.getPrivateCertPath(); + String serialNo = 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 = this.loadConfigInputStream(privateKeyPath); + InputStream certInputStream = this.loadConfigInputStream(privateCertPath); + try { + PrivateKey merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream); + X509Certificate certificate = PemUtils.loadCertificate(certInputStream); + if(StringUtils.isBlank(serialNo)){ + this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase(); + } + + AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier( + new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)), + apiV3Key.getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime()); + + CloseableHttpClient httpClient = WxPayV3HttpClientBuilder.create() + .withMerchant(mchId, certSerialNo, merchantPrivateKey) + .withWechatpay(Collections.singletonList(certificate)) + .withValidator(new WxPayValidator(verifier)) + .build(); + this.apiV3HttpClient = httpClient; + this.verifier=verifier; + this.privateKey = merchantPrivateKey; + + return httpClient; + } catch (Exception e) { + throw new WxPayException("v3请求构造异常!", e); + } + } + + /** + * 从配置路径 加载配置 信息(支持 classpath、本地路径、网络url) + * @param configPath 配置路径 + * @return + * @throws WxPayException + */ + private InputStream loadConfigInputStream(String configPath) throws WxPayException { + InputStream inputStream; + final String prefix = "classpath:"; + String fileHasProblemMsg = String.format(PROBLEM_MSG, configPath); + String fileNotFoundMsg = String.format(NOT_FOUND_MSG, configPath); + if (configPath.startsWith(prefix)) { + String path = RegExUtils.removeFirst(configPath, prefix); + if (!path.startsWith("/")) { + path = "/" + path; + } + try { + inputStream = ResourcesUtil.getResourceAsStream(path); + if (inputStream == null) { + throw new WxPayException(fileNotFoundMsg); + } + } catch (Exception e) { + throw new WxPayException(fileNotFoundMsg, e); + } + } else if (configPath.startsWith("http://") || configPath.startsWith("https://")) { + try { + inputStream = new URL(configPath).openStream(); + if (inputStream == null) { + throw new WxPayException(fileNotFoundMsg); + } + } catch (IOException e) { + throw new WxPayException(fileNotFoundMsg, e); + } + } else { + try { + File file = new File(configPath); + if (!file.exists()) { + throw new WxPayException(fileNotFoundMsg); + } + + inputStream = new FileInputStream(file); + } catch (IOException e) { + throw new WxPayException(fileHasProblemMsg, e); + } + } + return inputStream; + } } diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java new file mode 100644 index 00000000..21af39ae --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java @@ -0,0 +1,35 @@ +package com.github.binarywang.wxpay.service; + +import com.github.binarywang.wxpay.bean.businesscircle.BusinessCircleNotifyData; +import com.github.binarywang.wxpay.bean.businesscircle.PaidResult; +import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest; +import com.github.binarywang.wxpay.bean.businesscircle.RefundResult; +import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader; +import com.github.binarywang.wxpay.exception.WxPayException; + +/** + *

+ * 微信支付智慧商圈API
+ * 
+ * + * @author thinsstar + */ +public interface BusinessCircleService { + /** + *
+   * 智慧商圈接口-商圈积分同步API
+   * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_2.shtml
+   * 接口链接:https://api.mch.weixin.qq.com/v3/businesscircle/points/notify
+   * 
+ * + * @param request 请求对象 + * @throws WxPayException the wx pay exception + */ + void notifyPoints(PointsNotifyRequest request) throws WxPayException; + + BusinessCircleNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException; + + PaidResult decryptPaidNotifyDataResource(BusinessCircleNotifyData data) throws WxPayException; + + RefundResult decryptRefundNotifyDataResource(BusinessCircleNotifyData 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 52f99af6..c6043d6d 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 @@ -9,8 +9,11 @@ import com.github.binarywang.wxpay.bean.request.*; import com.github.binarywang.wxpay.bean.result.*; import com.github.binarywang.wxpay.config.WxPayConfig; import com.github.binarywang.wxpay.exception.WxPayException; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpRequestBase; import java.io.File; +import java.io.InputStream; import java.util.Date; import java.util.Map; @@ -53,6 +56,66 @@ 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; + + /** + * 发送post请求,得到响应字符串. + *

+ * 部分字段会包含敏感信息,所以在提交前需要在请求头中会包含"Wechatpay-Serial"信息 + * + * @param url 请求地址 + * @param requestStr 请求信息 + * @return 返回请求结果字符串 string + * @throws WxPayException the wx pay exception + */ + String postV3WithWechatpaySerial(String url, String requestStr) throws WxPayException; + + /** + * 发送post请求,得到响应字符串. + * + * @param url 请求地址 + * @param httpPost 请求信息 + * @return 返回请求结果字符串 string + * @throws WxPayException the wx pay exception + */ + String postV3(String url, HttpPost httpPost) throws WxPayException; + + /** + * 发送http请求,得到响应字符串. + * + * @param url 请求地址 + * @param httpRequest 请求信息,可以是put,post,get,delete等请求 + * @return 返回请求结果字符串 string + * @throws WxPayException the wx pay exception + */ + String requestV3(String url, HttpRequestBase httpRequest) throws WxPayException; + + /** + * 发送get V3请求,得到响应字符串. + * + * @param url 请求地址 + * @return 返回请求结果字符串 string + * @throws WxPayException the wx pay exception + */ + String getV3(String url) throws WxPayException; + + /** + * 发送下载 V3请求,得到响应流. + * + * @param url 请求地址 + * @return 返回请求响应流 input stream + * @throws WxPayException the wx pay exception + */ + InputStream downloadV3(String url) throws WxPayException; + /** * 获取企业付款服务类. * @@ -74,6 +137,13 @@ public interface WxPayService { */ ProfitSharingService getProfitSharingService(); + /** + * 获取微信支付智慧商圈服务类 + * + * @return the business circle service + */ + BusinessCircleService getBusinessCircleService(); + /** * 设置企业付款服务类,允许开发者自定义实现类. * 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 61dcee09..075fed99 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 @@ -16,10 +16,7 @@ import com.github.binarywang.wxpay.config.WxPayConfig; 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; @@ -63,6 +60,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 BusinessCircleService businessCircleService = new BusinessCircleServiceImpl(this); /** * The Config. @@ -79,6 +77,11 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { return profitSharingService; } + @Override + public BusinessCircleService getBusinessCircleService() { + return this.businessCircleService; + } + @Override public RedpackService getRedpackService() { return this.redpackService; diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java new file mode 100644 index 00000000..ee0874aa --- /dev/null +++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java @@ -0,0 +1,89 @@ +package com.github.binarywang.wxpay.service.impl; + +import com.github.binarywang.wxpay.bean.businesscircle.BusinessCircleNotifyData; +import com.github.binarywang.wxpay.bean.businesscircle.PaidResult; +import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest; +import com.github.binarywang.wxpay.bean.businesscircle.RefundResult; +import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader; +import com.github.binarywang.wxpay.exception.WxPayException; +import com.github.binarywang.wxpay.service.BusinessCircleService; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.v3.util.AesUtils; +import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.util.Objects; + +/** + * 微信支付-微信支付智慧商圈service + * + * @author thinsstar + */ +@Slf4j +@RequiredArgsConstructor +public class BusinessCircleServiceImpl implements BusinessCircleService { + private static final Gson GSON = new GsonBuilder().create(); + private final WxPayService payService; + + @Override + public void notifyPoints(PointsNotifyRequest request) throws WxPayException { + String url = String.format("%s/v3/businesscircle/points/notify", this.payService.getPayBaseUrl()); + RsaCryptoUtil.encryptFields(request, this.payService.getConfig().getVerifier().getValidCertificate()); + this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request)); + } + + /** + * 校验通知签名 + * + * @param header 通知头信息 + * @param data 通知数据 + * @return true:校验通过 false:校验不通过 + */ + private boolean verifyNotifySign(SignatureHeader header, String data) { + String beforeSign = String.format("%s%n%s%n%s%n", header.getTimeStamp(), header.getNonce(), data); + return payService.getConfig().getVerifier().verify(header.getSerialNo(), + beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned()); + } + + @Override + public BusinessCircleNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException { + if (Objects.nonNull(header) && !this.verifyNotifySign(header, data)) { + throw new WxPayException("非法请求,头部信息验证失败"); + } + return GSON.fromJson(data, BusinessCircleNotifyData.class); + } + + @Override + public PaidResult decryptPaidNotifyDataResource(BusinessCircleNotifyData data) throws WxPayException { + BusinessCircleNotifyData.Resource resource = data.getResource(); + String cipherText = resource.getCipherText(); + String associatedData = resource.getAssociatedData(); + String nonce = resource.getNonce(); + String apiV3Key = this.payService.getConfig().getApiV3Key(); + try { + return GSON.fromJson(AesUtils.decryptToString(associatedData, nonce, cipherText, apiV3Key), PaidResult.class); + } catch (GeneralSecurityException | IOException e) { + throw new WxPayException("解析报文异常!", e); + } + } + + @Override + public RefundResult decryptRefundNotifyDataResource(BusinessCircleNotifyData data) throws WxPayException { + BusinessCircleNotifyData.Resource resource = data.getResource(); + String cipherText = resource.getCipherText(); + String associatedData = resource.getAssociatedData(); + String nonce = resource.getNonce(); + String apiV3Key = this.payService.getConfig().getApiV3Key(); + try { + return GSON.fromJson(AesUtils.decryptToString(associatedData, nonce, cipherText, apiV3Key), RefundResult.class); + } catch (GeneralSecurityException | 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 1703c200..87ba43df 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,31 @@ package com.github.binarywang.wxpay.service.impl; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.StandardCharsets; import javax.net.ssl.SSLContext; 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.exception.WxPayException; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import me.chanjar.weixin.common.util.json.GsonParser; import jodd.util.Base64; import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; 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.client.methods.HttpRequestBase; 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; @@ -28,9 +33,6 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; -import com.github.binarywang.wxpay.bean.WxPayApiData; -import com.github.binarywang.wxpay.exception.WxPayException; -import jodd.util.Base64; /** *

@@ -90,6 +92,155 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
     }
   }
 
+  @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();
+      //post方法有可能会没有返回值的情况
+      String responseString;
+      if (response.getEntity() == null) {
+        responseString = null;
+      } else {
+        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 = GsonParser.parse(responseString);
+        throw convertException(jsonObject);
+      }
+    } catch (Exception e) {
+      this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage());
+      throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e);
+    } finally {
+      httpPost.releaseConnection();
+    }
+
+
+  }
+
+  @Override
+  public String postV3WithWechatpaySerial(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");
+    String serialNumber = getConfig().getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase();
+    httpPost.addHeader("Wechatpay-Serial", serialNumber);
+    try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
+      //v3已经改为通过状态码判断200 204 成功
+      int statusCode = response.getStatusLine().getStatusCode();
+      String responseString = "{}";
+      HttpEntity entity = response.getEntity();
+      if (entity != null) {
+        responseString = EntityUtils.toString(entity, 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 = GsonParser.parse(responseString);
+        throw convertException(jsonObject);
+      }
+    } catch (Exception e) {
+      this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage());
+      e.printStackTrace();
+      throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e);
+    } finally {
+      httpPost.releaseConnection();
+    }
+  }
+
+  @Override
+  public String postV3(String url, HttpPost httpPost) throws WxPayException {
+    return this.requestV3(url, httpPost);
+  }
+
+  @Override
+  public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayException {
+    httpRequest.setConfig(RequestConfig.custom()
+      .setConnectionRequestTimeout(this.getConfig().getHttpConnectionTimeout())
+      .setConnectTimeout(this.getConfig().getHttpConnectionTimeout())
+      .setSocketTimeout(this.getConfig().getHttpTimeout())
+      .build());
+
+    CloseableHttpClient httpClient = this.createApiV3HttpClient();
+    try (CloseableHttpResponse response = httpClient.execute(httpRequest)) {
+      //v3已经改为通过状态码判断200 204 成功
+      int statusCode = response.getStatusLine().getStatusCode();
+      //post方法有可能会没有返回值的情况
+      String responseString;
+      if (response.getEntity() == null) {
+        responseString = null;
+      } else {
+        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 = GsonParser.parse(responseString);
+        throw convertException(jsonObject);
+      }
+    } catch (Exception e) {
+      this.log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage());
+      throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e);
+    } finally {
+      httpRequest.releaseConnection();
+    }
+  }
+
+  @Override
+  public String getV3(String url) throws WxPayException {
+    HttpGet httpGet = new HttpGet(url);
+    httpGet.addHeader("Accept", "application/json");
+    httpGet.addHeader("Content-Type", "application/json");
+    return this.requestV3(url.toString(), httpGet);
+  }
+
+  @Override
+  public InputStream downloadV3(String url) throws WxPayException {
+    CloseableHttpClient httpClient = this.createApiV3HttpClient();
+    HttpGet httpGet = new HttpGet(url);
+    httpGet.addHeader("Accept", ContentType.WILDCARD.getMimeType());
+    try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
+      //v3已经改为通过状态码判断200 204 成功
+      int statusCode = response.getStatusLine().getStatusCode();
+      if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) {
+        this.log.info("\n【请求地址】:{}\n", url);
+        return response.getEntity().getContent();
+      } else {
+        //有错误提示信息返回
+        String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+        JsonObject jsonObject = GsonParser.parse(responseString);
+        throw convertException(jsonObject);
+      }
+    } catch (Exception e) {
+      this.log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage());
+      throw (e instanceof WxPayException) ? (WxPayException) e : 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) {
     try {
       return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
@@ -145,4 +296,15 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
     httpClientBuilder.setSSLSocketFactory(connectionSocketFactory);
   }
 
+  private WxPayException convertException(JsonObject jsonObject) {
+    //todo 这里考虑使用新的适用于V3的异常
+    JsonElement codeElement = jsonObject.get("code");
+    String code = codeElement == null ? null : codeElement.getAsString();
+    String message = jsonObject.get("message").getAsString();
+    WxPayException wxPayException = new WxPayException(message);
+    wxPayException.setErrCode(code);
+    wxPayException.setErrCodeDes(message);
+    return 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 81d35614..52ce46df 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.io.InputStream;
 import java.nio.charset.StandardCharsets;
 import javax.net.ssl.SSLContext;
 
@@ -20,6 +21,8 @@ import jodd.http.net.SSLSocketHttpConnectionProvider;
 import jodd.http.net.SocketHttpConnectionProvider;
 import jodd.util.Base64;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpRequestBase;
 
 import javax.net.ssl.SSLContext;
 import java.nio.charset.StandardCharsets;
@@ -67,6 +70,36 @@ public class WxPayServiceJoddHttpImpl extends BaseWxPayServiceImpl {
     }
   }
 
+  @Override
+  public String postV3(String url, String requestStr) throws WxPayException {
+    return null;
+  }
+
+  @Override
+  public String postV3WithWechatpaySerial(String url, String requestStr) throws WxPayException {
+    return null;
+  }
+
+  @Override
+  public String postV3(String url, HttpPost httpPost) throws WxPayException {
+    return null;
+  }
+
+  @Override
+  public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayException {
+    return null;
+  }
+
+  @Override
+  public String getV3(String url) throws WxPayException {
+    return null;
+  }
+
+  @Override
+  public InputStream downloadV3(String 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 00000000..e8860b6f
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java
@@ -0,0 +1,12 @@
+package com.github.binarywang.wxpay.v3;
+
+import org.apache.http.client.methods.HttpRequestWrapper;
+
+import java.io.IOException;
+
+public interface Credentials {
+
+  String getSchema();
+
+  String getToken(HttpRequestWrapper 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 00000000..a2235246
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
@@ -0,0 +1,89 @@
+package com.github.binarywang.wxpay.v3;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.HttpException;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpExecutionAware;
+import org.apache.http.client.methods.HttpRequestWrapper;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.entity.BufferedHttpEntity;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.execchain.ClientExecChain;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+
+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(HttpRequestWrapper request) throws IOException {
+    if (request instanceof HttpEntityEnclosingRequest) {
+      HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
+      if (entity != null) {
+        ((HttpEntityEnclosingRequest) request).setEntity(new BufferedHttpEntity(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 {
+    // 上传类不需要消耗两次故不做转换
+    if (!(request.getOriginal() instanceof WechatPayUploadHttpPost)) {
+      convertToRepeatableRequestEntity(request);
+    }
+    // 添加认证信息
+    request.addHeader("Authorization",
+      credentials.getSchema() + " " + credentials.getToken(request));
+
+    // 执行
+    CloseableHttpResponse response = mainExec.execute(route, request, 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/SpecEncrypt.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SpecEncrypt.java
new file mode 100644
index 00000000..4f1eb9e5
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SpecEncrypt.java
@@ -0,0 +1,16 @@
+package com.github.binarywang.wxpay.v3;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 敏感信息字段
+ * @author zhouyognshen
+ **/
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface SpecEncrypt {
+
+}
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 00000000..e5ce69dd
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java
@@ -0,0 +1,9 @@
+package com.github.binarywang.wxpay.v3;
+
+import org.apache.http.client.methods.CloseableHttpResponse;
+
+import java.io.IOException;
+
+public interface Validator {
+  boolean validate(CloseableHttpResponse response) throws IOException;
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
new file mode 100644
index 00000000..df0ee4e2
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
@@ -0,0 +1,76 @@
+package com.github.binarywang.wxpay.v3;
+
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.HttpMultipartMode;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URLConnection;
+
+public class WechatPayUploadHttpPost extends HttpPost {
+
+  private String meta;
+
+  private WechatPayUploadHttpPost(URI uri, String meta) {
+    super(uri);
+
+    this.meta = meta;
+  }
+
+  public String getMeta() {
+    return meta;
+  }
+
+  public static class Builder {
+
+    private String fileName;
+    private String fileSha256;
+    private InputStream fileInputStream;
+    private ContentType fileContentType;
+    private URI uri;
+
+    public Builder(URI uri) {
+      this.uri = uri;
+    }
+
+    public Builder withImage(String fileName, String fileSha256, InputStream inputStream) {
+      this.fileName = fileName;
+      this.fileSha256 = fileSha256;
+      this.fileInputStream = inputStream;
+
+      String mimeType = URLConnection.guessContentTypeFromName(fileName);
+      if (mimeType == null) {
+        // guess this is a video uploading
+        this.fileContentType = ContentType.APPLICATION_OCTET_STREAM;
+      } else {
+        this.fileContentType = ContentType.create(mimeType);
+      }
+      return this;
+    }
+
+    public WechatPayUploadHttpPost build() {
+      if (fileName == null || fileSha256 == null || fileInputStream == null) {
+        throw new IllegalArgumentException("缺少待上传图片文件信息");
+      }
+
+      if (uri == null) {
+        throw new IllegalArgumentException("缺少上传图片接口URL");
+      }
+
+      String meta = String.format("{\"filename\":\"%s\",\"sha256\":\"%s\"}", fileName, fileSha256);
+      WechatPayUploadHttpPost request = new WechatPayUploadHttpPost(uri, meta);
+
+      MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create();
+      entityBuilder.setMode(HttpMultipartMode.RFC6532)
+        .addBinaryBody("file", fileInputStream, fileContentType, fileName)
+        .addTextBody("meta", meta, ContentType.APPLICATION_JSON);
+
+      request.setEntity(entityBuilder.build());
+      request.addHeader("Accept", ContentType.APPLICATION_JSON.toString());
+
+      return request;
+    }
+  }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
new file mode 100644
index 00000000..c67d3daa
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
@@ -0,0 +1,75 @@
+package com.github.binarywang.wxpay.v3;
+
+
+import com.github.binarywang.wxpay.v3.auth.CertificatesVerifier;
+import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner;
+import com.github.binarywang.wxpay.v3.auth.WxPayCredentials;
+import com.github.binarywang.wxpay.v3.auth.WxPayValidator;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.execchain.ClientExecChain;
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+public class WxPayV3HttpClientBuilder 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 WxPayV3HttpClientBuilder() {
+    super();
+
+    String userAgent = String.format(
+        "WechatPay-Apache-HttpClient/%s (%s) Java/%s",
+        getClass().getPackage().getImplementationVersion(),
+      OS,
+        VERSION == null ? "Unknown" : VERSION);
+    setUserAgent(userAgent);
+  }
+
+  public static WxPayV3HttpClientBuilder create() {
+    return new WxPayV3HttpClientBuilder();
+  }
+
+  public WxPayV3HttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) {
+    this.credentials =
+        new WxPayCredentials(merchantId, new PrivateKeySigner(serialNo, privateKey));
+    return this;
+  }
+
+  public WxPayV3HttpClientBuilder withCredentials(Credentials credentials) {
+    this.credentials = credentials;
+    return this;
+  }
+
+  public WxPayV3HttpClientBuilder withWechatpay(List certificates) {
+    this.validator = new WxPayValidator(new CertificatesVerifier(certificates));
+    return this;
+  }
+
+  public WxPayV3HttpClientBuilder 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 00000000..428d3f62
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
@@ -0,0 +1,198 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import com.github.binarywang.wxpay.v3.Credentials;
+import com.github.binarywang.wxpay.v3.Validator;
+import com.github.binarywang.wxpay.v3.WxPayV3HttpClientBuilder;
+import com.github.binarywang.wxpay.v3.util.AesUtils;
+import com.github.binarywang.wxpay.v3.util.PemUtils;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxRuntimeException;
+import me.chanjar.weixin.common.util.json.GsonParser;
+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 java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 在原有CertificatesVerifier基础上,增加自动更新证书功能
+ *
+ * @author doger.wang
+ */
+@Slf4j
+public class AutoUpdateCertificatesVerifier implements Verifier {
+  /**
+   * 证书下载地址
+   */
+  private static final String CERT_DOWNLOAD_PATH = "https://api.mch.weixin.qq.com/v3/certificates";
+
+  /**
+   * 上次更新时间
+   */
+  private volatile Instant instant;
+
+  /**
+   * 证书更新间隔时间,单位为分钟
+   */
+  private final int minutesInterval;
+
+  private CertificatesVerifier verifier;
+
+  private final Credentials credentials;
+
+  private final byte[] apiV3Key;
+
+  private final ReentrantLock lock = new ReentrantLock();
+
+  /**
+   * 时间间隔枚举,支持一小时、六小时以及十二小时
+   */
+  @Getter
+  @RequiredArgsConstructor
+  public enum TimeInterval {
+    /**
+     * 一小时
+     */
+    OneHour(60),
+    /**
+     * 六小时
+     */
+    SixHours(60 * 6),
+    /**
+     * 十二小时
+     */
+    TwelveHours(60 * 12);
+
+    private final int 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 WxRuntimeException(e);
+    }
+  }
+
+  @Override
+  public boolean verify(String serialNumber, byte[] message, String signature) {
+    checkAndAutoUpdateCert();
+    return verifier.verify(serialNumber, message, signature);
+  }
+
+  /**
+   * 检查证书是否在有效期内,如果不在有效期内则进行更新
+   */
+  private void checkAndAutoUpdateCert() {
+    if (instant == null || instant.plus(minutesInterval, ChronoUnit.MINUTES).compareTo(Instant.now()) >= 0) {
+      if (lock.tryLock()) {
+        try {
+          autoUpdateCert();
+          //更新时间
+          instant = Instant.now();
+        } catch (GeneralSecurityException | IOException e) {
+          log.warn("Auto update cert failed, exception = " + e);
+        } finally {
+          lock.unlock();
+        }
+      }
+    }
+  }
+
+  private void autoUpdateCert() throws IOException, GeneralSecurityException {
+    CloseableHttpClient httpClient = WxPayV3HttpClientBuilder.create()
+      .withCredentials(credentials)
+      .withValidator(verifier == null ? new Validator() {
+        @Override
+        public boolean validate(CloseableHttpResponse response) throws IOException {
+          return true;
+        }
+      } : new WxPayValidator(verifier))
+      .build();
+
+    HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH);
+    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 aesUtils = new AesUtils(apiV3Key);
+    final JsonObject json = GsonParser.parse(body);
+    final JsonArray dataNode = json.getAsJsonArray("data");
+    if (dataNode == null) {
+      return Collections.emptyList();
+    }
+
+    List newCertList = new ArrayList<>();
+    for (int i = 0, count = dataNode.size(); i < count; i++) {
+      final JsonObject encryptCertificateNode = ((JsonObject) dataNode.get(i)).getAsJsonObject("encrypt_certificate");
+      //解密
+      String cert = aesUtils.decryptToString(
+        encryptCertificateNode.get("associated_data").toString().replaceAll("\"", "")
+          .getBytes(StandardCharsets.UTF_8),
+        encryptCertificateNode.get("nonce").toString().replaceAll("\"", "")
+          .getBytes(StandardCharsets.UTF_8),
+        encryptCertificateNode.get("ciphertext").toString().replaceAll("\"", ""));
+
+      X509Certificate x509Cert = PemUtils
+        .loadCertificate(new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8)));
+      try {
+        x509Cert.checkValidity();
+      } catch (CertificateExpiredException | CertificateNotYetValidException e) {
+        continue;
+      }
+      newCertList.add(x509Cert);
+    }
+
+    return newCertList;
+  }
+
+  @Override
+  public X509Certificate getValidCertificate() {
+    checkAndAutoUpdateCert();
+    return verifier.getValidCertificate();
+  }
+
+}
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 00000000..9ca8b5b8
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java
@@ -0,0 +1,65 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import me.chanjar.weixin.common.error.WxRuntimeException;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+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 WxRuntimeException("当前Java环境不支持SHA256withRSA", e);
+    } catch (SignatureException e) {
+      throw new WxRuntimeException("签名验证过程发生了错误", e);
+    } catch (InvalidKeyException e) {
+      throw new WxRuntimeException("无效的证书", 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);
+  }
+
+
+  @Override
+  public X509Certificate getValidCertificate() {
+    for (X509Certificate x509Cert : certificates.values()) {
+      try {
+        x509Cert.checkValidity();
+
+        return x509Cert;
+      } catch (CertificateExpiredException | CertificateNotYetValidException e) {
+        continue;
+      }
+    }
+
+    throw new NoSuchElementException("没有有效的微信支付平台证书");
+  }
+
+}
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 00000000..c337d48d
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java
@@ -0,0 +1,35 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import me.chanjar.weixin.common.error.WxRuntimeException;
+
+import java.security.*;
+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 WxRuntimeException("当前Java环境不支持SHA256withRSA", e);
+    } catch (SignatureException e) {
+      throw new WxRuntimeException("签名计算失败", e);
+    } catch (InvalidKeyException e) {
+      throw new WxRuntimeException("无效的私钥", 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 00000000..7255a1b4
--- /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 00000000..49f92e2f
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java
@@ -0,0 +1,10 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import java.security.cert.X509Certificate;
+
+public interface Verifier {
+  boolean verify(String serialNumber, byte[] message, String signature);
+
+
+  X509Certificate getValidCertificate();
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java
new file mode 100644
index 00000000..80eea8f6
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java
@@ -0,0 +1,93 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+
+import com.github.binarywang.wxpay.v3.Credentials;
+import com.github.binarywang.wxpay.v3.WechatPayUploadHttpPost;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.HttpEntityEnclosingRequest;
+import org.apache.http.client.methods.HttpRequestWrapper;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+import java.net.URI;
+import java.nio.charset.StandardCharsets;
+import java.security.SecureRandom;
+
+@Slf4j
+public class WxPayCredentials implements Credentials {
+  private static final String SYMBOLS =
+      "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+  private static final SecureRandom RANDOM = new SecureRandom();
+  protected String merchantId;
+  protected Signer signer;
+
+  public WxPayCredentials(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(HttpRequestWrapper 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(StandardCharsets.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, HttpRequestWrapper 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.getOriginal() instanceof WechatPayUploadHttpPost) {
+      body = ((WechatPayUploadHttpPost) request.getOriginal()).getMeta();
+    } else if (request instanceof HttpEntityEnclosingRequest) {
+      body = EntityUtils.toString(((HttpEntityEnclosingRequest) 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/WxPayValidator.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayValidator.java
new file mode 100644
index 00000000..e14d8b5b
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayValidator.java
@@ -0,0 +1,56 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+
+import com.github.binarywang.wxpay.v3.Validator;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.entity.ContentType;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+
+@Slf4j
+public class WxPayValidator implements Validator {
+  private Verifier verifier;
+
+  public WxPayValidator(Verifier verifier) {
+    this.verifier = verifier;
+  }
+
+  @Override
+  public final boolean validate(CloseableHttpResponse response) throws IOException {
+    if (!ContentType.APPLICATION_JSON.getMimeType().equals(ContentType.parse(String.valueOf(response.getFirstHeader("Content-Type").getValue())).getMimeType())) {
+      return true;
+    }
+    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 00000000..2c8c4025
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java
@@ -0,0 +1,134 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.io.BaseEncoding;
+import org.apache.commons.lang3.StringUtils;
+
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+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;
+
+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 static byte[] decryptToByte(byte[] nonce, byte[] cipherData, byte[] key)
+    throws GeneralSecurityException {
+    return decryptToByte(null, nonce, cipherData, key);
+  }
+
+  public static byte[] decryptToByte(byte[] associatedData, byte[] nonce, byte[] cipherData, byte[] key)
+    throws GeneralSecurityException {
+    try {
+      Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+
+      SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES");
+      GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
+
+      cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, spec);
+      if (associatedData != null) {
+        cipher.updateAAD(associatedData);
+      }
+      return cipher.doFinal(cipherData);
+    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+      throw new IllegalStateException(e);
+    } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  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(BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(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 00000000..ab29879e
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java
@@ -0,0 +1,58 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import me.chanjar.weixin.common.error.WxRuntimeException;
+
+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.*;
+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 WxRuntimeException("当前Java环境不支持RSA", e);
+    } catch (InvalidKeySpecException e) {
+      throw new WxRuntimeException("无效的密钥格式");
+    } catch (IOException e) {
+      throw new WxRuntimeException("无效的密钥");
+    }
+  }
+
+  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 WxRuntimeException("证书已过期", e);
+    } catch (CertificateNotYetValidException e) {
+      throw new WxRuntimeException("证书尚未生效", e);
+    } catch (CertificateException e) {
+      throw new WxRuntimeException("无效的证书", e);
+    }
+  }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
new file mode 100644
index 00000000..29530374
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
@@ -0,0 +1,109 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.v3.SpecEncrypt;
+import me.chanjar.weixin.common.error.WxRuntimeException;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.Collection;
+
+/**
+ * 微信支付敏感信息加密
+ * 文档见: https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi
+ *
+ * @author zhouyongshen
+ **/
+public class RsaCryptoUtil {
+
+
+  static String JAVA_LANG_STRING = "java.lang.String";
+
+  public static void encryptFields(Object encryptObject, X509Certificate certificate) throws WxPayException {
+    try {
+      encryptField(encryptObject, certificate);
+    } catch (Exception e) {
+      throw new WxPayException("敏感信息加密失败", e);
+    }
+  }
+
+  private static void encryptField(Object encryptObject, X509Certificate certificate) throws IllegalAccessException, IllegalBlockSizeException {
+    Class infoClass = encryptObject.getClass();
+    Field[] infoFieldArray = infoClass.getDeclaredFields();
+    for (Field field : infoFieldArray) {
+      if (field.isAnnotationPresent(SpecEncrypt.class)) {
+        //字段使用了@SpecEncrypt进行标识
+        if (field.getType().getTypeName().equals(JAVA_LANG_STRING)) {
+          field.setAccessible(true);
+          Object oldValue = field.get(encryptObject);
+          if (oldValue != null) {
+            String oldStr = (String) oldValue;
+            if (!oldStr.trim().equals("'")) {
+              field.set(encryptObject, encryptOAEP(oldStr, certificate));
+            }
+          }
+        } else {
+          field.setAccessible(true);
+          Object obj = field.get(encryptObject);
+          if (obj == null) {
+            continue;
+          }
+          if (obj instanceof Collection) {
+            Collection collection = (Collection) obj;
+            for (Object o : collection) {
+              if (o != null) {
+                encryptField(o, certificate);
+              }
+            }
+          } else {
+            encryptField(obj, certificate);
+          }
+        }
+      }
+    }
+  }
+
+  public static String encryptOAEP(String message, X509Certificate certificate)
+    throws IllegalBlockSizeException {
+    try {
+      Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+      cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey());
+
+      byte[] data = message.getBytes(StandardCharsets.UTF_8);
+      byte[] ciphertext = cipher.doFinal(data);
+      return Base64.getEncoder().encodeToString(ciphertext);
+    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+      throw new WxRuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException("无效的证书", e);
+    } catch (IllegalBlockSizeException | BadPaddingException e) {
+      throw new IllegalBlockSizeException("加密原串的长度不能超过214字节");
+    }
+  }
+
+  public static String decryptOAEP(String ciphertext, PrivateKey privateKey)
+    throws BadPaddingException {
+    try {
+      Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding");
+      cipher.init(Cipher.DECRYPT_MODE, privateKey);
+
+      byte[] data = Base64.getDecoder().decode(ciphertext);
+      return new String(cipher.doFinal(data), StandardCharsets.UTF_8);
+    } catch (NoSuchPaddingException | NoSuchAlgorithmException e) {
+      throw new WxRuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
+    } catch (InvalidKeyException e) {
+      throw new IllegalArgumentException("无效的私钥", e);
+    } catch (BadPaddingException | IllegalBlockSizeException e) {
+      throw new BadPaddingException("解密失败");
+    }
+  }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java
new file mode 100644
index 00000000..fff68ce2
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java
@@ -0,0 +1,50 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import me.chanjar.weixin.common.error.WxRuntimeException;
+
+import java.security.*;
+import java.util.Base64;
+import java.util.Random;
+
+public class SignUtils {
+
+  public static String sign(String string, PrivateKey privateKey) {
+    try {
+      Signature sign = Signature.getInstance("SHA256withRSA");
+      sign.initSign(privateKey);
+      sign.update(string.getBytes());
+
+      return Base64.getEncoder().encodeToString(sign.sign());
+    } catch (NoSuchAlgorithmException e) {
+      throw new WxRuntimeException("当前Java环境不支持SHA256withRSA", e);
+    } catch (SignatureException e) {
+      throw new WxRuntimeException("签名计算失败", e);
+    } catch (InvalidKeyException e) {
+      throw new WxRuntimeException("无效的私钥", e);
+    }
+  }
+
+  /**
+   * 随机生成32位字符串.
+   */
+  public static String genRandomStr() {
+    return genRandomStr(32);
+  }
+
+  /**
+   * 生成随机字符串
+   *
+   * @param length 字符串长度
+   * @return
+   */
+  public static String genRandomStr(int length) {
+    String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+    Random random = new Random();
+    StringBuilder sb = new StringBuilder();
+    for (int i = 0; i < length; i++) {
+      int number = random.nextInt(base.length());
+      sb.append(base.charAt(number));
+    }
+    return sb.toString();
+  }
+}
diff --git a/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java
new file mode 100644
index 00000000..d07392f1
--- /dev/null
+++ b/weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java
@@ -0,0 +1,79 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.github.binarywang.wxpay.bean.businesscircle.BusinessCircleNotifyData;
+import com.github.binarywang.wxpay.bean.businesscircle.PaidResult;
+import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest;
+import com.github.binarywang.wxpay.bean.businesscircle.RefundResult;
+import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.testbase.ApiTestModule;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * 
+ *  智慧商圈测试类
+ * 
+ * + * @author thinsstar + */ +@Slf4j +@Test +@Guice(modules = ApiTestModule.class) +public class BusinessCircleServiceImplTest { + + @Inject + private WxPayService wxPayService; + + private static final Gson GSON = new GsonBuilder().create(); + + @Test + public void testNotifyPointsV3() throws WxPayException { + PointsNotifyRequest request = new PointsNotifyRequest(); + String subMchid = "商圈商户ID"; + String transactionId = "微信订单号"; + String appId = "公众号id"; + String openId = "微信openid"; + request.setSubMchid(subMchid); + request.setTransactionId(transactionId); + request.setAppid(appId); + request.setOpenid(openId); + request.setEarnPoints(true); + request.setIncreasedPoints(10); + request.setPointsUpdateTime("2021-03-03T13:29:35.120+08:00"); + wxPayService.getBusinessCircleService().notifyPoints(request); + } + + @Test + public void testDecryptPaidNotifyDataResource() throws WxPayException { + SignatureHeader header = new SignatureHeader(); + header.setSerialNo("Wechatpay-Serial"); + header.setTimeStamp("Wechatpay-Timestamp"); + header.setNonce("Wechatpay-Nonce"); + header.setSigned("Wechatpay-Signature"); + String data = "body"; + BusinessCircleNotifyData notifyData = wxPayService.getBusinessCircleService().parseNotifyData(data, header); + PaidResult result = wxPayService.getBusinessCircleService().decryptPaidNotifyDataResource(notifyData); + + log.info("result: {}", GSON.toJson(result)); + } + + @Test + public void testDecryptRefundNotifyDataResource() throws WxPayException { + SignatureHeader header = new SignatureHeader(); + header.setSerialNo("Wechatpay-Serial"); + header.setTimeStamp("Wechatpay-Timestamp"); + header.setNonce("Wechatpay-Nonce"); + header.setSigned("Wechatpay-Signature"); + String data = "body"; + BusinessCircleNotifyData notifyData = wxPayService.getBusinessCircleService().parseNotifyData(data, header); + RefundResult result = wxPayService.getBusinessCircleService().decryptRefundNotifyDataResource(notifyData); + + log.info("result: {}", GSON.toJson(result)); + } +}