certificates = new ArrayList<>();
+ certificates.add(x509Certificate);
+ builder.withMerchant(mchId, certSerialNo, merchantPrivateKey);
+ builder.withWechatpay(certificates);
+ AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
+ new WechatPay2Credentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
+ apiv3Key.getBytes("utf-8"));
+ builder.withValidator(new WechatPay2Validator(verifier));
+ httpClient = builder.build();
+ this.apiv3HttpClient =httpClient;
+ } catch (Exception e) {
+ throw new WxPayException("v3请求构造异常", e);
+ }
+ return httpClient;
+
+
+ }
+ }
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java
new file mode 100644
index 000000000..e85df7f88
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java
@@ -0,0 +1,164 @@
+package com.github.binarywang.wxpay.service;
+
+import com.github.binarywang.wxpay.bean.payscore.NotifyData;
+import com.github.binarywang.wxpay.bean.payscore.WxPayScoreRequest;
+import com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult;
+import com.github.binarywang.wxpay.exception.WxPayException;
+
+import java.net.URISyntaxException;
+
+/**
+ *
+ * 支付分相关服务类.
+ * 微信支付分是对个人的身份特质、支付行为、使用历史等情况的综合计算分值,旨在为用户提供更简单便捷的生活方式。
+ * 微信用户可以在具体应用场景中,开通微信支付分。开通后,用户可以在【微信—>钱包—>支付分】中查看分数和使用记录。(即需在应用场景中使用过一次,钱包才会出现支付分入口)
+ *
+ * Created by doger.wang on 2020/05/12.
+ *
+ *
+ *
+ */
+public interface PayScoreService {
+
+
+
+ /**
+ *
+ * 支付分创建订单API.
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter1_1.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_1.shtml
+ *
+ *
+ * @param request 请求对象
+ * @return WxPayScoreResult
+ * @throws WxPayException the wx pay exception
+ */
+ WxPayScoreResult createServiceOrder(WxPayScoreRequest request) throws WxPayException;
+
+
+
+ /**
+ *
+ * 支付分查询订单API.
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_2.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_2.shtml
+ *
+ *
+ * @Author doger.wang
+ * @Description
+ * @Date 2020/5/14 15:40
+ * @Param out_order_no, query_id选填一个
+ * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult
+ **/
+ WxPayScoreResult queryServiceOrder( String out_order_no,String query_id ) throws WxPayException, URISyntaxException;
+
+
+ /**
+ *
+ * 支付分取消订单API.
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_3.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_3.shtml
+ *
+ *
+ * @Author doger.wang
+ * @Description
+ * @Date 2020/5/14 15:40
+ * @Param out_order_no reason
+ * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult
+ **/
+ WxPayScoreResult cancelServiceOrder(String out_order_no, String reason) throws WxPayException;
+
+ /**
+ *
+ * 支付分修改订单金额API.
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_4.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_4.shtml
+ *
+ *
+ * @Author doger.wang
+ * @Description
+ * @Date 2020/5/14 15:40
+ * @Param WxPayScoreRequest
+ * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult
+ **/
+ WxPayScoreResult modifyServiceOrder(WxPayScoreRequest request) throws WxPayException;
+
+
+ /**
+ *
+ * 支付分完结订单API.
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_5.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_5.shtml
+ *
+ *
+ * @Author doger.wang
+ * @Description
+ * @Date 2020/5/14 15:40
+ * @Param WxPayScoreRequest
+ * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult
+ **/
+ WxPayScoreResult completeServiceOrder(WxPayScoreRequest request) throws WxPayException;
+
+
+ /**
+ *
+ * 支付分订单收款API.
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_6.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_6.shtml
+ *
+ *
+ * @Author doger.wang
+ * @Description
+ * @Date 2020/5/14 15:40
+ * @Param out_order_no
+ * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult
+ **/
+ WxPayScoreResult payServiceOrder(String out_order_no) throws WxPayException;
+
+
+ /**
+ *
+ * 支付分订单收款API.
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_7.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter3_7.shtml
+ *
+ *
+ * @Author doger.wang
+ * @Description
+ * @Date 2020/5/14 15:40
+ * @Param WxPayScoreRequest
+ * @return com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult
+ **/
+ WxPayScoreResult syncServiceOrder(WxPayScoreRequest request) throws WxPayException;
+
+
+ /**
+ *
+ * 支付分回调内容解密方法
+ * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter5_2.shtml
+ * 接口链接:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/payscore/chapter5_2.shtml
+ *
+ *
+ * @param NotifyData 请求对象
+ * @return WxPayScoreResult
+ */
+ WxPayScoreResult decryptNotifyData(NotifyData data) throws WxPayException;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
index 109ce34b9..ba9de0ffd 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
@@ -12,6 +12,7 @@ import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import java.io.File;
+import java.net.URI;
import java.util.Date;
import java.util.Map;
@@ -54,6 +55,27 @@ public interface WxPayService {
*/
String post(String url, String requestStr, boolean useKey) throws WxPayException;
+ /**
+ * 发送post请求,得到响应字符串.
+ *
+ * @param url 请求地址
+ * @param requestStr 请求信息
+ * @return 返回请求结果字符串 string
+ * @throws WxPayException the wx pay exception
+ */
+ String postV3(String url, String requestStr) throws WxPayException;
+
+
+ /**
+ * 发送get V3请求,得到响应字符串.
+ *
+ * @param url 请求地址
+ * @param param 请求信息
+ * @return 返回请求结果字符串 string
+ * @throws WxPayException the wx pay exception
+ */
+ String getV3(URI url) throws WxPayException;
+
/**
* 获取企业付款服务类.
*
@@ -75,6 +97,14 @@ public interface WxPayService {
*/
ProfitSharingService getProfitSharingService();
+
+ /**
+ * 获取支付分服务类.
+ *
+ * @return the ent pay service
+ */
+ PayScoreService getPayScoreService();
+
/**
* 设置企业付款服务类,允许开发者自定义实现类.
*
@@ -729,4 +759,5 @@ public interface WxPayService {
*/
WxPayFacepayResult facepay(WxPayFacepayRequest request) throws WxPayException;
+
}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
index 1f0e52838..6d6dc748c 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
@@ -17,10 +17,7 @@ import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.constant.WxPayConstants.SignType;
import com.github.binarywang.wxpay.constant.WxPayConstants.TradeType;
import com.github.binarywang.wxpay.exception.WxPayException;
-import com.github.binarywang.wxpay.service.EntPayService;
-import com.github.binarywang.wxpay.service.ProfitSharingService;
-import com.github.binarywang.wxpay.service.RedpackService;
-import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.*;
import com.github.binarywang.wxpay.util.SignUtils;
import com.github.binarywang.wxpay.util.XmlConfig;
import com.google.common.base.Joiner;
@@ -64,6 +61,7 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
private EntPayService entPayService = new EntPayServiceImpl(this);
private ProfitSharingService profitSharingService = new ProfitSharingServiceImpl(this);
private RedpackService redpackService = new RedpackServiceImpl(this);
+ private PayScoreService payScoreService = new PayScoreServiceImpl(this);
/**
* The Config.
@@ -80,6 +78,11 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
return profitSharingService;
}
+ @Override
+ public PayScoreService getPayScoreService() {
+ return payScoreService;
+ }
+
@Override
public RedpackService getRedpackService() {
return this.redpackService;
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java
new file mode 100644
index 000000000..2756a712c
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java
@@ -0,0 +1,193 @@
+package com.github.binarywang.wxpay.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.github.binarywang.wxpay.bean.payscore.NotifyData;
+import com.github.binarywang.wxpay.bean.payscore.WxPayScoreRequest;
+import com.github.binarywang.wxpay.bean.payscore.WxPayScoreResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.exception.WxPayException;
+import com.github.binarywang.wxpay.service.PayScoreService;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.v3.util.AesUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.utils.URIBuilder;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author doger.wang
+ * @date 2020/5/14 9:43
+ */
+
+public class PayScoreServiceImpl implements PayScoreService {
+ private WxPayService payService;
+
+ public PayScoreServiceImpl(WxPayService payService) {
+ this.payService = payService;
+ }
+
+
+ @Override
+ public WxPayScoreResult createServiceOrder(WxPayScoreRequest request) throws WxPayException {
+ boolean need_user_confirm = request.isNeed_user_confirm();
+ WxPayConfig config = this.payService.getConfig();
+ String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder";
+ request.setAppid(config.getAppId());
+ request.setService_id(config.getServiceId());
+ request.setNotify_url(config.getPayScoreNotifyUrl());
+ String result = payService.postV3(url, JSONObject.toJSONString(request));
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class);
+
+ //补充算一下签名给小程序跳转用
+ String currentTimeMillis = System.currentTimeMillis() + "";
+ Map signMap = new HashMap<>();
+ signMap.put("mch_id", config.getMchId());
+ if (need_user_confirm){
+ signMap.put("package", wxPayScoreCreateResult.getPackageX());
+ }else {
+ signMap.put("service_id", config.getServiceId());
+ signMap.put("out_order_no", request.getOut_order_no());
+ }
+ signMap.put("timestamp", currentTimeMillis);
+ signMap.put("nonce_str", currentTimeMillis);
+ signMap.put("sign_type", "HMAC-SHA256");
+ String sign = AesUtils.createSign(signMap, config.getMchKey());
+ signMap.put("sign", sign);
+ wxPayScoreCreateResult.setPayScoreSignInfo(signMap);
+ return wxPayScoreCreateResult;
+ }
+
+ @Override
+ public WxPayScoreResult queryServiceOrder(String out_order_no, String query_id) throws WxPayException, URISyntaxException {
+ WxPayConfig config = this.payService.getConfig();
+ String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder";
+ URIBuilder uriBuilder = new URIBuilder(url);
+ if (StringUtils.isAllEmpty(out_order_no,query_id) || !StringUtils.isAnyEmpty(out_order_no,query_id)){
+ throw new WxPayException("out_order_no,query_id不允许都填写或都不填写");
+ }
+ if (StringUtils.isNotEmpty(out_order_no)){
+ uriBuilder.setParameter("out_order_no", out_order_no);
+ }
+ if (StringUtils.isNotEmpty(query_id)){
+ uriBuilder.setParameter("query_id", query_id);
+ }
+ uriBuilder.setParameter("service_id", config.getServiceId());
+ uriBuilder.setParameter("appid", config.getAppId());
+ URI build = uriBuilder.build();
+ String result = payService.getV3(build);
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class);
+ //补充一下加密跳转信息
+/* String currentTimeMillis = System.currentTimeMillis() + "";
+ Map signMap = new HashMap();
+ signMap.put("mch_id", config.getMchId());
+ signMap.put("service_id", config.getServiceId());
+ signMap.put("out_order_no", out_order_no);
+ signMap.put("timestamp", currentTimeMillis);
+ signMap.put("nonce_str", currentTimeMillis);
+ signMap.put("sign_type", "HMAC-SHA256");
+ String sign = AesUtil.createSign(signMap, config.getMchKey());
+ signMap.put("sign", sign);
+ wxPayScoreCreateResult.setPayScoreSignInfo(signMap);*/
+ return wxPayScoreCreateResult;
+
+ }
+
+
+
+ @Override
+ public WxPayScoreResult cancelServiceOrder(String out_order_no, String reason) throws WxPayException {
+ WxPayConfig config = this.payService.getConfig();
+ String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/cancel";
+ HashMap map = new HashMap<>();
+ map.put("appid",config.getAppId());
+ map.put("service_id",config.getServiceId());
+ map.put("reason",reason);
+ String result = payService.postV3(url, JSONObject.toJSONString(map));
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class);
+ return wxPayScoreCreateResult;
+
+ }
+
+ @Override
+ public WxPayScoreResult modifyServiceOrder(WxPayScoreRequest request) throws WxPayException {
+ WxPayConfig config = this.payService.getConfig();
+ String out_order_no = request.getOut_order_no();
+ String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/modify";
+ request.setAppid(config.getAppId());
+ request.setService_id(config.getServiceId());
+ request.setOut_order_no(null);
+ //request.setNotify_url(config.getPayScoreNotifyUrl());
+ String result = payService.postV3(url, JSONObject.toJSONString(request));
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class);
+ return wxPayScoreCreateResult;
+
+
+ }
+
+ @Override
+ public WxPayScoreResult completeServiceOrder(WxPayScoreRequest request) throws WxPayException {
+ WxPayConfig config = this.payService.getConfig();
+ String out_order_no = request.getOut_order_no();
+ String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/complete";
+ request.setAppid(config.getAppId());
+ request.setService_id(config.getServiceId());
+ //request.setNotify_url(config.getPayScoreNotifyUrl());
+ request.setOut_order_no(null);
+ String result = payService.postV3(url, JSONObject.toJSONString(request));
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class);
+ return wxPayScoreCreateResult;
+
+ }
+
+ @Override
+ public WxPayScoreResult payServiceOrder(String out_order_no) throws WxPayException {
+ WxPayConfig config = this.payService.getConfig();
+ String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/pay";
+ HashMap map = new HashMap<>();
+ map.put("appid",config.getAppId());
+ map.put("service_id",config.getServiceId());
+ String result = payService.postV3(url, JSONObject.toJSONString(map));
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class);
+ return wxPayScoreCreateResult;
+
+ }
+
+ @Override
+ public WxPayScoreResult syncServiceOrder(WxPayScoreRequest request) throws WxPayException {
+ WxPayConfig config = this.payService.getConfig();
+ String out_order_no = request.getOut_order_no();
+ String url = this.payService.getPayBaseUrl() + "/v3/payscore/serviceorder/"+out_order_no+"/sync";
+ request.setAppid(config.getAppId());
+ request.setService_id(config.getServiceId());
+ request.setOut_order_no(null);
+ //request.setNotify_url(config.getPayScoreNotifyUrl());
+ String result = payService.postV3(url, JSONObject.toJSONString(request));
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(result, WxPayScoreResult.class);
+ return wxPayScoreCreateResult;
+
+ }
+
+ @Override
+ public WxPayScoreResult decryptNotifyData(NotifyData data) throws WxPayException{
+ NotifyData.Resource resource = data.getResource();
+ String ciphertext = resource.getCiphertext();
+ String associated_data = resource.getAssociated_data();
+ String nonce = resource.getNonce();
+ String apiv3Key = this.payService.getConfig().getApiv3Key();
+ try {
+ String s = AesUtils.decryptToString(associated_data, nonce, ciphertext, apiv3Key);
+ WxPayScoreResult wxPayScoreCreateResult = JSONObject.parseObject(s, WxPayScoreResult.class);
+ return wxPayScoreCreateResult;
+ } catch (GeneralSecurityException e) {
+ throw new WxPayException("解析报文异常",e);
+ } catch (IOException e) {
+ throw new WxPayException("解析报文异常",e);
+ }
+
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
index 1703c200f..065a3fc08 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
@@ -1,26 +1,33 @@
package com.github.binarywang.wxpay.service.impl;
+import java.io.IOException;
import java.io.UnsupportedEncodingException;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLContext;
+import com.alibaba.fastjson.JSONObject;
import com.github.binarywang.wxpay.bean.WxPayApiData;
import com.github.binarywang.wxpay.bean.request.WxPayQueryCommentRequest;
import com.github.binarywang.wxpay.bean.request.WxPayRedpackQueryRequest;
import com.github.binarywang.wxpay.bean.result.WxPayCommonResult;
import com.github.binarywang.wxpay.bean.result.WxPayRedpackQueryResult;
+import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import jodd.util.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpHost;
+import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
@@ -90,14 +97,81 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
}
}
- private StringEntity createEntry(String requestStr) {
- try {
- return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
- } catch (UnsupportedEncodingException e) {
- //cannot happen
- this.log.error(e.getMessage(), e);
- return null;
+ @Override
+ public String postV3(String url, String requestStr) throws WxPayException {
+ CloseableHttpClient httpClient = this.createApiV3HttpClient();
+ HttpPost httpPost = this.createHttpPost(url, requestStr);
+ httpPost.addHeader("Accept", "application/json");
+ httpPost.addHeader("Content-Type", "application/json");
+ try (CloseableHttpResponse response = httpClient.execute(httpPost)){
+ //v3已经改为通过状态码判断200 204 成功
+ int statusCode = response.getStatusLine().getStatusCode();
+ String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+ if (HttpStatus.SC_OK==statusCode || HttpStatus.SC_NO_CONTENT==statusCode){
+ this.log.info("\n【请求地址】:{}\n【请求数据】:{}\n【响应数据】:{}", url, requestStr, responseString);
+ return responseString;
+ }else {
+ //有错误提示信息返回
+ JSONObject jsonObject = JSONObject.parseObject(responseString);
+ String message = jsonObject.getString("message");
+ throw new WxPayException(message);
+ }
+ } catch (Exception e) {
+ this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage());
+ throw new WxPayException(e.getMessage(), e);
+ } finally {
+ httpPost.releaseConnection();
}
+
+
+
+
+ }
+
+ @Override
+ public String getV3(URI url) throws WxPayException {
+ CloseableHttpClient httpClient = this.createApiV3HttpClient();
+ HttpGet httpGet = new HttpGet(url);
+ httpGet.addHeader("Accept", "application/json");
+ httpGet.addHeader("Content-Type", "application/json");
+ try (CloseableHttpResponse response = httpClient.execute(httpGet)){
+ //v3已经改为通过状态码判断200 204 成功
+ int statusCode = response.getStatusLine().getStatusCode();
+ String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
+ if (HttpStatus.SC_OK==statusCode || HttpStatus.SC_NO_CONTENT==statusCode){
+ this.log.info("\n【请求地址】:{}\n【响应数据】:{}", url , responseString);
+ return responseString;
+ }else {
+ //有错误提示信息返回
+ JSONObject jsonObject = JSONObject.parseObject(responseString);
+ String message = jsonObject.getString("message");
+ throw new WxPayException(message);
+ }
+ } catch (Exception e) {
+ this.log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage());
+ throw new WxPayException(e.getMessage(), e);
+ } finally {
+ httpGet.releaseConnection();
+ }
+
+
+ }
+
+ private CloseableHttpClient createApiV3HttpClient() throws WxPayException {
+ CloseableHttpClient apiv3HttpClient = this.getConfig().getApiv3HttpClient();
+ if (null==apiv3HttpClient){
+ return this.getConfig().initApiV3HttpClient();
+ }
+ return apiv3HttpClient;
+ }
+
+
+ private StringEntity createEntry(String requestStr) {
+
+ return new StringEntity(requestStr, ContentType.create("application/json", "utf-8"));
+ //return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
+
+
}
private HttpClientBuilder createHttpClientBuilder(boolean useKey) throws WxPayException {
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java
index 81d35614d..72dac0171 100644
--- a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java
@@ -1,5 +1,6 @@
package com.github.binarywang.wxpay.service.impl;
+import java.net.URI;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLContext;
@@ -67,6 +68,16 @@ public class WxPayServiceJoddHttpImpl extends BaseWxPayServiceImpl {
}
}
+ @Override
+ public String postV3(String url, String requestStr) throws WxPayException {
+ return null;
+ }
+
+ @Override
+ public String getV3(URI url) throws WxPayException {
+ return null;
+ }
+
private HttpRequest buildHttpRequest(String url, String requestStr, boolean useKey) throws WxPayException {
HttpRequest request = HttpRequest
.post(url)
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java
new file mode 100644
index 000000000..d5102d1fa
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java
@@ -0,0 +1,11 @@
+package com.github.binarywang.wxpay.v3;
+
+import java.io.IOException;
+import org.apache.http.client.methods.HttpUriRequest;
+
+public interface Credentials {
+
+ String getSchema();
+
+ String getToken(HttpUriRequest request) throws IOException;
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
new file mode 100644
index 000000000..a28dfdcd6
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
@@ -0,0 +1,88 @@
+package com.github.binarywang.wxpay.v3;
+
+import java.io.IOException;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpException;
+import org.apache.http.StatusLine;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpExecutionAware;
+import org.apache.http.client.methods.HttpRequestWrapper;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.client.methods.RequestBuilder;
+import org.apache.http.client.protocol.HttpClientContext;
+import org.apache.http.conn.routing.HttpRoute;
+import org.apache.http.entity.ByteArrayEntity;
+import org.apache.http.impl.execchain.ClientExecChain;
+import org.apache.http.util.EntityUtils;
+
+public class SignatureExec implements ClientExecChain {
+ final ClientExecChain mainExec;
+ final Credentials credentials;
+ final Validator validator;
+
+ SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) {
+ this.credentials = credentials;
+ this.validator = validator;
+ this.mainExec = mainExec;
+ }
+
+ protected HttpEntity newRepeatableEntity(HttpEntity entity) throws IOException {
+ byte[] content = EntityUtils.toByteArray(entity);
+ ByteArrayEntity newEntity = new ByteArrayEntity(content);
+ newEntity.setContentEncoding(entity.getContentEncoding());
+ newEntity.setContentType(entity.getContentType());
+
+ return newEntity;
+ }
+
+ protected void convertToRepeatableResponseEntity(CloseableHttpResponse response) throws IOException {
+ HttpEntity entity = response.getEntity();
+ if (entity != null && !entity.isRepeatable()) {
+ response.setEntity(newRepeatableEntity(entity));
+ }
+ }
+
+ protected void convertToRepeatableRequestEntity(HttpUriRequest request) throws IOException {
+ if (request instanceof HttpEntityEnclosingRequestBase) {
+ HttpEntity entity = ((HttpEntityEnclosingRequestBase) request).getEntity();
+ if (entity != null && !entity.isRepeatable()) {
+ ((HttpEntityEnclosingRequestBase) request).setEntity(newRepeatableEntity(entity));
+ }
+ }
+ }
+
+ @Override
+ public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
+ HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException {
+ if (request.getURI().getHost().endsWith(".mch.weixin.qq.com")) {
+ return executeWithSignature(route, request, context, execAware);
+ } else {
+ return mainExec.execute(route, request, context, execAware);
+ }
+ }
+
+ private CloseableHttpResponse executeWithSignature(HttpRoute route, HttpRequestWrapper request,
+ HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException {
+ HttpUriRequest newRequest = RequestBuilder.copy(request.getOriginal()).build();
+ convertToRepeatableRequestEntity(newRequest);
+ // 添加认证信息
+ newRequest.addHeader("Authorization",
+ credentials.getSchema() + " " + credentials.getToken(newRequest));
+
+ // 执行
+ CloseableHttpResponse response = mainExec.execute(
+ route, HttpRequestWrapper.wrap(newRequest), context, execAware);
+
+ // 对成功应答验签
+ StatusLine statusLine = response.getStatusLine();
+ if (statusLine.getStatusCode() >= 200 && statusLine.getStatusCode() < 300) {
+ convertToRepeatableResponseEntity(response);
+ if (!validator.validate(response)) {
+ throw new HttpException("应答的微信支付签名验证失败");
+ }
+ }
+ return response;
+ }
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java
new file mode 100644
index 000000000..cdeb8ac27
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java
@@ -0,0 +1,8 @@
+package com.github.binarywang.wxpay.v3;
+
+import java.io.IOException;
+import org.apache.http.client.methods.CloseableHttpResponse;
+
+public interface Validator {
+ boolean validate(CloseableHttpResponse response) throws IOException;
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java
new file mode 100644
index 000000000..0f2732ce9
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java
@@ -0,0 +1,75 @@
+package com.github.binarywang.wxpay.v3;
+
+
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import com.github.binarywang.wxpay.v3.auth.CertificatesVerifier;
+import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner;
+import com.github.binarywang.wxpay.v3.auth.WechatPay2Credentials;
+import com.github.binarywang.wxpay.v3.auth.WechatPay2Validator;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.execchain.ClientExecChain;
+
+public class WechatPayHttpClientBuilder extends HttpClientBuilder {
+ private Credentials credentials;
+ private Validator validator;
+
+ static final String os = System.getProperty("os.name") + "/" + System.getProperty("os.version");
+ static final String version = System.getProperty("java.version");
+
+ private WechatPayHttpClientBuilder() {
+ super();
+
+ String userAgent = String.format(
+ "WechatPay-Apache-HttpClient/%s (%s) Java/%s",
+ getClass().getPackage().getImplementationVersion(),
+ os,
+ version == null ? "Unknown" : version);
+ setUserAgent(userAgent);
+ }
+
+ public static WechatPayHttpClientBuilder create() {
+ return new WechatPayHttpClientBuilder();
+ }
+
+ public WechatPayHttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) {
+ this.credentials =
+ new WechatPay2Credentials(merchantId, new PrivateKeySigner(serialNo, privateKey));
+ return this;
+ }
+
+ public WechatPayHttpClientBuilder withCredentials(Credentials credentials) {
+ this.credentials = credentials;
+ return this;
+ }
+
+ public WechatPayHttpClientBuilder withWechatpay(List certificates) {
+ this.validator = new WechatPay2Validator(new CertificatesVerifier(certificates));
+ return this;
+ }
+
+ public WechatPayHttpClientBuilder withValidator(Validator validator) {
+ this.validator = validator;
+ return this;
+ }
+
+ @Override
+ public CloseableHttpClient build() {
+ if (credentials == null) {
+ throw new IllegalArgumentException("缺少身份认证信息");
+ }
+ if (validator == null) {
+ throw new IllegalArgumentException("缺少签名验证信息");
+ }
+
+ return super.build();
+ }
+
+ @Override
+ protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) {
+ return new SignatureExec(this.credentials, this.validator, requestExecutor);
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
new file mode 100644
index 000000000..3c382181b
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
@@ -0,0 +1,161 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.locks.ReentrantLock;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.binarywang.wxpay.v3.Credentials;
+import com.github.binarywang.wxpay.v3.WechatPayHttpClientBuilder;
+import com.github.binarywang.wxpay.v3.util.AesUtils;
+import com.github.binarywang.wxpay.v3.util.PemUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.util.EntityUtils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 在原有CertificatesVerifier基础上,增加自动更新证书功能
+ */
+public class AutoUpdateCertificatesVerifier implements Verifier {
+
+ private static final Logger log = LoggerFactory.getLogger(AutoUpdateCertificatesVerifier.class);
+
+ //证书下载地址
+ private static final String CertDownloadPath = "https://api.mch.weixin.qq.com/v3/certificates";
+
+ //上次更新时间
+ private volatile Instant instant;
+
+ //证书更新间隔时间,单位为分钟
+ private int minutesInterval;
+
+ private CertificatesVerifier verifier;
+
+ private Credentials credentials;
+
+ private byte[] apiV3Key;
+
+ private ReentrantLock lock = new ReentrantLock();
+
+ //时间间隔枚举,支持一小时、六小时以及十二小时
+ public enum TimeInterval {
+ OneHour(60), SixHours(60 * 6), TwelveHours(60 * 12);
+
+ private int minutes;
+
+ TimeInterval(int minutes) {
+ this.minutes = minutes;
+ }
+
+ public int getMinutes() {
+ return minutes;
+ }
+ }
+
+ public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) {
+ this(credentials, apiV3Key, TimeInterval.OneHour.getMinutes());
+ }
+
+ public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key, int minutesInterval) {
+ this.credentials = credentials;
+ this.apiV3Key = apiV3Key;
+ this.minutesInterval = minutesInterval;
+ //构造时更新证书
+ try {
+ autoUpdateCert();
+ instant = Instant.now();
+ } catch (IOException | GeneralSecurityException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public boolean verify(String serialNumber, byte[] message, String signature) {
+ if (instant == null || Duration.between(instant, Instant.now()).toMinutes() >= minutesInterval) {
+ if (lock.tryLock()) {
+ try {
+ autoUpdateCert();
+ //更新时间
+ instant = Instant.now();
+ } catch (GeneralSecurityException | IOException e) {
+ log.warn("Auto update cert failed, exception = " + e);
+ } finally {
+ lock.unlock();
+ }
+ }
+ }
+ return verifier.verify(serialNumber, message, signature);
+ }
+
+ private void autoUpdateCert() throws IOException, GeneralSecurityException {
+ CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
+ .withCredentials(credentials)
+ .withValidator(verifier == null ? (response) -> true : new WechatPay2Validator(verifier))
+ .build();
+
+ HttpGet httpGet = new HttpGet(CertDownloadPath);
+ httpGet.addHeader("Accept", "application/json");
+
+ CloseableHttpResponse response = httpClient.execute(httpGet);
+ int statusCode = response.getStatusLine().getStatusCode();
+ String body = EntityUtils.toString(response.getEntity());
+ if (statusCode == 200) {
+ List newCertList = deserializeToCerts(apiV3Key, body);
+ if (newCertList.isEmpty()) {
+ log.warn("Cert list is empty");
+ return;
+ }
+ this.verifier = new CertificatesVerifier(newCertList);
+ } else {
+ log.warn("Auto update cert failed, statusCode = " + statusCode + ",body = " + body);
+ }
+ }
+
+
+ /**
+ * 反序列化证书并解密
+ */
+ private List deserializeToCerts(byte[] apiV3Key, String body)
+ throws GeneralSecurityException, IOException {
+ AesUtils decryptor = new AesUtils(apiV3Key);
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode dataNode = mapper.readTree(body).get("data");
+ List newCertList = new ArrayList<>();
+ if (dataNode != null) {
+ for (int i = 0, count = dataNode.size(); i < count; i++) {
+ JsonNode encryptCertificateNode = dataNode.get(i).get("encrypt_certificate");
+ //解密
+ String cert = decryptor.decryptToString(
+ encryptCertificateNode.get("associated_data").toString().replaceAll("\"", "")
+ .getBytes("utf-8"),
+ encryptCertificateNode.get("nonce").toString().replaceAll("\"", "")
+ .getBytes("utf-8"),
+ encryptCertificateNode.get("ciphertext").toString().replaceAll("\"", ""));
+
+ X509Certificate x509Cert = PemUtils
+ .loadCertificate(new ByteArrayInputStream(cert.getBytes("utf-8")));
+ try {
+ x509Cert.checkValidity();
+ } catch (CertificateExpiredException | CertificateNotYetValidException e) {
+ continue;
+ }
+ newCertList.add(x509Cert);
+ }
+ }
+ return newCertList;
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java
new file mode 100644
index 000000000..9853cf2ee
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java
@@ -0,0 +1,43 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import java.math.BigInteger;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.cert.X509Certificate;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+
+public class CertificatesVerifier implements Verifier {
+ private final HashMap certificates = new HashMap<>();
+
+ public CertificatesVerifier(List list) {
+
+ for (X509Certificate item : list) {
+ certificates.put(item.getSerialNumber(), item);
+ }
+ }
+
+ private boolean verify(X509Certificate certificate, byte[] message, String signature) {
+ try {
+ Signature sign = Signature.getInstance("SHA256withRSA");
+ sign.initVerify(certificate);
+ sign.update(message);
+ return sign.verify(Base64.getDecoder().decode(signature));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
+ } catch (SignatureException e) {
+ throw new RuntimeException("签名验证过程发生了错误", e);
+ } catch (InvalidKeyException e) {
+ throw new RuntimeException("无效的证书", e);
+ }
+ }
+
+ @Override
+ public boolean verify(String serialNumber, byte[] message, String signature) {
+ BigInteger val = new BigInteger(serialNumber, 16);
+ return certificates.containsKey(val) && verify(certificates.get(val), message, signature);
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java
new file mode 100644
index 000000000..37ec51cf5
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java
@@ -0,0 +1,37 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.util.Base64;
+
+public class PrivateKeySigner implements Signer {
+ private String certificateSerialNumber;
+
+ private PrivateKey privateKey;
+
+ public PrivateKeySigner(String serialNumber, PrivateKey privateKey) {
+ this.certificateSerialNumber = serialNumber;
+ this.privateKey = privateKey;
+ }
+
+ @Override
+ public SignatureResult sign(byte[] message) {
+ try {
+ Signature sign = Signature.getInstance("SHA256withRSA");
+ sign.initSign(privateKey);
+ sign.update(message);
+
+ return new SignatureResult(
+ Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber);
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
+ } catch (SignatureException e) {
+ throw new RuntimeException("签名计算失败", e);
+ } catch (InvalidKeyException e) {
+ throw new RuntimeException("无效的私钥", e);
+ }
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java
new file mode 100644
index 000000000..7255a1b43
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java
@@ -0,0 +1,15 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+public interface Signer {
+ SignatureResult sign(byte[] message);
+
+ class SignatureResult {
+ String sign;
+ String certificateSerialNumber;
+
+ public SignatureResult(String sign, String serialNumber) {
+ this.sign = sign;
+ this.certificateSerialNumber = serialNumber;
+ }
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java
new file mode 100644
index 000000000..ed591a4ef
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java
@@ -0,0 +1,5 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+public interface Verifier {
+ boolean verify(String serialNumber, byte[] message, String signature);
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java
new file mode 100644
index 000000000..a0730bdda
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java
@@ -0,0 +1,91 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+
+import java.io.IOException;
+import java.net.URI;
+import java.security.SecureRandom;
+
+import com.github.binarywang.wxpay.v3.Credentials;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WechatPay2Credentials implements Credentials {
+ private static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class);
+
+ private static final String SYMBOLS =
+ "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ private static final SecureRandom RANDOM = new SecureRandom();
+ protected String merchantId;
+ protected Signer signer;
+
+ public WechatPay2Credentials(String merchantId, Signer signer) {
+ this.merchantId = merchantId;
+ this.signer = signer;
+ }
+
+ public String getMerchantId() {
+ return merchantId;
+ }
+
+ protected long generateTimestamp() {
+ return System.currentTimeMillis() / 1000;
+ }
+
+ protected String generateNonceStr() {
+ char[] nonceChars = new char[32];
+ for (int index = 0; index < nonceChars.length; ++index) {
+ nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
+ }
+ return new String(nonceChars);
+ }
+
+ @Override
+ public final String getSchema() {
+ return "WECHATPAY2-SHA256-RSA2048";
+ }
+
+ @Override
+ public final String getToken(HttpUriRequest request) throws IOException {
+ String nonceStr = generateNonceStr();
+ long timestamp = generateTimestamp();
+
+ String message = buildMessage(nonceStr, timestamp, request);
+ log.debug("authorization message=[{}]", message);
+
+ Signer.SignatureResult signature = signer.sign(message.getBytes("utf-8"));
+
+ String token = "mchid=\"" + getMerchantId() + "\","
+ + "nonce_str=\"" + nonceStr + "\","
+ + "timestamp=\"" + timestamp + "\","
+ + "serial_no=\"" + signature.certificateSerialNumber + "\","
+ + "signature=\"" + signature.sign + "\"";
+ log.debug("authorization token=[{}]", token);
+
+ return token;
+ }
+
+ protected final String buildMessage(String nonce, long timestamp, HttpUriRequest request)
+ throws IOException {
+ URI uri = request.getURI();
+ String canonicalUrl = uri.getRawPath();
+ if (uri.getQuery() != null) {
+ canonicalUrl += "?" + uri.getRawQuery();
+ }
+
+ String body = "";
+ // PATCH,POST,PUT
+ if (request instanceof HttpEntityEnclosingRequestBase) {
+ body = EntityUtils.toString(((HttpEntityEnclosingRequestBase) request).getEntity());
+ }
+
+ return request.getRequestLine().getMethod() + "\n"
+ + canonicalUrl + "\n"
+ + timestamp + "\n"
+ + nonce + "\n"
+ + body + "\n";
+ }
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java
new file mode 100644
index 000000000..38735c61c
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java
@@ -0,0 +1,55 @@
+package com.github.binarywang.wxpay.v3.auth;
+
+
+import java.io.IOException;
+
+import com.github.binarywang.wxpay.v3.Validator;
+import org.apache.http.Header;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.util.EntityUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class WechatPay2Validator implements Validator {
+
+ private static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
+
+ private Verifier verifier;
+
+ public WechatPay2Validator(Verifier verifier) {
+ this.verifier = verifier;
+ }
+
+ @Override
+ public final boolean validate(CloseableHttpResponse response) throws IOException {
+ Header serialNo = response.getFirstHeader("Wechatpay-Serial");
+ Header sign = response.getFirstHeader("Wechatpay-Signature");
+ Header timestamp = response.getFirstHeader("Wechatpay-TimeStamp");
+ Header nonce = response.getFirstHeader("Wechatpay-Nonce");
+
+ // todo: check timestamp
+ if (timestamp == null || nonce == null || serialNo == null || sign == null) {
+ return false;
+ }
+
+ String message = buildMessage(response);
+ return verifier.verify(serialNo.getValue(), message.getBytes("utf-8"), sign.getValue());
+ }
+
+ protected final String buildMessage(CloseableHttpResponse response) throws IOException {
+ String timestamp = response.getFirstHeader("Wechatpay-TimeStamp").getValue();
+ String nonce = response.getFirstHeader("Wechatpay-Nonce").getValue();
+
+ String body = getResponseBody(response);
+ return timestamp + "\n"
+ + nonce + "\n"
+ + body + "\n";
+ }
+
+ protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
+ HttpEntity entity = response.getEntity();
+
+ return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
+ }
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java
new file mode 100644
index 000000000..bd68cac1a
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java
@@ -0,0 +1,107 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Base64;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+import javax.crypto.Cipher;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.GCMParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AesUtils {
+
+ static final int KEY_LENGTH_BYTE = 32;
+ static final int TAG_LENGTH_BIT = 128;
+ private final byte[] aesKey;
+
+ public AesUtils(byte[] key) {
+ if (key.length != KEY_LENGTH_BYTE) {
+ throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
+ }
+ this.aesKey = key;
+ }
+
+ public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
+ throws GeneralSecurityException, IOException {
+ try {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+
+ SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
+ GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
+
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ cipher.updateAAD(associatedData);
+
+ return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new IllegalStateException(e);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ public static String decryptToString(String associatedData, String nonce, String ciphertext,String apiV3Key)
+ throws GeneralSecurityException, IOException {
+ try {
+ Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
+
+ SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(), "AES");
+ GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce.getBytes());
+
+ cipher.init(Cipher.DECRYPT_MODE, key, spec);
+ cipher.updateAAD(associatedData.getBytes());
+
+ return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ throw new IllegalStateException(e);
+ } catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+
+ public static String createSign(Map map, String mchKey) {
+ Map params = map;
+ SortedMap sortedMap = new TreeMap<>(params);
+
+ StringBuilder toSign = new StringBuilder();
+ for (String key : sortedMap.keySet()) {
+ String value = params.get(key);
+ if ("sign".equals(key) || StringUtils.isEmpty(value)) {
+ continue;
+ }
+ toSign.append(key).append("=").append(value).append("&");
+ }
+ toSign.append("key=" + mchKey);
+ return HMACSHA256(toSign.toString(), mchKey);
+
+ }
+
+ public static String HMACSHA256(String data, String key) {
+ try {
+ Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
+ SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
+ sha256_HMAC.init(secret_key);
+ byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
+ StringBuilder sb = new StringBuilder();
+ for (byte item : array) {
+ sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
+ }
+ return sb.toString().toUpperCase();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+
+}
diff --git a/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java
new file mode 100644
index 000000000..bf4d2657b
--- /dev/null
+++ b/weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java
@@ -0,0 +1,60 @@
+package com.github.binarywang.wxpay.v3.util;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateExpiredException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CertificateNotYetValidException;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+import java.util.Base64;
+
+public class PemUtils {
+
+ public static PrivateKey loadPrivateKey(InputStream inputStream) {
+ try {
+ ByteArrayOutputStream array = new ByteArrayOutputStream();
+ byte[] buffer = new byte[1024];
+ int length;
+ while ((length = inputStream.read(buffer)) != -1) {
+ array.write(buffer, 0, length);
+ }
+
+ String privateKey = array.toString("utf-8")
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replaceAll("\\s+", "");
+
+ KeyFactory kf = KeyFactory.getInstance("RSA");
+ return kf.generatePrivate(
+ new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("当前Java环境不支持RSA", e);
+ } catch (InvalidKeySpecException e) {
+ throw new RuntimeException("无效的密钥格式");
+ } catch (IOException e) {
+ throw new RuntimeException("无效的密钥");
+ }
+ }
+
+ public static X509Certificate loadCertificate(InputStream inputStream) {
+ try {
+ CertificateFactory cf = CertificateFactory.getInstance("X509");
+ X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
+ cert.checkValidity();
+ return cert;
+ } catch (CertificateExpiredException e) {
+ throw new RuntimeException("证书已过期", e);
+ } catch (CertificateNotYetValidException e) {
+ throw new RuntimeException("证书尚未生效", e);
+ } catch (CertificateException e) {
+ throw new RuntimeException("无效的证书", e);
+ }
+ }
+}