Browse Source

🆕 #1090 增加微信支付分和免押租借相关接口

dev1
spvycf 4 years ago
committed by GitHub
parent
commit
8709a9c5a7
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1762 additions and 12 deletions
  1. +7
    -0
      spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
  2. +31
    -0
      spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
  3. +25
    -0
      weixin-java-pay/pom.xml
  4. +45
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/NotifyData.java
  5. +130
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreRequest.java
  6. +163
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreResult.java
  7. +118
    -1
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
  8. +164
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java
  9. +31
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
  10. +7
    -4
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
  11. +193
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java
  12. +81
    -7
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
  13. +11
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java
  14. +11
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java
  15. +88
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
  16. +8
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java
  17. +75
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java
  18. +161
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
  19. +43
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java
  20. +37
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java
  21. +15
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java
  22. +5
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java
  23. +91
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java
  24. +55
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java
  25. +107
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java
  26. +60
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java

+ 7
- 0
spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java View File

@@ -49,6 +49,13 @@ public class WxPayAutoConfiguration {
payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId()));
payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
//以下是apiv3以及支付分相关
payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId()));
payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl()));
payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo()));
payConfig.setApiv3Key(StringUtils.trimToNull(this.properties.getApiv3Key()));

wxPayService.setConfig(payConfig);
return wxPayService;


+ 31
- 0
spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java View File

@@ -43,4 +43,35 @@ public class WxPayProperties {
* apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定.
*/
private String keyPath;

/**
* 微信支付分serviceId
*/
private String serviceId;

/**
* 证书序列号
*/
private String certSerialNo;

/**
* apiV3秘钥
*/
private String apiv3Key;

/**
* 微信支付分回调地址
*/
private String payScoreNotifyUrl;

/**
* apiv3 商户apiclient_key.pem
*/
private String privateKeyPath;

/**
* apiv3 商户apiclient_cert.pem
*/
private String privateCertPath;

}

+ 25
- 0
weixin-java-pay/pom.xml View File

@@ -12,6 +12,18 @@
<artifactId>weixin-java-pay</artifactId>
<name>WxJava - PAY Java SDK</name>
<description>微信支付 Java SDK</description>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>

<dependencies>
<dependency>
@@ -70,6 +82,19 @@
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.58</version>
</dependency>


</dependencies>

<profiles>


+ 45
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/NotifyData.java View File

@@ -0,0 +1,45 @@
package com.github.binarywang.wxpay.bean.payscore;

import lombok.Data;
import lombok.NoArgsConstructor;

/**
* 微信支付分确认订单跟支付回调对象
* @author doger.wang
* @date 2020/5/14 12:18
*/
@NoArgsConstructor
@Data
public class NotifyData {


/**
* id : EV-2018022511223320873
* create_time : 20180225112233
* resource_type : encrypt-resource
* event_type : PAYSCORE.USER_CONFIRM
* resource : {"algorithm":"AEAD_AES_256_GCM","ciphertext":"...","nonce":"...","associated_data":""}
*/

private String id;
private String create_time;
private String resource_type;
private String event_type;
private Resource resource;

@NoArgsConstructor
@Data
public static class Resource {
/**
* algorithm : AEAD_AES_256_GCM
* ciphertext : ...
* nonce : ...
* associated_data :
*/

private String algorithm;
private String ciphertext;
private String nonce;
private String associated_data;
}
}

+ 130
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreRequest.java View File

@@ -0,0 +1,130 @@
package com.github.binarywang.wxpay.bean.payscore;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;

