@@ -49,6 +49,13 @@ public class WxPayAutoConfiguration { | |||||
payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId())); | payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId())); | ||||
payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId())); | payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId())); | ||||
payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath())); | 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); | wxPayService.setConfig(payConfig); | ||||
return wxPayService; | return wxPayService; | ||||
@@ -43,4 +43,35 @@ public class WxPayProperties { | |||||
* apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定. | * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定. | ||||
*/ | */ | ||||
private String keyPath; | 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> | <artifactId>weixin-java-pay</artifactId> | ||||
<name>WxJava - PAY Java SDK</name> | <name>WxJava - PAY Java SDK</name> | ||||
<description>微信支付 Java SDK</description> | <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> | <dependencies> | ||||
<dependency> | <dependency> | ||||
@@ -70,6 +82,19 @@ | |||||
<groupId>org.projectlombok</groupId> | <groupId>org.projectlombok</groupId> | ||||
<artifactId>lombok</artifactId> | <artifactId>lombok</artifactId> | ||||
</dependency> | </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> | </dependencies> | ||||
<profiles> | <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; | package com.github.binarywang.wxpay.config; | ||||
import com.github.binarywang.wxpay.exception.WxPayException; | 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 lombok.Data; | ||||
import org.apache.commons.io.IOUtils; | import org.apache.commons.io.IOUtils; | ||||
import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
import org.apache.http.impl.client.CloseableHttpClient; | |||||
import org.apache.http.ssl.SSLContexts; | import org.apache.http.ssl.SSLContexts; | ||||
import javax.net.ssl.SSLContext; | import javax.net.ssl.SSLContext; | ||||
import java.io.*; | import java.io.*; | ||||
import java.net.URL; | import java.net.URL; | ||||
import java.security.KeyStore; | 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; | 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证书文件内容的字节数组. | * 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 com.github.binarywang.wxpay.exception.WxPayException; | ||||
import java.io.File; | import java.io.File; | ||||
import java.net.URI; | |||||
import java.util.Date; | import java.util.Date; | ||||
import java.util.Map; | import java.util.Map; | ||||
@@ -54,6 +55,27 @@ public interface WxPayService { | |||||
*/ | */ | ||||
String post(String url, String requestStr, boolean useKey) throws WxPayException; | 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(); | ProfitSharingService getProfitSharingService(); | ||||
/** | |||||
* 获取支付分服务类. | |||||
* | |||||
* @return the ent pay service | |||||
*/ | |||||
PayScoreService getPayScoreService(); | |||||
/** | /** | ||||
* 设置企业付款服务类,允许开发者自定义实现类. | * 设置企业付款服务类,允许开发者自定义实现类. | ||||
* | * | ||||
@@ -729,4 +759,5 @@ public interface WxPayService { | |||||
*/ | */ | ||||
WxPayFacepayResult facepay(WxPayFacepayRequest request) throws WxPayException; | 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.SignType; | ||||
import com.github.binarywang.wxpay.constant.WxPayConstants.TradeType; | import com.github.binarywang.wxpay.constant.WxPayConstants.TradeType; | ||||
import com.github.binarywang.wxpay.exception.WxPayException; | 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.SignUtils; | ||||
import com.github.binarywang.wxpay.util.XmlConfig; | import com.github.binarywang.wxpay.util.XmlConfig; | ||||
import com.google.common.base.Joiner; | import com.google.common.base.Joiner; | ||||
@@ -64,6 +61,7 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { | |||||
private EntPayService entPayService = new EntPayServiceImpl(this); | private EntPayService entPayService = new EntPayServiceImpl(this); | ||||
private ProfitSharingService profitSharingService = new ProfitSharingServiceImpl(this); | private ProfitSharingService profitSharingService = new ProfitSharingServiceImpl(this); | ||||
private RedpackService redpackService = new RedpackServiceImpl(this); | private RedpackService redpackService = new RedpackServiceImpl(this); | ||||
private PayScoreService payScoreService = new PayScoreServiceImpl(this); | |||||
/** | /** | ||||
* The Config. | * The Config. | ||||
@@ -80,6 +78,11 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { | |||||
return profitSharingService; | return profitSharingService; | ||||
} | } | ||||
@Override | |||||
public PayScoreService getPayScoreService() { | |||||
return payScoreService; | |||||
} | |||||
@Override | @Override | ||||
public RedpackService getRedpackService() { | public RedpackService getRedpackService() { | ||||
return this.redpackService; | 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; | package com.github.binarywang.wxpay.service.impl; | ||||
import java.io.IOException; | |||||
import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||
import java.net.URI; | |||||
import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||
import javax.net.ssl.SSLContext; | import javax.net.ssl.SSLContext; | ||||
import com.alibaba.fastjson.JSONObject; | |||||
import com.github.binarywang.wxpay.bean.WxPayApiData; | import com.github.binarywang.wxpay.bean.WxPayApiData; | ||||
import com.github.binarywang.wxpay.bean.request.WxPayQueryCommentRequest; | import com.github.binarywang.wxpay.bean.request.WxPayQueryCommentRequest; | ||||
import com.github.binarywang.wxpay.bean.request.WxPayRedpackQueryRequest; | import com.github.binarywang.wxpay.bean.request.WxPayRedpackQueryRequest; | ||||
import com.github.binarywang.wxpay.bean.result.WxPayCommonResult; | import com.github.binarywang.wxpay.bean.result.WxPayCommonResult; | ||||
import com.github.binarywang.wxpay.bean.result.WxPayRedpackQueryResult; | import com.github.binarywang.wxpay.bean.result.WxPayRedpackQueryResult; | ||||
import com.github.binarywang.wxpay.config.WxPayConfig; | |||||
import com.github.binarywang.wxpay.exception.WxPayException; | import com.github.binarywang.wxpay.exception.WxPayException; | ||||
import jodd.util.Base64; | import jodd.util.Base64; | ||||
import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
import org.apache.http.HttpHost; | import org.apache.http.HttpHost; | ||||
import org.apache.http.HttpStatus; | |||||
import org.apache.http.auth.AuthScope; | import org.apache.http.auth.AuthScope; | ||||
import org.apache.http.auth.UsernamePasswordCredentials; | import org.apache.http.auth.UsernamePasswordCredentials; | ||||
import org.apache.http.client.CredentialsProvider; | import org.apache.http.client.CredentialsProvider; | ||||
import org.apache.http.client.config.RequestConfig; | import org.apache.http.client.config.RequestConfig; | ||||
import org.apache.http.client.methods.CloseableHttpResponse; | import org.apache.http.client.methods.CloseableHttpResponse; | ||||
import org.apache.http.client.methods.HttpGet; | |||||
import org.apache.http.client.methods.HttpPost; | import org.apache.http.client.methods.HttpPost; | ||||
import org.apache.http.conn.ssl.DefaultHostnameVerifier; | import org.apache.http.conn.ssl.DefaultHostnameVerifier; | ||||
import org.apache.http.conn.ssl.SSLConnectionSocketFactory; | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; | ||||
import org.apache.http.entity.ContentType; | |||||
import org.apache.http.entity.StringEntity; | import org.apache.http.entity.StringEntity; | ||||
import org.apache.http.impl.client.BasicCredentialsProvider; | import org.apache.http.impl.client.BasicCredentialsProvider; | ||||
import org.apache.http.impl.client.CloseableHttpClient; | 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 { | private HttpClientBuilder createHttpClientBuilder(boolean useKey) throws WxPayException { | ||||
@@ -1,5 +1,6 @@ | |||||
package com.github.binarywang.wxpay.service.impl; | package com.github.binarywang.wxpay.service.impl; | ||||
import java.net.URI; | |||||
import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||
import javax.net.ssl.SSLContext; | 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 { | private HttpRequest buildHttpRequest(String url, String requestStr, boolean useKey) throws WxPayException { | ||||
HttpRequest request = HttpRequest | HttpRequest request = HttpRequest | ||||
.post(url) | .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); | |||||
} | |||||
} | |||||
} |