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