/**
* @author doger.wang
* @date 2020/5/12 16:36
*/
@NoArgsConstructor
@Data
public class WxPayScoreRequest implements Serializable {


private static final long serialVersionUID = 364764508076146082L;
/**
* out_order_no : 1234323JKHDFE1243252
* appid : wxd678efh567hg6787
* service_id : 500001
* service_introduction : 某某酒店
* post_payments : [{"name":"就餐费用服务费","amount":4000,"description":"就餐人均100元服务费:100/小时","count":1}]
* post_discounts : [{"name":"满20减1元","description":"不与其他优惠叠加"}]
* time_range : {"start_time":"20091225091010","end_time":"20091225121010"}
* location : {"start_location":"嗨客时尚主题展餐厅","end_location":"嗨客时尚主题展餐厅"}
* risk_fund : {"name":"ESTIMATE_ORDER_COST","amount":10000,"description":"就餐的预估费用"}
* attach : Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald
* notify_url : https://api.test.com
* openid : oUpF8uMuAJO_M2pxb1Q9zNjWeS6o
* need_user_confirm : true
*/

private String out_order_no;
private String appid;
private String service_id;
private String service_introduction;
private TimeRange time_range;
private Location location;
private RiskFund risk_fund;
private String attach;
private String notify_url;
private String openid;
private boolean need_user_confirm;
private boolean profit_sharing;
private List<PostPayments> post_payments;
private List<PostDiscounts> post_discounts;
private int total_amount;
private String reason;
private String goods_tag;
private String type;
private Detail detail;

@NoArgsConstructor
@Data
public static class Detail {
private String paid_time;
}



@NoArgsConstructor
@Data
public static class TimeRange {
/**
* start_time : 20091225091010
* end_time : 20091225121010
*/

private String start_time;
private String end_time;
}

@NoArgsConstructor
@Data
public static class Location {
/**
* start_location : 嗨客时尚主题展餐厅
* end_location : 嗨客时尚主题展餐厅
*/

private String start_location;
private String end_location;
}

@NoArgsConstructor
@Data
public static class RiskFund {
/**
* name : ESTIMATE_ORDER_COST
* amount : 10000
* description : 就餐的预估费用
*/

private String name;
private int amount;
private String description;
}

@NoArgsConstructor
@Data
public static class PostPayments {
/**
* name : 就餐费用服务费
* amount : 4000
* description : 就餐人均100元服务费:100/小时
* count : 1
*/

private String name;
private int amount;
private String description;
private int count;
}

@NoArgsConstructor
@Data
public static class PostDiscounts {
/**
* name : 满20减1元
* description : 不与其他优惠叠加
*/

private String name;
private String description;
private int count;
private int amount;
}
}

+ 163
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/payscore/WxPayScoreResult.java View File

@@ -0,0 +1,163 @@
package com.github.binarywang.wxpay.bean.payscore;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.util.List;
import java.util.Map;

/**
* @author doger.wang
* @date 2020/5/12 17:05
*/
@NoArgsConstructor
@Data
public class WxPayScoreResult implements Serializable {


private static final long serialVersionUID = 8809250065540275770L;
/**
* appid : wxd678efh567hg6787
* mchid : 1230000109
* out_order_no : 1234323JKHDFE1243252
* service_id : 500001
* service_introduction : 某某酒店
* state : CREATED
* state_description : MCH_COMPLETE
* post_payments : [{"name":"就餐费用服务费","amount":4000,"description":"就餐人均100元服务费:100/小时","count":1}]
* post_discounts : [{"name":"满20减1元","description":"不与其他优惠叠加"}]
* risk_fund : {"name":" ESTIMATE_ORDER_COST","amount":10000,"description":"就餐的预估费用"}
* time_range : {"start_time":"20091225091010","end_time":"20091225121010"}
* location : {"start_location":"嗨客时尚主题展餐厅","end_location":"嗨客时尚主题展餐厅"}
* attach : Easdfowealsdkjfnlaksjdlfkwqoi&wl3l2sald
* notify_url : https://api.test.com
* order_id : 15646546545165651651
* package : DJIOSQPYWDxsjdldeuwhdodwxasd_dDiodnwjh9we
*/

private String appid;
private String mchid;
private String out_order_no;
private String service_id;
private String service_introduction;
private String state;
private String state_description;
private RiskFund risk_fund;
private TimeRange time_range;
private Location location;
private String attach;
private String notify_url;
private String order_id;
@JSONField(name = "package")
private String packageX;
private List<PostPayments> post_payments;
private List<PostDiscounts> post_discounts;
private boolean need_collection;
private Collection collection;
//用于跳转的sign注意区分需确认模式和无需确认模式的数据差别。创单接口会返回,查询请自行组装
private Map<String, String> payScoreSignInfo;

@NoArgsConstructor
@Data
public static class RiskFund {
/**
* name : ESTIMATE_ORDER_COST
* amount : 10000
* description : 就餐的预估费用
*/

private String name;
private int amount;
private String description;
}

@NoArgsConstructor
@Data
public static class TimeRange {
/**
* start_time : 20091225091010
* end_time : 20091225121010
*/

private String start_time;
private String end_time;
}

@NoArgsConstructor
@Data
public static class Location {
/**
* start_location : 嗨客时尚主题展餐厅
* end_location : 嗨客时尚主题展餐厅
*/

private String start_location;
private String end_location;
}

@NoArgsConstructor
@Data
public static class PostPayments {
/**
* name : 就餐费用服务费
* amount : 4000
* description : 就餐人均100元服务费:100/小时
* count : 1
*/

private String name;
private int amount;
private String description;
private int count;
}

@NoArgsConstructor
@Data
public static class PostDiscounts {
/**
* name : 满20减1元
* description : 不与其他优惠叠加
*/

private String name;
private String description;
}

@NoArgsConstructor
@Data
public static class Collection {
/**
* state : USER_PAID
* total_amount : 3900
* paying_amount : 3000
* paid_amount : 900
* details : [{"seq":1,"amount":900,"paid_type":"NEWTON","paid_time":"20091225091210","transaction_id":"15646546545165651651"}]
*/

private String state;
private int total_amount;
private int paying_amount;
private int paid_amount;
private List<Details> details;

@NoArgsConstructor
@Data
public static class Details {
/**
* seq : 1
* amount : 900
* paid_type : NEWTON
* paid_time : 20091225091210
* transaction_id : 15646546545165651651
*/

private int seq;
private int amount;
private String paid_type;
private String paid_time;
private String transaction_id;
}
}
}

