mirror of
https://gitee.com/binary/weixin-java-tools.git
synced 2025-04-05 17:38:05 +08:00
🆕 #1090 增加微信支付分和免押租借相关接口
This commit is contained in:
parent
0bc2cf9ade
commit
8709a9c5a7
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
public interface Verifier {
|
||||
boolean verify(String serialNumber, byte[] message, String signature);
|
||||
}
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
@ -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) : "";
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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…
Reference in New Issue
Block a user