+ 118
- 1
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java View File

@@ -1,15 +1,25 @@
package com.github.binarywang.wxpay.config;

import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.v3.WechatPayHttpClientBuilder;
import com.github.binarywang.wxpay.v3.auth.AutoUpdateCertificatesVerifier;
import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner;
import com.github.binarywang.wxpay.v3.auth.WechatPay2Credentials;
import com.github.binarywang.wxpay.v3.auth.WechatPay2Validator;
import com.github.binarywang.wxpay.v3.util.PemUtils;
import lombok.Data;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.URL;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.ArrayList;

/**
* 微信支付配置
@@ -85,6 +95,39 @@ public class WxPayConfig {
*/
private String keyPath;

/**
* apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String privateKeyPath;
/**
* apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.
*/
private String privateCertPath;

/**
* apiV3 秘钥值.
*/
private String apiv3Key;

/**
* apiV3 证书序列号值
*/
private String certSerialNo;


/**
* 微信支付分serviceId
*/
private String serviceId;

/**
* 微信支付分回调地址
*/
private String payScoreNotifyUrl;

private CloseableHttpClient apiv3HttpClient;


/**
* p12证书文件内容的字节数组.
*/
@@ -185,4 +228,78 @@ public class WxPayConfig {
}
}

}

/**
* @Author doger.wang
* @Description 初始化api v3请求头 自动签名验签
* 方法参照微信官方https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient
* @Date 2020/5/14 10:10
* @Param []
* @return org.apache.http.impl.client.CloseableHttpClient
**/
public CloseableHttpClient initApiV3HttpClient()throws WxPayException {
String privateKeyPath = this.getPrivateKeyPath();
String privateCertPath = this.getPrivateCertPath();
String certSerialNo = this.getCertSerialNo();
String apiv3Key = this.getApiv3Key();
if (StringUtils.isBlank(privateKeyPath)) {
throw new WxPayException("请确保privateKeyPath已设置");
}
if (StringUtils.isBlank(privateCertPath)) {
throw new WxPayException("请确保privateCertPath已设置");
}
if (StringUtils.isBlank(certSerialNo)) {
throw new WxPayException("请确保certSerialNo证书序列号已设置");
}
if (StringUtils.isBlank(apiv3Key)) {
throw new WxPayException("请确保apiv3Key值已设置");
}


InputStream keyinputStream=null;
InputStream certinputStream=null;
final String prefix = "classpath:";
if (privateKeyPath.startsWith(prefix)) {
String keypath = StringUtils.removeFirst(privateKeyPath, prefix);
if (!keypath.startsWith("/")) {
keypath = "/" + keypath;
}
keyinputStream = WxPayConfig.class.getResourceAsStream(keypath);
if (keyinputStream == null) {
throw new WxPayException("证书文件【" + this.getPrivateKeyPath() + "】不存在,请核实!");
}
}

if (privateCertPath.startsWith(prefix)) {
String certpath = StringUtils.removeFirst(privateCertPath, prefix);
if (!certpath.startsWith("/")) {
certpath = "/" + certpath;
}
certinputStream = WxPayConfig.class.getResourceAsStream(certpath);
if (certinputStream == null) {
throw new WxPayException("证书文件【" + this.getPrivateCertPath() + "】不存在,请核实!");
}
}
CloseableHttpClient httpClient = null;
try {
WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create();
PrivateKey merchantPrivateKey = PemUtils.loadPrivateKey(keyinputStream);
X509Certificate x509Certificate = PemUtils.loadCertificate(certinputStream);
ArrayList<X509Certificate> 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;


}
}

+ 164
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/PayScoreService.java View File

@@ -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;

/**
* <pre>
* 支付分相关服务类.
* 微信支付分是对个人的身份特质、支付行为、使用历史等情况的综合计算分值,旨在为用户提供更简单便捷的生活方式。
* 微信用户可以在具体应用场景中,开通微信支付分。开通后,用户可以在【微信—>钱包—>支付分】中查看分数和使用记录。(即需在应用场景中使用过一次,钱包才会出现支付分入口)
*
* Created by doger.wang on 2020/05/12.
* </pre>
*
*
*/
public interface PayScoreService {



/**
* <pre>
* 支付分创建订单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
* </pre>
*
* @param request 请求对象
* @return WxPayScoreResult
* @throws WxPayException the wx pay exception
*/
WxPayScoreResult createServiceOrder(WxPayScoreRequest request) throws WxPayException;



/**
* <pre>
* 支付分查询订单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
* </pre>
*
* @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;


/**
* <pre>
* 支付分取消订单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
* </pre>
*
* @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;

/**
* <pre>
* 支付分修改订单金额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
* </pre>
*
* @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;


/**
* <pre>
* 支付分完结订单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
* </pre>
*
* @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;


/**
* <pre>
* 支付分订单收款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
* </pre>
*
* @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;


/**
* <pre>
* 支付分订单收款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
* </pre>
*
* @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;


/**
* <pre>
* 支付分回调内容解密方法
* 文档详见: 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
* </pre>
*
* @param NotifyData 请求对象
* @return WxPayScoreResult
*/
WxPayScoreResult decryptNotifyData(NotifyData data) throws WxPayException;


















}

+ 31
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java View File

@@ -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;


}

+ 7
- 4
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java View File

@@ -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;


+ 193
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/PayScoreServiceImpl.java View File

@@ -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<String, String> 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<String, String> 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<String, Object> 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<String, Object> 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);
}

}
}

+ 81
- 7
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java View File

@@ -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 {


+ 11
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java View File

@@ -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)


+ 11
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java View File

@@ -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;
}

+ 88
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java View File

@@ -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;
}

}

+ 8
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java View File

@@ -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;
}

+ 75
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayHttpClientBuilder.java View File

@@ -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<X509Certificate> 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);
}
}

+ 161
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java View File

@@ -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<X509Certificate> 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<X509Certificate> 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<X509Certificate> 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;
}
}

+ 43
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java View File

@@ -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<BigInteger, X509Certificate> certificates = new HashMap<>();

public CertificatesVerifier(List<X509Certificate> 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);
}
}

+ 37
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java View File

@@ -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);
}
}
}

+ 15
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java View File

@@ -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;
}
}
}

+ 5
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java View File

@@ -0,0 +1,5 @@
package com.github.binarywang.wxpay.v3.auth;

public interface Verifier {
boolean verify(String serialNumber, byte[] message, String signature);
}

+ 91
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Credentials.java View File

@@ -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";
}

}

+ 55
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WechatPay2Validator.java View File

@@ -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) : "";
}
}

+ 107
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java View File

@@ -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<String, String> map, String mchKey) {
Map<String, String> params = map;
SortedMap<String, String> 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;
}
}


}

+ 60
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java View File

@@ -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);
}
}
}

Loading…
Cancel
Save