| @@ -126,11 +126,10 @@ | |||||
| <artifactId>qrcode-utils</artifactId> | <artifactId>qrcode-utils</artifactId> | ||||
| <version>1.1</version> | <version>1.1</version> | ||||
| </dependency> | </dependency> | ||||
| <!-- 由于jodd-http较新的3.8版本需要jdk8,故而此处采用较低版本 --> | |||||
| <dependency> | <dependency> | ||||
| <groupId>org.jodd</groupId> | <groupId>org.jodd</groupId> | ||||
| <artifactId>jodd-http</artifactId> | <artifactId>jodd-http</artifactId> | ||||
| <version>3.7.1</version> | |||||
| <version>5.2.0</version> | |||||
| <scope>provided</scope> | <scope>provided</scope> | ||||
| </dependency> | </dependency> | ||||
| <dependency> | <dependency> | ||||
| @@ -163,7 +162,7 @@ | |||||
| <dependency> | <dependency> | ||||
| <groupId>org.apache.commons</groupId> | <groupId>org.apache.commons</groupId> | ||||
| <artifactId>commons-lang3</artifactId> | <artifactId>commons-lang3</artifactId> | ||||
| <version>3.5</version> | |||||
| <version>3.10</version> | |||||
| </dependency> | </dependency> | ||||
| <dependency> | <dependency> | ||||
| <groupId>org.slf4j</groupId> | <groupId>org.slf4j</groupId> | ||||
| @@ -50,6 +50,14 @@ public class WxPayAutoConfiguration { | |||||
| 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,34 @@ 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; | |||||
| } | } | ||||
| @@ -0,0 +1,23 @@ | |||||
| package me.chanjar.weixin.common.error; | |||||
| /** | |||||
| * WxJava专用的runtime exception. | |||||
| * | |||||
| * @author <a href="https://github.com/binarywang">Binary Wang</a> | |||||
| * @date 2020-09-26 | |||||
| */ | |||||
| public class WxRuntimeException extends RuntimeException { | |||||
| private static final long serialVersionUID = 4881698471192264412L; | |||||
| public WxRuntimeException(Throwable e) { | |||||
| super(e); | |||||
| } | |||||
| public WxRuntimeException(String msg) { | |||||
| super(msg); | |||||
| } | |||||
| public WxRuntimeException(String msg, Throwable e) { | |||||
| super(msg, e); | |||||
| } | |||||
| } | |||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| import me.chanjar.weixin.common.error.WxErrorException; | import me.chanjar.weixin.common.error.WxErrorException; | ||||
| @@ -19,6 +18,8 @@ import java.io.ByteArrayInputStream; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * . | * . | ||||
| @@ -47,7 +48,7 @@ public class JoddHttpMediaDownloadRequestExecutor extends BaseMediaDownloadReque | |||||
| request.withConnectionProvider(requestHttp.getRequestHttpClient()); | request.withConnectionProvider(requestHttp.getRequestHttpClient()); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String contentType = response.header("Content-Type"); | String contentType = response.header("Content-Type"); | ||||
| if (contentType != null && contentType.startsWith("application/json")) { | if (contentType != null && contentType.startsWith("application/json")) { | ||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| @@ -14,6 +13,7 @@ import me.chanjar.weixin.common.util.http.RequestHttp; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * . | * . | ||||
| @@ -35,7 +35,7 @@ public class JoddHttpMediaUploadRequestExecutor extends MediaUploadRequestExecut | |||||
| request.withConnectionProvider(requestHttp.getRequestHttpClient()); | request.withConnectionProvider(requestHttp.getRequestHttpClient()); | ||||
| request.form("media", file); | request.form("media", file); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| WxError error = WxError.fromJson(responseContent, wxType); | WxError error = WxError.fromJson(responseContent, wxType); | ||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| import me.chanjar.weixin.common.error.WxErrorException; | import me.chanjar.weixin.common.error.WxErrorException; | ||||
| @@ -12,6 +11,7 @@ import me.chanjar.weixin.common.util.http.RequestHttp; | |||||
| import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; | import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * . | * . | ||||
| @@ -39,7 +39,7 @@ public class JoddHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor<H | |||||
| } | } | ||||
| request.withConnectionProvider(requestHttp.getRequestHttpClient()); | request.withConnectionProvider(requestHttp.getRequestHttpClient()); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| import me.chanjar.weixin.common.error.WxErrorException; | import me.chanjar.weixin.common.error.WxErrorException; | ||||
| @@ -12,6 +11,7 @@ import me.chanjar.weixin.common.util.http.RequestHttp; | |||||
| import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; | import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * . | * . | ||||
| @@ -38,7 +38,7 @@ public class JoddHttpSimplePostRequestExecutor extends SimplePostRequestExecutor | |||||
| request.bodyText(postEntity); | request.bodyText(postEntity); | ||||
| } | } | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| if (responseContent.isEmpty()) { | if (responseContent.isEmpty()) { | ||||
| @@ -0,0 +1,26 @@ | |||||
| package me.chanjar.weixin.common.util.json; | |||||
| import com.google.gson.JsonObject; | |||||
| import com.google.gson.JsonParser; | |||||
| import com.google.gson.stream.JsonReader; | |||||
| import java.io.Reader; | |||||
| /** | |||||
| * @author niefy | |||||
| */ | |||||
| public class GsonParser { | |||||
| private static final JsonParser JSON_PARSER = new JsonParser(); | |||||
| public static JsonObject parse(String json) { | |||||
| return JSON_PARSER.parse(json).getAsJsonObject(); | |||||
| } | |||||
| public static JsonObject parse(Reader json) { | |||||
| return JSON_PARSER.parse(json).getAsJsonObject(); | |||||
| } | |||||
| public static JsonObject parse(JsonReader json) { | |||||
| return JSON_PARSER.parse(json).getAsJsonObject(); | |||||
| } | |||||
| } | |||||
| @@ -3,8 +3,8 @@ package me.chanjar.weixin.cp.api.impl; | |||||
| import jodd.http.HttpConnectionProvider; | import jodd.http.HttpConnectionProvider; | ||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.JoddHttp; | |||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.http.net.SocketHttpConnectionProvider; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.bean.WxAccessToken; | import me.chanjar.weixin.common.bean.WxAccessToken; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| @@ -68,7 +68,7 @@ public class WxCpServiceJoddHttpImpl extends BaseWxCpServiceImpl<HttpConnectionP | |||||
| configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword()); | configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword()); | ||||
| } | } | ||||
| httpClient = JoddHttp.httpConnectionProvider; | |||||
| httpClient = new SocketHttpConnectionProvider(); | |||||
| } | } | ||||
| @Override | @Override | ||||
| @@ -46,7 +46,7 @@ public class WxMpServiceJoddHttpImpl extends BaseWxMpServiceImpl<HttpConnectionP | |||||
| httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword()); | httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword()); | ||||
| } | } | ||||
| httpClient = JoddHttp.httpConnectionProvider; | |||||
| httpClient = new SocketHttpConnectionProvider(); | |||||
| } | } | ||||
| @Override | @Override | ||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| @@ -12,6 +11,7 @@ import me.chanjar.weixin.common.error.WxErrorException; | |||||
| import me.chanjar.weixin.common.util.http.RequestHttp; | import me.chanjar.weixin.common.util.http.RequestHttp; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * Created by ecoolper on 2017/5/5. | * Created by ecoolper on 2017/5/5. | ||||
| @@ -31,7 +31,7 @@ public class MaterialDeleteJoddHttpRequestExecutor extends MaterialDeleteRequest | |||||
| request.query("media_id", materialId); | request.query("media_id", materialId); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| WxError error = WxError.fromJson(responseContent, WxType.MP); | WxError error = WxError.fromJson(responseContent, WxType.MP); | ||||
| if (error.getErrorCode() != 0) { | if (error.getErrorCode() != 0) { | ||||
| @@ -5,7 +5,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| @@ -18,6 +17,7 @@ import org.slf4j.Logger; | |||||
| import org.slf4j.LoggerFactory; | import org.slf4j.LoggerFactory; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * Created by ecoolper on 2017/5/5. | * Created by ecoolper on 2017/5/5. | ||||
| @@ -38,7 +38,7 @@ public class MaterialNewsInfoJoddHttpRequestExecutor extends MaterialNewsInfoReq | |||||
| .withConnectionProvider(requestHttp.getRequestHttpClient()) | .withConnectionProvider(requestHttp.getRequestHttpClient()) | ||||
| .body(WxGsonBuilder.create().toJson(ImmutableMap.of("media_id", materialId))); | .body(WxGsonBuilder.create().toJson(ImmutableMap.of("media_id", materialId))); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| this.logger.debug("响应原始数据:{}", responseContent); | this.logger.debug("响应原始数据:{}", responseContent); | ||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| @@ -17,6 +16,7 @@ import me.chanjar.weixin.mp.bean.material.WxMpMaterialUploadResult; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.FileNotFoundException; | import java.io.FileNotFoundException; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.util.Map; | import java.util.Map; | ||||
| /** | /** | ||||
| @@ -50,7 +50,7 @@ public class MaterialUploadJoddHttpRequestExecutor extends MaterialUploadRequest | |||||
| } | } | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| WxError error = WxError.fromJson(responseContent, WxType.MP); | WxError error = WxError.fromJson(responseContent, WxType.MP); | ||||
| if (error.getErrorCode() != 0) { | if (error.getErrorCode() != 0) { | ||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| @@ -13,6 +12,7 @@ import me.chanjar.weixin.common.util.http.RequestHttp; | |||||
| import me.chanjar.weixin.mp.bean.material.WxMpMaterialVideoInfoResult; | import me.chanjar.weixin.mp.bean.material.WxMpMaterialVideoInfoResult; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * Created by ecoolper on 2017/5/5. | * Created by ecoolper on 2017/5/5. | ||||
| @@ -32,7 +32,7 @@ public class MaterialVideoInfoJoddHttpRequestExecutor extends MaterialVideoInfoR | |||||
| request.query("media_id", materialId); | request.query("media_id", materialId); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| WxError error = WxError.fromJson(responseContent, WxType.MP); | WxError error = WxError.fromJson(responseContent, WxType.MP); | ||||
| if (error.getErrorCode() != 0) { | if (error.getErrorCode() != 0) { | ||||
| @@ -37,7 +37,7 @@ public class MaterialVoiceAndImageDownloadJoddHttpRequestExecutor extends Materi | |||||
| request.query("media_id", materialId); | request.query("media_id", materialId); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| try (InputStream inputStream = new ByteArrayInputStream(response.bodyBytes())) { | try (InputStream inputStream = new ByteArrayInputStream(response.bodyBytes())) { | ||||
| // 下载媒体文件出错 | // 下载媒体文件出错 | ||||
| byte[] responseContent = IOUtils.toByteArray(inputStream); | byte[] responseContent = IOUtils.toByteArray(inputStream); | ||||
| @@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.StringPool; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| @@ -14,6 +13,7 @@ import me.chanjar.weixin.mp.bean.material.WxMediaImgUploadResult; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| /** | /** | ||||
| * Created by ecoolper on 2017/5/5. | * Created by ecoolper on 2017/5/5. | ||||
| @@ -39,7 +39,7 @@ public class MediaImgUploadHttpRequestExecutor extends MediaImgUploadRequestExec | |||||
| request.form("media", data); | request.form("media", data); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| WxError error = WxError.fromJson(responseContent, WxType.MP); | WxError error = WxError.fromJson(responseContent, WxType.MP); | ||||
| if (error.getErrorCode() != 0) { | if (error.getErrorCode() != 0) { | ||||
| @@ -4,9 +4,8 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.MimeTypes; | |||||
| import jodd.util.StringPool; | |||||
| import jodd.net.MimeTypes; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| import me.chanjar.weixin.common.error.WxErrorException; | import me.chanjar.weixin.common.error.WxErrorException; | ||||
| @@ -19,6 +18,7 @@ import java.io.File; | |||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| import java.net.URLEncoder; | import java.net.URLEncoder; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.util.UUID; | import java.util.UUID; | ||||
| /** | /** | ||||
| @@ -47,7 +47,7 @@ public class QrCodeJoddHttpRequestExecutor extends QrCodeRequestExecutor<HttpCon | |||||
| request.withConnectionProvider(requestHttp.getRequestHttpClient()); | request.withConnectionProvider(requestHttp.getRequestHttpClient()); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String contentTypeHeader = response.header("Content-Type"); | String contentTypeHeader = response.header("Content-Type"); | ||||
| if (MimeTypes.MIME_TEXT_PLAIN.equals(contentTypeHeader)) { | if (MimeTypes.MIME_TEXT_PLAIN.equals(contentTypeHeader)) { | ||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| @@ -4,8 +4,7 @@ import jodd.http.HttpConnectionProvider; | |||||
| import jodd.http.HttpRequest; | import jodd.http.HttpRequest; | ||||
| import jodd.http.HttpResponse; | import jodd.http.HttpResponse; | ||||
| import jodd.http.ProxyInfo; | import jodd.http.ProxyInfo; | ||||
| import jodd.util.MimeTypes; | |||||
| import jodd.util.StringPool; | |||||
| import jodd.net.MimeTypes; | |||||
| import me.chanjar.weixin.common.WxType; | import me.chanjar.weixin.common.WxType; | ||||
| import me.chanjar.weixin.common.error.WxError; | import me.chanjar.weixin.common.error.WxError; | ||||
| import me.chanjar.weixin.common.error.WxErrorException; | import me.chanjar.weixin.common.error.WxErrorException; | ||||
| @@ -19,6 +18,7 @@ import java.io.File; | |||||
| import java.io.IOException; | import java.io.IOException; | ||||
| import java.io.InputStream; | import java.io.InputStream; | ||||
| import java.net.URLEncoder; | import java.net.URLEncoder; | ||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.util.UUID; | import java.util.UUID; | ||||
| /** | /** | ||||
| @@ -49,7 +49,7 @@ public class MaQrCodeJoddHttpRequestExecutor extends MaQrCodeRequestExecutor<Htt | |||||
| request.withConnectionProvider(requestHttp.getRequestHttpClient()); | request.withConnectionProvider(requestHttp.getRequestHttpClient()); | ||||
| HttpResponse response = request.send(); | HttpResponse response = request.send(); | ||||
| response.charset(StringPool.UTF_8); | |||||
| response.charset(StandardCharsets.UTF_8.name()); | |||||
| String contentTypeHeader = response.header("Content-Type"); | String contentTypeHeader = response.header("Content-Type"); | ||||
| if (MimeTypes.MIME_TEXT_PLAIN.equals(contentTypeHeader)) { | if (MimeTypes.MIME_TEXT_PLAIN.equals(contentTypeHeader)) { | ||||
| String responseContent = response.bodyText(); | String responseContent = response.bodyText(); | ||||
| @@ -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> | ||||
| @@ -0,0 +1,93 @@ | |||||
| package com.github.binarywang.wxpay.bean.businesscircle; | |||||
| import com.google.gson.annotations.SerializedName; | |||||
| import lombok.Data; | |||||
| import lombok.NoArgsConstructor; | |||||
| import java.io.Serializable; | |||||
| /** | |||||
| * 智慧商圈回调通知对象 | |||||
| * <pre> | |||||
| * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_1.shtml | |||||
| * https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_3.shtml | |||||
| * </pre> | |||||
| * | |||||
| * @author thinsstar | |||||
| */ | |||||
| @NoArgsConstructor | |||||
| @Data | |||||
| public class BusinessCircleNotifyData implements Serializable { | |||||
| private static final long serialVersionUID = 1L; | |||||
| /** | |||||
| * 通知ID | |||||
| */ | |||||
| @SerializedName("id") | |||||
| private String id; | |||||
| /** | |||||
| * 通知创建时间 | |||||
| */ | |||||
| @SerializedName("create_time") | |||||
| private String createTime; | |||||
| /** | |||||
| * 通知类型 | |||||
| */ | |||||
| @SerializedName("event_type") | |||||
| private String eventType; | |||||
| /** | |||||
| * 通知数据类型 | |||||
| */ | |||||
| @SerializedName("resource_type") | |||||
| private String resourceType; | |||||
| /** | |||||
| * 回调摘要 | |||||
| * summary | |||||
| */ | |||||
| @SerializedName("summary") | |||||
| private String summary; | |||||
| /** | |||||
| * 通知数据 | |||||
| */ | |||||
| @SerializedName("resource") | |||||
| private Resource resource; | |||||
| @Data | |||||
| public static class Resource implements Serializable { | |||||
| private static final long serialVersionUID = 1L; | |||||
| /** | |||||
| * 加密算法类型 | |||||
| */ | |||||
| @SerializedName("algorithm") | |||||
| private String algorithm; | |||||
| /** | |||||
| * 数据密文 | |||||
| */ | |||||
| @SerializedName("ciphertext") | |||||
| private String cipherText; | |||||
| /** | |||||
| * 附加数据 | |||||
| */ | |||||
| @SerializedName("associated_data") | |||||
| private String associatedData; | |||||
| /** | |||||
| * 随机串 | |||||
| */ | |||||
| @SerializedName("nonce") | |||||
| private String nonce; | |||||
| /** | |||||
| * 原始回调类型 | |||||
| */ | |||||
| @SerializedName("original_type") | |||||
| private String originalType; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,112 @@ | |||||
| package com.github.binarywang.wxpay.bean.businesscircle; | |||||
| import com.google.gson.annotations.SerializedName; | |||||
| import lombok.Data; | |||||
| import lombok.NoArgsConstructor; | |||||
| import java.io.Serializable; | |||||
| /** | |||||
| * 商圈支付结果通知内容 | |||||
| * <pre> | |||||
| * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_1.shtml | |||||
| * </pre> | |||||
| * | |||||
| * @author thinsstar | |||||
| */ | |||||
| @NoArgsConstructor | |||||
| @Data | |||||
| public class PaidResult implements Serializable { | |||||
| private static final long serialVersionUID = 1L; | |||||
| /** | |||||
| * 商户号 | |||||
| * <p> | |||||
| * 微信支付分配的商户号 | |||||
| * 示例值:1230000109 | |||||
| */ | |||||
| @SerializedName("mchid") | |||||
| private String mchid; | |||||
| /** | |||||
| * 商圈商户名称 | |||||
| * <p> | |||||
| * 商圈商户名称 | |||||
| * 示例值:微信支付 | |||||
| */ | |||||
| @SerializedName("merchant_name") | |||||
| private String merchantName; | |||||
| /** | |||||
| * 门店名称 | |||||
| * <p> | |||||
| * 门店名称,商圈在商圈小程序上圈店时填写的门店名称 | |||||
| * 示例值:微信支付 | |||||
| */ | |||||
| @SerializedName("shop_name") | |||||
| private String shopName; | |||||
| /** | |||||
| * 门店编号 | |||||
| * <p> | |||||
| * 门店编号,商圈在商圈小程序上圈店时填写的门店编号,用于跟商圈自身已有的商户识别码对齐 | |||||
| * 示例值:123456 | |||||
| */ | |||||
| @SerializedName("shop_number") | |||||
| private String shopNumber; | |||||
| /** | |||||
| * 小程序APPID | |||||
| * <p> | |||||
| * 顾客授权积分时使用的小程序的appid | |||||
| * 示例值:wxd678efh567hg6787 | |||||
| */ | |||||
| @SerializedName("appid") | |||||
| private String appid; | |||||
| /** | |||||
| * 用户标识 | |||||
| * <p> | |||||
| * 顾客授权时使用的小程序上的openid | |||||
| * 示例值:oUpF8uMuAJ2pxb1Q9zNjWeS6o | |||||
| */ | |||||
| @SerializedName("openid") | |||||
| private String openid; | |||||
| /** | |||||
| * 交易完成时间 | |||||
| * <p> | |||||
| * 交易完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒毫秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒(需要增加所有跟时间有关的参数的描述) | |||||
| * 示例值:2015-05-20T13:29:35+08:00 | |||||
| */ | |||||
| @SerializedName("time_end") | |||||
| private String timeEnd; | |||||
| /** | |||||
| * 金额 | |||||
| * <p> | |||||
| * 用户实际消费金额,单位(分) | |||||
| * 示例值:200 | |||||
| */ | |||||
| @SerializedName("amount") | |||||
| private Integer amount; | |||||
| /** | |||||
| * 微信支付订单号 | |||||
| * <p> | |||||
| * 微信支付订单号 | |||||
| * 示例值:1234567890 | |||||
| */ | |||||
| @SerializedName("transaction_id") | |||||
| private String transactionId; | |||||
| /** | |||||
| * 手动提交积分标记 | |||||
| * <p> | |||||
| * 手动提交积分标记,自动提交时无该字段,用于区分用户手动申请后推送的积分数据 | |||||
| * 示例值:oUpF8uMuAJ2pxb1Q9zNjWUHsd | |||||
| */ | |||||
| @SerializedName("commit_tag") | |||||
| private String commitTag; | |||||
| } | |||||
| @@ -0,0 +1,154 @@ | |||||
| package com.github.binarywang.wxpay.bean.businesscircle; | |||||
| import com.google.gson.annotations.SerializedName; | |||||
| import lombok.AllArgsConstructor; | |||||
| import lombok.Builder; | |||||
| import lombok.Data; | |||||
| import lombok.NoArgsConstructor; | |||||
| import java.io.Serializable; | |||||
| /** | |||||
| * 商圈积分同步 | |||||
| * <pre> | |||||
| * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_2.shtml | |||||
| * </pre> | |||||
| * | |||||
| * @author thinsstar | |||||
| */ | |||||
| @Data | |||||
| @Builder | |||||
| @NoArgsConstructor | |||||
| @AllArgsConstructor | |||||
| public class PointsNotifyRequest implements Serializable { | |||||
| private static final long serialVersionUID = 1L; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:商圈商户ID | |||||
| * 变量名:sub_mchid | |||||
| * 是否必填:否 | |||||
| * 类型:string[1,64] | |||||
| * 描述: | |||||
| * 当以服务商模式管理商圈积分能力时,则要带上商圈商户ID,否则留空 | |||||
| * 示例值:1234567890 | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "sub_mchid") | |||||
| private String subMchid; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:微信订单号 | |||||
| * 变量名:transaction_id | |||||
| * 是否必填:是 | |||||
| * 类型:string[1,64] | |||||
| * 描述: | |||||
| * 微信支付推送的商圈内交易通知里携带的微信订单号 | |||||
| * 示例值:1217752501201407033233368018 | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "transaction_id") | |||||
| private String transactionId; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:小程序appid | |||||
| * 变量名:appid | |||||
| * 是否必填:是 | |||||
| * 类型:string[1,128] | |||||
| * 描述: | |||||
| * 顾客授权积分时使用的小程序的appid | |||||
| * 示例值:wx1234567890abcdef | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "appid") | |||||
| private String appid; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:用户标识 | |||||
| * 变量名:openid | |||||
| * 是否必填:是 | |||||
| * 类型:string[1,64] | |||||
| * 描述: | |||||
| * 顾客授权时使用的小程序上的openid | |||||
| * 示例值:oWmnN4xxxxxxxxxxe92NHIGf1xd8 | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "openid") | |||||
| private String openid; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:是否获得积分 | |||||
| * 变量名:earn_points | |||||
| * 是否必填:是 | |||||
| * 类型:boolean | |||||
| * 描述: | |||||
| * 用于标明此单是否获得积分, | |||||
| * true为获得积分, | |||||
| * false为未获得 | |||||
| * 示例值:true | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "earn_points") | |||||
| private Boolean earnPoints; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:订单新增积分值 | |||||
| * 变量名:increased_points | |||||
| * 是否必填:是 | |||||
| * 类型:int | |||||
| * 描述: | |||||
| * 顾客此笔交易新增的积分值 | |||||
| * 示例值:100 | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "increased_points") | |||||
| private Integer increasedPoints; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:积分更新时间 | |||||
| * 变量名:points_update_time | |||||
| * 是否必填:是 | |||||
| * 类型:string[1,32] | |||||
| * 描述: | |||||
| * 为顾客此笔交易成功积分的时间 | |||||
| * 示例值:2020-05-20T13:29:35.120+08:00 | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "points_update_time") | |||||
| private String pointsUpdateTime; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:未获得积分的备注信息 | |||||
| * 变量名:no_points_remarks | |||||
| * 是否必填:否 | |||||
| * 类型:string[1,128] | |||||
| * 描述: | |||||
| * 当未获得积分时,提供未获得积分的原因等备注信息 | |||||
| * 示例值:商品不参与积分活动 | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "no_points_remarks") | |||||
| private String noPointsRemarks; | |||||
| /** | |||||
| * <pre> | |||||
| * 字段名:顾客积分总额 | |||||
| * 变量名:total_points | |||||
| * 是否必填:否 | |||||
| * 类型:int | |||||
| * 描述: | |||||
| * 当前顾客积分总额 | |||||
| * 示例值:888888 | |||||
| * </pre> | |||||
| */ | |||||
| @SerializedName(value = "total_points") | |||||
| private Integer totalPoints; | |||||
| } | |||||
| @@ -0,0 +1,121 @@ | |||||
| package com.github.binarywang.wxpay.bean.businesscircle; | |||||
| import com.google.gson.annotations.SerializedName; | |||||
| import lombok.Data; | |||||
| import lombok.NoArgsConstructor; | |||||
| import java.io.Serializable; | |||||
| /** | |||||
| * 商圈退款成功通知内容 | |||||
| * <pre> | |||||
| * 文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_3.shtml | |||||
| * </pre> | |||||
| * | |||||
| * @author thinsstar | |||||
| */ | |||||
| @NoArgsConstructor | |||||
| @Data | |||||
| public class RefundResult implements Serializable { | |||||
| private static final long serialVersionUID = 1L; | |||||
| /** | |||||
| * 商户号 | |||||
| * <p> | |||||
| * 微信支付分配的商户号 | |||||
| * 示例值:1230000109 | |||||
| */ | |||||
| @SerializedName("mchid") | |||||
| private String mchid; | |||||
| /** | |||||
| * 商圈商户名称 | |||||
| * <p> | |||||
| * 商圈商户名称 | |||||
| * 示例值:微信支付 | |||||
| */ | |||||
| @SerializedName("merchant_name") | |||||
| private String merchantName; | |||||
| /** | |||||
| * 门店名称 | |||||
| * <p> | |||||
| * 门店名称,商圈在商圈小程序上圈店时填写的门店名称 | |||||
| * 示例值:微信支付 | |||||
| */ | |||||
| @SerializedName("shop_name") | |||||
| private String shopName; | |||||
| /** | |||||
| * 门店编号 | |||||
| * <p> | |||||
| * 门店编号,商圈在商圈小程序上圈店时填写的门店编号,用于跟商圈自身已有的商户识别码对齐 | |||||
| * 示例值:123456 | |||||
| */ | |||||
| @SerializedName("shop_number") | |||||
| private String shopNumber; | |||||
| /** | |||||
| * 小程序APPID | |||||
| * <p> | |||||
| * 顾客授权积分时使用的小程序的appid | |||||
| * 示例值:wxd678efh567hg6787 | |||||
| */ | |||||
| @SerializedName("appid") | |||||
| private String appid; | |||||
| /** | |||||
| * 用户标识 | |||||
| * <p> | |||||
| * 顾客授权时使用的小程序上的openid | |||||
| * 示例值:oUpF8uMuAJ2pxb1Q9zNjWeS6o | |||||
| */ | |||||
| @SerializedName("openid") | |||||
| private String openid; | |||||
| /** | |||||
| * 退款完成时间 | |||||
| * <p> | |||||
| * 退款完成时间,遵循rfc3339标准格式,格式为YYYY-MM-DDTHH:mm:ss+TIMEZONE,YYYY-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss表示时分秒毫秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒(需要增加所有跟时间有关的参数的描述) | |||||
| * 示例值:2015-05-20T13:29:35+08:00 | |||||
| */ | |||||
| @SerializedName("refund_time") | |||||
| private String refundTime; | |||||
| /** | |||||
| * 消费金额 | |||||
| * <p> | |||||
| * 用户实际消费金额,单位(分) | |||||
| * 示例值:100 | |||||
| */ | |||||
| @SerializedName("pay_amount") | |||||
| private Integer payAmount; | |||||
| /** | |||||
| * 退款金额 | |||||
| * <p> | |||||
| * 用户退款金额,单位(分) | |||||
| * 示例值:100 | |||||
| */ | |||||
| @SerializedName("refund_amount") | |||||
| private Integer refundAmount; | |||||
| /** | |||||
| * 微信支付订单号 | |||||
| * <p> | |||||
| * 微信支付订单号 | |||||
| * 示例值:1234567890 | |||||
| */ | |||||
| @SerializedName("transaction_id") | |||||
| private String transactionId; | |||||
| /** | |||||
| * 微信支付退款单号 | |||||
| * <p> | |||||
| * 微信支付退款单号 | |||||
| * 示例值:1217752501201407033233368999 | |||||
| */ | |||||
| @SerializedName("refund_id") | |||||
| private String refundId; | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| package com.github.binarywang.wxpay.bean.ecommerce; | |||||
| import lombok.Data; | |||||
| import lombok.NoArgsConstructor; | |||||
| import java.io.Serializable; | |||||
| /** | |||||
| * 微信通知接口头部信息,需要做签名验证 | |||||
| * 文档地址: https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/qian-ming-yan-zheng | |||||
| */ | |||||
| @Data | |||||
| @NoArgsConstructor | |||||
| public class SignatureHeader implements Serializable { | |||||
| private static final long serialVersionUID = -6958015499416059949L; | |||||
| /** | |||||
| * 时间戳 | |||||
| */ | |||||
| private String timeStamp; | |||||
| /** | |||||
| * 随机串 | |||||
| */ | |||||
| private String nonce; | |||||
| /** | |||||
| * 已签名字符串 | |||||
| */ | |||||
| private String signed; | |||||
| /** | |||||
| * 证书序列号 | |||||
| */ | |||||
| private String serialNo; | |||||
| } | |||||
| @@ -1,15 +1,27 @@ | |||||
| 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.WxPayV3HttpClientBuilder; | |||||
| import com.github.binarywang.wxpay.v3.auth.*; | |||||
| import com.github.binarywang.wxpay.v3.util.PemUtils; | |||||
| import jodd.util.ResourcesUtil; | |||||
| import lombok.Data; | import lombok.Data; | ||||
| import lombok.EqualsAndHashCode; | |||||
| import lombok.SneakyThrows; | |||||
| import org.apache.commons.io.IOUtils; | import org.apache.commons.io.IOUtils; | ||||
| import org.apache.commons.lang3.RegExUtils; | |||||
| 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.nio.charset.StandardCharsets; | |||||
| import java.security.KeyStore; | import java.security.KeyStore; | ||||
| import java.security.PrivateKey; | |||||
| import java.security.cert.X509Certificate; | |||||
| import java.util.Collections; | |||||
| /** | /** | ||||
| * 微信支付配置 | * 微信支付配置 | ||||
| @@ -17,8 +29,11 @@ import java.security.KeyStore; | |||||
| * @author Binary Wang (https://github.com/binarywang) | * @author Binary Wang (https://github.com/binarywang) | ||||
| */ | */ | ||||
| @Data | @Data | ||||
| @EqualsAndHashCode(exclude = "verifier") | |||||
| public class WxPayConfig { | public class WxPayConfig { | ||||
| private static final String DEFAULT_PAY_BASE_URL = "https://api.mch.weixin.qq.com"; | private static final String DEFAULT_PAY_BASE_URL = "https://api.mch.weixin.qq.com"; | ||||
| private static final String PROBLEM_MSG = "证书文件【%s】有问题,请核实!"; | |||||
| private static final String NOT_FOUND_MSG = "证书文件【%s】不存在,请核实!"; | |||||
| /** | /** | ||||
| * 微信支付接口请求地址域名部分. | * 微信支付接口请求地址域名部分. | ||||
| @@ -85,6 +100,52 @@ 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 String payScorePermissionNotifyUrl; | |||||
| private CloseableHttpClient apiV3HttpClient; | |||||
| /** | |||||
| * 私钥信息 | |||||
| */ | |||||
| private PrivateKey privateKey; | |||||
| /** | |||||
| * 证书自动更新时间差(分钟),默认一分钟 | |||||
| */ | |||||
| private int certAutoUpdateTime = 60; | |||||
| /** | /** | ||||
| * p12证书文件内容的字节数组. | * p12证书文件内容的字节数组. | ||||
| */ | */ | ||||
| @@ -106,8 +167,15 @@ public class WxPayConfig { | |||||
| private String httpProxyUsername; | private String httpProxyUsername; | ||||
| private String httpProxyPassword; | private String httpProxyPassword; | ||||
| /** | |||||
| * v3接口下证书检验对象,通过改对象可以获取到X509Certificate,进一步对敏感信息加密 | |||||
| * 文档见 https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi | |||||
| */ | |||||
| private Verifier verifier; | |||||
| /** | /** | ||||
| * 返回所设置的微信支付接口请求地址域名. | * 返回所设置的微信支付接口请求地址域名. | ||||
| * | |||||
| * @return 微信支付接口请求地址域名 | * @return 微信支付接口请求地址域名 | ||||
| */ | */ | ||||
| public String getPayBaseUrl() { | public String getPayBaseUrl() { | ||||
| @@ -118,6 +186,15 @@ public class WxPayConfig { | |||||
| return this.payBaseUrl; | return this.payBaseUrl; | ||||
| } | } | ||||
| @SneakyThrows | |||||
| public Verifier getVerifier() { | |||||
| if (verifier == null) { | |||||
| //当改对象为null时,初始化api v3的请求头 | |||||
| initApiV3HttpClient(); | |||||
| } | |||||
| return verifier; | |||||
| } | |||||
| /** | /** | ||||
| * 初始化ssl. | * 初始化ssl. | ||||
| * | * | ||||
| @@ -136,40 +213,7 @@ public class WxPayConfig { | |||||
| if (StringUtils.isBlank(this.getKeyPath())) { | if (StringUtils.isBlank(this.getKeyPath())) { | ||||
| throw new WxPayException("请确保证书文件地址keyPath已配置"); | throw new WxPayException("请确保证书文件地址keyPath已配置"); | ||||
| } | } | ||||
| final String prefix = "classpath:"; | |||||
| String fileHasProblemMsg = "证书文件【" + this.getKeyPath() + "】有问题,请核实!"; | |||||
| String fileNotFoundMsg = "证书文件【" + this.getKeyPath() + "】不存在,请核实!"; | |||||
| if (this.getKeyPath().startsWith(prefix)) { | |||||
| String path = StringUtils.removeFirst(this.getKeyPath(), prefix); | |||||
| if (!path.startsWith("/")) { | |||||
| path = "/" + path; | |||||
| } | |||||
| inputStream = WxPayConfig.class.getResourceAsStream(path); | |||||
| if (inputStream == null) { | |||||
| throw new WxPayException(fileNotFoundMsg); | |||||
| } | |||||
| } else if (this.getKeyPath().startsWith("http://") || this.getKeyPath().startsWith("https://")) { | |||||
| try { | |||||
| inputStream = new URL(this.keyPath).openStream(); | |||||
| if (inputStream == null) { | |||||
| throw new WxPayException(fileNotFoundMsg); | |||||
| } | |||||
| } catch (IOException e) { | |||||
| throw new WxPayException(fileNotFoundMsg, e); | |||||
| } | |||||
| } else { | |||||
| try { | |||||
| File file = new File(this.getKeyPath()); | |||||
| if (!file.exists()) { | |||||
| throw new WxPayException(fileNotFoundMsg); | |||||
| } | |||||
| inputStream = new FileInputStream(file); | |||||
| } catch (IOException e) { | |||||
| throw new WxPayException(fileHasProblemMsg, e); | |||||
| } | |||||
| } | |||||
| inputStream = this.loadConfigInputStream(this.getKeyPath()); | |||||
| } | } | ||||
| try { | try { | ||||
| @@ -185,4 +229,104 @@ public class WxPayConfig { | |||||
| } | } | ||||
| } | } | ||||
| /** | |||||
| * 初始化api v3请求头 自动签名验签 | |||||
| * 方法参照微信官方https://github.com/wechatpay-apiv3/wechatpay-apache-httpclient | |||||
| * | |||||
| * @return org.apache.http.impl.client.CloseableHttpClient | |||||
| * @author doger.wang | |||||
| **/ | |||||
| public CloseableHttpClient initApiV3HttpClient() throws WxPayException { | |||||
| String privateKeyPath = this.getPrivateKeyPath(); | |||||
| String privateCertPath = this.getPrivateCertPath(); | |||||
| String serialNo = 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 = this.loadConfigInputStream(privateKeyPath); | |||||
| InputStream certInputStream = this.loadConfigInputStream(privateCertPath); | |||||
| try { | |||||
| PrivateKey merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream); | |||||
| X509Certificate certificate = PemUtils.loadCertificate(certInputStream); | |||||
| if(StringUtils.isBlank(serialNo)){ | |||||
| this.certSerialNo = certificate.getSerialNumber().toString(16).toUpperCase(); | |||||
| } | |||||
| AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier( | |||||
| new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)), | |||||
| apiV3Key.getBytes(StandardCharsets.UTF_8), this.getCertAutoUpdateTime()); | |||||
| CloseableHttpClient httpClient = WxPayV3HttpClientBuilder.create() | |||||
| .withMerchant(mchId, certSerialNo, merchantPrivateKey) | |||||
| .withWechatpay(Collections.singletonList(certificate)) | |||||
| .withValidator(new WxPayValidator(verifier)) | |||||
| .build(); | |||||
| this.apiV3HttpClient = httpClient; | |||||
| this.verifier=verifier; | |||||
| this.privateKey = merchantPrivateKey; | |||||
| return httpClient; | |||||
| } catch (Exception e) { | |||||
| throw new WxPayException("v3请求构造异常!", e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 从配置路径 加载配置 信息(支持 classpath、本地路径、网络url) | |||||
| * @param configPath 配置路径 | |||||
| * @return | |||||
| * @throws WxPayException | |||||
| */ | |||||
| private InputStream loadConfigInputStream(String configPath) throws WxPayException { | |||||
| InputStream inputStream; | |||||
| final String prefix = "classpath:"; | |||||
| String fileHasProblemMsg = String.format(PROBLEM_MSG, configPath); | |||||
| String fileNotFoundMsg = String.format(NOT_FOUND_MSG, configPath); | |||||
| if (configPath.startsWith(prefix)) { | |||||
| String path = RegExUtils.removeFirst(configPath, prefix); | |||||
| if (!path.startsWith("/")) { | |||||
| path = "/" + path; | |||||
| } | |||||
| try { | |||||
| inputStream = ResourcesUtil.getResourceAsStream(path); | |||||
| if (inputStream == null) { | |||||
| throw new WxPayException(fileNotFoundMsg); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| throw new WxPayException(fileNotFoundMsg, e); | |||||
| } | |||||
| } else if (configPath.startsWith("http://") || configPath.startsWith("https://")) { | |||||
| try { | |||||
| inputStream = new URL(configPath).openStream(); | |||||
| if (inputStream == null) { | |||||
| throw new WxPayException(fileNotFoundMsg); | |||||
| } | |||||
| } catch (IOException e) { | |||||
| throw new WxPayException(fileNotFoundMsg, e); | |||||
| } | |||||
| } else { | |||||
| try { | |||||
| File file = new File(configPath); | |||||
| if (!file.exists()) { | |||||
| throw new WxPayException(fileNotFoundMsg); | |||||
| } | |||||
| inputStream = new FileInputStream(file); | |||||
| } catch (IOException e) { | |||||
| throw new WxPayException(fileHasProblemMsg, e); | |||||
| } | |||||
| } | |||||
| return inputStream; | |||||
| } | |||||
| } | } | ||||
| @@ -0,0 +1,35 @@ | |||||
| package com.github.binarywang.wxpay.service; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.BusinessCircleNotifyData; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.PaidResult; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.RefundResult; | |||||
| import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader; | |||||
| import com.github.binarywang.wxpay.exception.WxPayException; | |||||
| /** | |||||
| * <pre> | |||||
| * 微信支付智慧商圈API | |||||
| * </pre> | |||||
| * | |||||
| * @author thinsstar | |||||
| */ | |||||
| public interface BusinessCircleService { | |||||
| /** | |||||
| * <pre> | |||||
| * 智慧商圈接口-商圈积分同步API | |||||
| * 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/businesscircle/chapter3_2.shtml | |||||
| * 接口链接:https://api.mch.weixin.qq.com/v3/businesscircle/points/notify | |||||
| * </pre> | |||||
| * | |||||
| * @param request 请求对象 | |||||
| * @throws WxPayException the wx pay exception | |||||
| */ | |||||
| void notifyPoints(PointsNotifyRequest request) throws WxPayException; | |||||
| BusinessCircleNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException; | |||||
| PaidResult decryptPaidNotifyDataResource(BusinessCircleNotifyData data) throws WxPayException; | |||||
| RefundResult decryptRefundNotifyDataResource(BusinessCircleNotifyData data) throws WxPayException; | |||||
| } | |||||
| @@ -9,8 +9,11 @@ import com.github.binarywang.wxpay.bean.request.*; | |||||
| import com.github.binarywang.wxpay.bean.result.*; | import com.github.binarywang.wxpay.bean.result.*; | ||||
| import com.github.binarywang.wxpay.config.WxPayConfig; | import com.github.binarywang.wxpay.config.WxPayConfig; | ||||
| import com.github.binarywang.wxpay.exception.WxPayException; | import com.github.binarywang.wxpay.exception.WxPayException; | ||||
| import org.apache.http.client.methods.HttpPost; | |||||
| import org.apache.http.client.methods.HttpRequestBase; | |||||
| import java.io.File; | import java.io.File; | ||||
| import java.io.InputStream; | |||||
| import java.util.Date; | import java.util.Date; | ||||
| import java.util.Map; | import java.util.Map; | ||||
| @@ -53,6 +56,66 @@ 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; | |||||
| /** | |||||
| * 发送post请求,得到响应字符串. | |||||
| * <p> | |||||
| * 部分字段会包含敏感信息,所以在提交前需要在请求头中会包含"Wechatpay-Serial"信息 | |||||
| * | |||||
| * @param url 请求地址 | |||||
| * @param requestStr 请求信息 | |||||
| * @return 返回请求结果字符串 string | |||||
| * @throws WxPayException the wx pay exception | |||||
| */ | |||||
| String postV3WithWechatpaySerial(String url, String requestStr) throws WxPayException; | |||||
| /** | |||||
| * 发送post请求,得到响应字符串. | |||||
| * | |||||
| * @param url 请求地址 | |||||
| * @param httpPost 请求信息 | |||||
| * @return 返回请求结果字符串 string | |||||
| * @throws WxPayException the wx pay exception | |||||
| */ | |||||
| String postV3(String url, HttpPost httpPost) throws WxPayException; | |||||
| /** | |||||
| * 发送http请求,得到响应字符串. | |||||
| * | |||||
| * @param url 请求地址 | |||||
| * @param httpRequest 请求信息,可以是put,post,get,delete等请求 | |||||
| * @return 返回请求结果字符串 string | |||||
| * @throws WxPayException the wx pay exception | |||||
| */ | |||||
| String requestV3(String url, HttpRequestBase httpRequest) throws WxPayException; | |||||
| /** | |||||
| * 发送get V3请求,得到响应字符串. | |||||
| * | |||||
| * @param url 请求地址 | |||||
| * @return 返回请求结果字符串 string | |||||
| * @throws WxPayException the wx pay exception | |||||
| */ | |||||
| String getV3(String url) throws WxPayException; | |||||
| /** | |||||
| * 发送下载 V3请求,得到响应流. | |||||
| * | |||||
| * @param url 请求地址 | |||||
| * @return 返回请求响应流 input stream | |||||
| * @throws WxPayException the wx pay exception | |||||
| */ | |||||
| InputStream downloadV3(String url) throws WxPayException; | |||||
| /** | /** | ||||
| * 获取企业付款服务类. | * 获取企业付款服务类. | ||||
| * | * | ||||
| @@ -74,6 +137,13 @@ public interface WxPayService { | |||||
| */ | */ | ||||
| ProfitSharingService getProfitSharingService(); | ProfitSharingService getProfitSharingService(); | ||||
| /** | |||||
| * 获取微信支付智慧商圈服务类 | |||||
| * | |||||
| * @return the business circle service | |||||
| */ | |||||
| BusinessCircleService getBusinessCircleService(); | |||||
| /** | /** | ||||
| * 设置企业付款服务类,允许开发者自定义实现类. | * 设置企业付款服务类,允许开发者自定义实现类. | ||||
| * | * | ||||
| @@ -16,10 +16,7 @@ import com.github.binarywang.wxpay.config.WxPayConfig; | |||||
| 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; | ||||
| @@ -63,6 +60,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 BusinessCircleService businessCircleService = new BusinessCircleServiceImpl(this); | |||||
| /** | /** | ||||
| * The Config. | * The Config. | ||||
| @@ -79,6 +77,11 @@ public abstract class BaseWxPayServiceImpl implements WxPayService { | |||||
| return profitSharingService; | return profitSharingService; | ||||
| } | } | ||||
| @Override | |||||
| public BusinessCircleService getBusinessCircleService() { | |||||
| return this.businessCircleService; | |||||
| } | |||||
| @Override | @Override | ||||
| public RedpackService getRedpackService() { | public RedpackService getRedpackService() { | ||||
| return this.redpackService; | return this.redpackService; | ||||
| @@ -0,0 +1,89 @@ | |||||
| package com.github.binarywang.wxpay.service.impl; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.BusinessCircleNotifyData; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.PaidResult; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.RefundResult; | |||||
| import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader; | |||||
| import com.github.binarywang.wxpay.exception.WxPayException; | |||||
| import com.github.binarywang.wxpay.service.BusinessCircleService; | |||||
| import com.github.binarywang.wxpay.service.WxPayService; | |||||
| import com.github.binarywang.wxpay.v3.util.AesUtils; | |||||
| import com.github.binarywang.wxpay.v3.util.RsaCryptoUtil; | |||||
| import com.google.gson.Gson; | |||||
| import com.google.gson.GsonBuilder; | |||||
| import lombok.RequiredArgsConstructor; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import java.io.IOException; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.security.GeneralSecurityException; | |||||
| import java.util.Objects; | |||||
| /** | |||||
| * 微信支付-微信支付智慧商圈service | |||||
| * | |||||
| * @author thinsstar | |||||
| */ | |||||
| @Slf4j | |||||
| @RequiredArgsConstructor | |||||
| public class BusinessCircleServiceImpl implements BusinessCircleService { | |||||
| private static final Gson GSON = new GsonBuilder().create(); | |||||
| private final WxPayService payService; | |||||
| @Override | |||||
| public void notifyPoints(PointsNotifyRequest request) throws WxPayException { | |||||
| String url = String.format("%s/v3/businesscircle/points/notify", this.payService.getPayBaseUrl()); | |||||
| RsaCryptoUtil.encryptFields(request, this.payService.getConfig().getVerifier().getValidCertificate()); | |||||
| this.payService.postV3WithWechatpaySerial(url, GSON.toJson(request)); | |||||
| } | |||||
| /** | |||||
| * 校验通知签名 | |||||
| * | |||||
| * @param header 通知头信息 | |||||
| * @param data 通知数据 | |||||
| * @return true:校验通过 false:校验不通过 | |||||
| */ | |||||
| private boolean verifyNotifySign(SignatureHeader header, String data) { | |||||
| String beforeSign = String.format("%s%n%s%n%s%n", header.getTimeStamp(), header.getNonce(), data); | |||||
| return payService.getConfig().getVerifier().verify(header.getSerialNo(), | |||||
| beforeSign.getBytes(StandardCharsets.UTF_8), header.getSigned()); | |||||
| } | |||||
| @Override | |||||
| public BusinessCircleNotifyData parseNotifyData(String data, SignatureHeader header) throws WxPayException { | |||||
| if (Objects.nonNull(header) && !this.verifyNotifySign(header, data)) { | |||||
| throw new WxPayException("非法请求,头部信息验证失败"); | |||||
| } | |||||
| return GSON.fromJson(data, BusinessCircleNotifyData.class); | |||||
| } | |||||
| @Override | |||||
| public PaidResult decryptPaidNotifyDataResource(BusinessCircleNotifyData data) throws WxPayException { | |||||
| BusinessCircleNotifyData.Resource resource = data.getResource(); | |||||
| String cipherText = resource.getCipherText(); | |||||
| String associatedData = resource.getAssociatedData(); | |||||
| String nonce = resource.getNonce(); | |||||
| String apiV3Key = this.payService.getConfig().getApiV3Key(); | |||||
| try { | |||||
| return GSON.fromJson(AesUtils.decryptToString(associatedData, nonce, cipherText, apiV3Key), PaidResult.class); | |||||
| } catch (GeneralSecurityException | IOException e) { | |||||
| throw new WxPayException("解析报文异常!", e); | |||||
| } | |||||
| } | |||||
| @Override | |||||
| public RefundResult decryptRefundNotifyDataResource(BusinessCircleNotifyData data) throws WxPayException { | |||||
| BusinessCircleNotifyData.Resource resource = data.getResource(); | |||||
| String cipherText = resource.getCipherText(); | |||||
| String associatedData = resource.getAssociatedData(); | |||||
| String nonce = resource.getNonce(); | |||||
| String apiV3Key = this.payService.getConfig().getApiV3Key(); | |||||
| try { | |||||
| return GSON.fromJson(AesUtils.decryptToString(associatedData, nonce, cipherText, apiV3Key), RefundResult.class); | |||||
| } catch (GeneralSecurityException | IOException e) { | |||||
| throw new WxPayException("解析报文异常!", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -1,26 +1,31 @@ | |||||
| package com.github.binarywang.wxpay.service.impl; | package com.github.binarywang.wxpay.service.impl; | ||||
| import java.io.InputStream; | |||||
| import java.io.UnsupportedEncodingException; | import java.io.UnsupportedEncodingException; | ||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||
| import javax.net.ssl.SSLContext; | import javax.net.ssl.SSLContext; | ||||
| 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.WxPayRedpackQueryRequest; | |||||
| import com.github.binarywang.wxpay.bean.result.WxPayCommonResult; | |||||
| import com.github.binarywang.wxpay.bean.result.WxPayRedpackQueryResult; | |||||
| import com.github.binarywang.wxpay.exception.WxPayException; | import com.github.binarywang.wxpay.exception.WxPayException; | ||||
| import com.google.gson.JsonElement; | |||||
| import com.google.gson.JsonObject; | |||||
| import me.chanjar.weixin.common.util.json.GsonParser; | |||||
| import jodd.util.Base64; | import jodd.util.Base64; | ||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
| import org.apache.http.HttpEntity; | |||||
| 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.client.methods.HttpRequestBase; | |||||
| 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; | ||||
| @@ -28,9 +33,6 @@ import org.apache.http.impl.client.HttpClientBuilder; | |||||
| import org.apache.http.impl.client.HttpClients; | import org.apache.http.impl.client.HttpClients; | ||||
| import org.apache.http.util.EntityUtils; | import org.apache.http.util.EntityUtils; | ||||
| import com.github.binarywang.wxpay.bean.WxPayApiData; | |||||
| import com.github.binarywang.wxpay.exception.WxPayException; | |||||
| import jodd.util.Base64; | |||||
| /** | /** | ||||
| * <pre> | * <pre> | ||||
| @@ -90,6 +92,155 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl { | |||||
| } | } | ||||
| } | } | ||||
| @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(); | |||||
| //post方法有可能会没有返回值的情况 | |||||
| String responseString; | |||||
| if (response.getEntity() == null) { | |||||
| responseString = null; | |||||
| } else { | |||||
| 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 = GsonParser.parse(responseString); | |||||
| throw convertException(jsonObject); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage()); | |||||
| throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); | |||||
| } finally { | |||||
| httpPost.releaseConnection(); | |||||
| } | |||||
| } | |||||
| @Override | |||||
| public String postV3WithWechatpaySerial(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"); | |||||
| String serialNumber = getConfig().getVerifier().getValidCertificate().getSerialNumber().toString(16).toUpperCase(); | |||||
| httpPost.addHeader("Wechatpay-Serial", serialNumber); | |||||
| try (CloseableHttpResponse response = httpClient.execute(httpPost)) { | |||||
| //v3已经改为通过状态码判断200 204 成功 | |||||
| int statusCode = response.getStatusLine().getStatusCode(); | |||||
| String responseString = "{}"; | |||||
| HttpEntity entity = response.getEntity(); | |||||
| if (entity != null) { | |||||
| responseString = EntityUtils.toString(entity, 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 = GsonParser.parse(responseString); | |||||
| throw convertException(jsonObject); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| this.log.error("\n【请求地址】:{}\n【请求数据】:{}\n【异常信息】:{}", url, requestStr, e.getMessage()); | |||||
| e.printStackTrace(); | |||||
| throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); | |||||
| } finally { | |||||
| httpPost.releaseConnection(); | |||||
| } | |||||
| } | |||||
| @Override | |||||
| public String postV3(String url, HttpPost httpPost) throws WxPayException { | |||||
| return this.requestV3(url, httpPost); | |||||
| } | |||||
| @Override | |||||
| public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayException { | |||||
| httpRequest.setConfig(RequestConfig.custom() | |||||
| .setConnectionRequestTimeout(this.getConfig().getHttpConnectionTimeout()) | |||||
| .setConnectTimeout(this.getConfig().getHttpConnectionTimeout()) | |||||
| .setSocketTimeout(this.getConfig().getHttpTimeout()) | |||||
| .build()); | |||||
| CloseableHttpClient httpClient = this.createApiV3HttpClient(); | |||||
| try (CloseableHttpResponse response = httpClient.execute(httpRequest)) { | |||||
| //v3已经改为通过状态码判断200 204 成功 | |||||
| int statusCode = response.getStatusLine().getStatusCode(); | |||||
| //post方法有可能会没有返回值的情况 | |||||
| String responseString; | |||||
| if (response.getEntity() == null) { | |||||
| responseString = null; | |||||
| } else { | |||||
| 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 = GsonParser.parse(responseString); | |||||
| throw convertException(jsonObject); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| this.log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage()); | |||||
| throw (e instanceof WxPayException) ? (WxPayException) e : new WxPayException(e.getMessage(), e); | |||||
| } finally { | |||||
| httpRequest.releaseConnection(); | |||||
| } | |||||
| } | |||||
| @Override | |||||
| public String getV3(String url) throws WxPayException { | |||||
| HttpGet httpGet = new HttpGet(url); | |||||
| httpGet.addHeader("Accept", "application/json"); | |||||
| httpGet.addHeader("Content-Type", "application/json"); | |||||
| return this.requestV3(url.toString(), httpGet); | |||||
| } | |||||
| @Override | |||||
| public InputStream downloadV3(String url) throws WxPayException { | |||||
| CloseableHttpClient httpClient = this.createApiV3HttpClient(); | |||||
| HttpGet httpGet = new HttpGet(url); | |||||
| httpGet.addHeader("Accept", ContentType.WILDCARD.getMimeType()); | |||||
| try (CloseableHttpResponse response = httpClient.execute(httpGet)) { | |||||
| //v3已经改为通过状态码判断200 204 成功 | |||||
| int statusCode = response.getStatusLine().getStatusCode(); | |||||
| if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) { | |||||
| this.log.info("\n【请求地址】:{}\n", url); | |||||
| return response.getEntity().getContent(); | |||||
| } else { | |||||
| //有错误提示信息返回 | |||||
| String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); | |||||
| JsonObject jsonObject = GsonParser.parse(responseString); | |||||
| throw convertException(jsonObject); | |||||
| } | |||||
| } catch (Exception e) { | |||||
| this.log.error("\n【请求地址】:{}\n【异常信息】:{}", url, e.getMessage()); | |||||
| throw (e instanceof WxPayException) ? (WxPayException) e : 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) { | private StringEntity createEntry(String requestStr) { | ||||
| try { | try { | ||||
| return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); | return new StringEntity(new String(requestStr.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1)); | ||||
| @@ -145,4 +296,15 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl { | |||||
| httpClientBuilder.setSSLSocketFactory(connectionSocketFactory); | httpClientBuilder.setSSLSocketFactory(connectionSocketFactory); | ||||
| } | } | ||||
| private WxPayException convertException(JsonObject jsonObject) { | |||||
| //todo 这里考虑使用新的适用于V3的异常 | |||||
| JsonElement codeElement = jsonObject.get("code"); | |||||
| String code = codeElement == null ? null : codeElement.getAsString(); | |||||
| String message = jsonObject.get("message").getAsString(); | |||||
| WxPayException wxPayException = new WxPayException(message); | |||||
| wxPayException.setErrCode(code); | |||||
| wxPayException.setErrCodeDes(message); | |||||
| return wxPayException; | |||||
| } | |||||
| } | } | ||||
| @@ -1,5 +1,6 @@ | |||||
| package com.github.binarywang.wxpay.service.impl; | package com.github.binarywang.wxpay.service.impl; | ||||
| import java.io.InputStream; | |||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||
| import javax.net.ssl.SSLContext; | import javax.net.ssl.SSLContext; | ||||
| @@ -20,6 +21,8 @@ import jodd.http.net.SSLSocketHttpConnectionProvider; | |||||
| import jodd.http.net.SocketHttpConnectionProvider; | import jodd.http.net.SocketHttpConnectionProvider; | ||||
| import jodd.util.Base64; | import jodd.util.Base64; | ||||
| import org.apache.commons.lang3.StringUtils; | import org.apache.commons.lang3.StringUtils; | ||||
| import org.apache.http.client.methods.HttpPost; | |||||
| import org.apache.http.client.methods.HttpRequestBase; | |||||
| import javax.net.ssl.SSLContext; | import javax.net.ssl.SSLContext; | ||||
| import java.nio.charset.StandardCharsets; | import java.nio.charset.StandardCharsets; | ||||
| @@ -67,6 +70,36 @@ public class WxPayServiceJoddHttpImpl extends BaseWxPayServiceImpl { | |||||
| } | } | ||||
| } | } | ||||
| @Override | |||||
| public String postV3(String url, String requestStr) throws WxPayException { | |||||
| return null; | |||||
| } | |||||
| @Override | |||||
| public String postV3WithWechatpaySerial(String url, String requestStr) throws WxPayException { | |||||
| return null; | |||||
| } | |||||
| @Override | |||||
| public String postV3(String url, HttpPost httpPost) throws WxPayException { | |||||
| return null; | |||||
| } | |||||
| @Override | |||||
| public String requestV3(String url, HttpRequestBase httpRequest) throws WxPayException { | |||||
| return null; | |||||
| } | |||||
| @Override | |||||
| public String getV3(String url) throws WxPayException { | |||||
| return null; | |||||
| } | |||||
| @Override | |||||
| public InputStream downloadV3(String 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,12 @@ | |||||
| package com.github.binarywang.wxpay.v3; | |||||
| import org.apache.http.client.methods.HttpRequestWrapper; | |||||
| import java.io.IOException; | |||||
| public interface Credentials { | |||||
| String getSchema(); | |||||
| String getToken(HttpRequestWrapper request) throws IOException; | |||||
| } | |||||
| @@ -0,0 +1,89 @@ | |||||
| package com.github.binarywang.wxpay.v3; | |||||
| import org.apache.http.HttpEntity; | |||||
| import org.apache.http.HttpEntityEnclosingRequest; | |||||
| import org.apache.http.HttpException; | |||||
| import org.apache.http.StatusLine; | |||||
| import org.apache.http.client.methods.CloseableHttpResponse; | |||||
| import org.apache.http.client.methods.HttpExecutionAware; | |||||
| import org.apache.http.client.methods.HttpRequestWrapper; | |||||
| import org.apache.http.client.protocol.HttpClientContext; | |||||
| import org.apache.http.conn.routing.HttpRoute; | |||||
| import org.apache.http.entity.BufferedHttpEntity; | |||||
| import org.apache.http.entity.ByteArrayEntity; | |||||
| import org.apache.http.impl.execchain.ClientExecChain; | |||||
| import org.apache.http.util.EntityUtils; | |||||
| import java.io.IOException; | |||||
| 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(HttpRequestWrapper request) throws IOException { | |||||
| if (request instanceof HttpEntityEnclosingRequest) { | |||||
| HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity(); | |||||
| if (entity != null) { | |||||
| ((HttpEntityEnclosingRequest) request).setEntity(new BufferedHttpEntity(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 { | |||||
| // 上传类不需要消耗两次故不做转换 | |||||
| if (!(request.getOriginal() instanceof WechatPayUploadHttpPost)) { | |||||
| convertToRepeatableRequestEntity(request); | |||||
| } | |||||
| // 添加认证信息 | |||||
| request.addHeader("Authorization", | |||||
| credentials.getSchema() + " " + credentials.getToken(request)); | |||||
| // 执行 | |||||
| CloseableHttpResponse response = mainExec.execute(route, request, 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,16 @@ | |||||
| package com.github.binarywang.wxpay.v3; | |||||
| import java.lang.annotation.ElementType; | |||||
| import java.lang.annotation.Retention; | |||||
| import java.lang.annotation.RetentionPolicy; | |||||
| import java.lang.annotation.Target; | |||||
| /** | |||||
| * 敏感信息字段 | |||||
| * @author zhouyognshen | |||||
| **/ | |||||
| @Target({ElementType.FIELD}) | |||||
| @Retention(RetentionPolicy.RUNTIME) | |||||
| public @interface SpecEncrypt { | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| package com.github.binarywang.wxpay.v3; | |||||
| import org.apache.http.client.methods.CloseableHttpResponse; | |||||
| import java.io.IOException; | |||||
| public interface Validator { | |||||
| boolean validate(CloseableHttpResponse response) throws IOException; | |||||
| } | |||||
| @@ -0,0 +1,76 @@ | |||||
| package com.github.binarywang.wxpay.v3; | |||||
| import org.apache.http.client.methods.HttpPost; | |||||
| import org.apache.http.entity.ContentType; | |||||
| import org.apache.http.entity.mime.HttpMultipartMode; | |||||
| import org.apache.http.entity.mime.MultipartEntityBuilder; | |||||
| import java.io.InputStream; | |||||
| import java.net.URI; | |||||
| import java.net.URLConnection; | |||||
| public class WechatPayUploadHttpPost extends HttpPost { | |||||
| private String meta; | |||||
| private WechatPayUploadHttpPost(URI uri, String meta) { | |||||
| super(uri); | |||||
| this.meta = meta; | |||||
| } | |||||
| public String getMeta() { | |||||
| return meta; | |||||
| } | |||||
| public static class Builder { | |||||
| private String fileName; | |||||
| private String fileSha256; | |||||
| private InputStream fileInputStream; | |||||
| private ContentType fileContentType; | |||||
| private URI uri; | |||||
| public Builder(URI uri) { | |||||
| this.uri = uri; | |||||
| } | |||||
| public Builder withImage(String fileName, String fileSha256, InputStream inputStream) { | |||||
| this.fileName = fileName; | |||||
| this.fileSha256 = fileSha256; | |||||
| this.fileInputStream = inputStream; | |||||
| String mimeType = URLConnection.guessContentTypeFromName(fileName); | |||||
| if (mimeType == null) { | |||||
| // guess this is a video uploading | |||||
| this.fileContentType = ContentType.APPLICATION_OCTET_STREAM; | |||||
| } else { | |||||
| this.fileContentType = ContentType.create(mimeType); | |||||
| } | |||||
| return this; | |||||
| } | |||||
| public WechatPayUploadHttpPost build() { | |||||
| if (fileName == null || fileSha256 == null || fileInputStream == null) { | |||||
| throw new IllegalArgumentException("缺少待上传图片文件信息"); | |||||
| } | |||||
| if (uri == null) { | |||||
| throw new IllegalArgumentException("缺少上传图片接口URL"); | |||||
| } | |||||
| String meta = String.format("{\"filename\":\"%s\",\"sha256\":\"%s\"}", fileName, fileSha256); | |||||
| WechatPayUploadHttpPost request = new WechatPayUploadHttpPost(uri, meta); | |||||
| MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create(); | |||||
| entityBuilder.setMode(HttpMultipartMode.RFC6532) | |||||
| .addBinaryBody("file", fileInputStream, fileContentType, fileName) | |||||
| .addTextBody("meta", meta, ContentType.APPLICATION_JSON); | |||||
| request.setEntity(entityBuilder.build()); | |||||
| request.addHeader("Accept", ContentType.APPLICATION_JSON.toString()); | |||||
| return request; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,75 @@ | |||||
| package com.github.binarywang.wxpay.v3; | |||||
| import com.github.binarywang.wxpay.v3.auth.CertificatesVerifier; | |||||
| import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner; | |||||
| import com.github.binarywang.wxpay.v3.auth.WxPayCredentials; | |||||
| import com.github.binarywang.wxpay.v3.auth.WxPayValidator; | |||||
| import org.apache.http.impl.client.CloseableHttpClient; | |||||
| import org.apache.http.impl.client.HttpClientBuilder; | |||||
| import org.apache.http.impl.execchain.ClientExecChain; | |||||
| import java.security.PrivateKey; | |||||
| import java.security.cert.X509Certificate; | |||||
| import java.util.List; | |||||
| public class WxPayV3HttpClientBuilder 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 WxPayV3HttpClientBuilder() { | |||||
| super(); | |||||
| String userAgent = String.format( | |||||
| "WechatPay-Apache-HttpClient/%s (%s) Java/%s", | |||||
| getClass().getPackage().getImplementationVersion(), | |||||
| OS, | |||||
| VERSION == null ? "Unknown" : VERSION); | |||||
| setUserAgent(userAgent); | |||||
| } | |||||
| public static WxPayV3HttpClientBuilder create() { | |||||
| return new WxPayV3HttpClientBuilder(); | |||||
| } | |||||
| public WxPayV3HttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) { | |||||
| this.credentials = | |||||
| new WxPayCredentials(merchantId, new PrivateKeySigner(serialNo, privateKey)); | |||||
| return this; | |||||
| } | |||||
| public WxPayV3HttpClientBuilder withCredentials(Credentials credentials) { | |||||
| this.credentials = credentials; | |||||
| return this; | |||||
| } | |||||
| public WxPayV3HttpClientBuilder withWechatpay(List<X509Certificate> certificates) { | |||||
| this.validator = new WxPayValidator(new CertificatesVerifier(certificates)); | |||||
| return this; | |||||
| } | |||||
| public WxPayV3HttpClientBuilder 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,198 @@ | |||||
| package com.github.binarywang.wxpay.v3.auth; | |||||
| import com.github.binarywang.wxpay.v3.Credentials; | |||||
| import com.github.binarywang.wxpay.v3.Validator; | |||||
| import com.github.binarywang.wxpay.v3.WxPayV3HttpClientBuilder; | |||||
| import com.github.binarywang.wxpay.v3.util.AesUtils; | |||||
| import com.github.binarywang.wxpay.v3.util.PemUtils; | |||||
| import com.google.gson.JsonArray; | |||||
| import com.google.gson.JsonObject; | |||||
| import lombok.Getter; | |||||
| import lombok.RequiredArgsConstructor; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import me.chanjar.weixin.common.error.WxRuntimeException; | |||||
| import me.chanjar.weixin.common.util.json.GsonParser; | |||||
| 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 java.io.ByteArrayInputStream; | |||||
| import java.io.IOException; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.security.GeneralSecurityException; | |||||
| import java.security.cert.CertificateExpiredException; | |||||
| import java.security.cert.CertificateNotYetValidException; | |||||
| import java.security.cert.X509Certificate; | |||||
| import java.time.Instant; | |||||
| import java.time.temporal.ChronoUnit; | |||||
| import java.util.ArrayList; | |||||
| import java.util.Collections; | |||||
| import java.util.List; | |||||
| import java.util.concurrent.locks.ReentrantLock; | |||||
| /** | |||||
| * 在原有CertificatesVerifier基础上,增加自动更新证书功能 | |||||
| * | |||||
| * @author doger.wang | |||||
| */ | |||||
| @Slf4j | |||||
| public class AutoUpdateCertificatesVerifier implements Verifier { | |||||
| /** | |||||
| * 证书下载地址 | |||||
| */ | |||||
| private static final String CERT_DOWNLOAD_PATH = "https://api.mch.weixin.qq.com/v3/certificates"; | |||||
| /** | |||||
| * 上次更新时间 | |||||
| */ | |||||
| private volatile Instant instant; | |||||
| /** | |||||
| * 证书更新间隔时间,单位为分钟 | |||||
| */ | |||||
| private final int minutesInterval; | |||||
| private CertificatesVerifier verifier; | |||||
| private final Credentials credentials; | |||||
| private final byte[] apiV3Key; | |||||
| private final ReentrantLock lock = new ReentrantLock(); | |||||
| /** | |||||
| * 时间间隔枚举,支持一小时、六小时以及十二小时 | |||||
| */ | |||||
| @Getter | |||||
| @RequiredArgsConstructor | |||||
| public enum TimeInterval { | |||||
| /** | |||||
| * 一小时 | |||||
| */ | |||||
| OneHour(60), | |||||
| /** | |||||
| * 六小时 | |||||
| */ | |||||
| SixHours(60 * 6), | |||||
| /** | |||||
| * 十二小时 | |||||
| */ | |||||
| TwelveHours(60 * 12); | |||||
| private final int 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 WxRuntimeException(e); | |||||
| } | |||||
| } | |||||
| @Override | |||||
| public boolean verify(String serialNumber, byte[] message, String signature) { | |||||
| checkAndAutoUpdateCert(); | |||||
| return verifier.verify(serialNumber, message, signature); | |||||
| } | |||||
| /** | |||||
| * 检查证书是否在有效期内,如果不在有效期内则进行更新 | |||||
| */ | |||||
| private void checkAndAutoUpdateCert() { | |||||
| if (instant == null || instant.plus(minutesInterval, ChronoUnit.MINUTES).compareTo(Instant.now()) >= 0) { | |||||
| if (lock.tryLock()) { | |||||
| try { | |||||
| autoUpdateCert(); | |||||
| //更新时间 | |||||
| instant = Instant.now(); | |||||
| } catch (GeneralSecurityException | IOException e) { | |||||
| log.warn("Auto update cert failed, exception = " + e); | |||||
| } finally { | |||||
| lock.unlock(); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| private void autoUpdateCert() throws IOException, GeneralSecurityException { | |||||
| CloseableHttpClient httpClient = WxPayV3HttpClientBuilder.create() | |||||
| .withCredentials(credentials) | |||||
| .withValidator(verifier == null ? new Validator() { | |||||
| @Override | |||||
| public boolean validate(CloseableHttpResponse response) throws IOException { | |||||
| return true; | |||||
| } | |||||
| } : new WxPayValidator(verifier)) | |||||
| .build(); | |||||
| HttpGet httpGet = new HttpGet(CERT_DOWNLOAD_PATH); | |||||
| 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 aesUtils = new AesUtils(apiV3Key); | |||||
| final JsonObject json = GsonParser.parse(body); | |||||
| final JsonArray dataNode = json.getAsJsonArray("data"); | |||||
| if (dataNode == null) { | |||||
| return Collections.emptyList(); | |||||
| } | |||||
| List<X509Certificate> newCertList = new ArrayList<>(); | |||||
| for (int i = 0, count = dataNode.size(); i < count; i++) { | |||||
| final JsonObject encryptCertificateNode = ((JsonObject) dataNode.get(i)).getAsJsonObject("encrypt_certificate"); | |||||
| //解密 | |||||
| String cert = aesUtils.decryptToString( | |||||
| encryptCertificateNode.get("associated_data").toString().replaceAll("\"", "") | |||||
| .getBytes(StandardCharsets.UTF_8), | |||||
| encryptCertificateNode.get("nonce").toString().replaceAll("\"", "") | |||||
| .getBytes(StandardCharsets.UTF_8), | |||||
| encryptCertificateNode.get("ciphertext").toString().replaceAll("\"", "")); | |||||
| X509Certificate x509Cert = PemUtils | |||||
| .loadCertificate(new ByteArrayInputStream(cert.getBytes(StandardCharsets.UTF_8))); | |||||
| try { | |||||
| x509Cert.checkValidity(); | |||||
| } catch (CertificateExpiredException | CertificateNotYetValidException e) { | |||||
| continue; | |||||
| } | |||||
| newCertList.add(x509Cert); | |||||
| } | |||||
| return newCertList; | |||||
| } | |||||
| @Override | |||||
| public X509Certificate getValidCertificate() { | |||||
| checkAndAutoUpdateCert(); | |||||
| return verifier.getValidCertificate(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,65 @@ | |||||
| package com.github.binarywang.wxpay.v3.auth; | |||||
| import me.chanjar.weixin.common.error.WxRuntimeException; | |||||
| import java.math.BigInteger; | |||||
| import java.security.InvalidKeyException; | |||||
| import java.security.NoSuchAlgorithmException; | |||||
| import java.security.Signature; | |||||
| import java.security.SignatureException; | |||||
| import java.security.cert.CertificateExpiredException; | |||||
| import java.security.cert.CertificateNotYetValidException; | |||||
| import java.security.cert.X509Certificate; | |||||
| import java.util.Base64; | |||||
| import java.util.HashMap; | |||||
| import java.util.List; | |||||
| import java.util.NoSuchElementException; | |||||
| 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 WxRuntimeException("当前Java环境不支持SHA256withRSA", e); | |||||
| } catch (SignatureException e) { | |||||
| throw new WxRuntimeException("签名验证过程发生了错误", e); | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new WxRuntimeException("无效的证书", 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); | |||||
| } | |||||
| @Override | |||||
| public X509Certificate getValidCertificate() { | |||||
| for (X509Certificate x509Cert : certificates.values()) { | |||||
| try { | |||||
| x509Cert.checkValidity(); | |||||
| return x509Cert; | |||||
| } catch (CertificateExpiredException | CertificateNotYetValidException e) { | |||||
| continue; | |||||
| } | |||||
| } | |||||
| throw new NoSuchElementException("没有有效的微信支付平台证书"); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,35 @@ | |||||
| package com.github.binarywang.wxpay.v3.auth; | |||||
| import me.chanjar.weixin.common.error.WxRuntimeException; | |||||
| import java.security.*; | |||||
| 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 WxRuntimeException("当前Java环境不支持SHA256withRSA", e); | |||||
| } catch (SignatureException e) { | |||||
| throw new WxRuntimeException("签名计算失败", e); | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new WxRuntimeException("无效的私钥", 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,10 @@ | |||||
| package com.github.binarywang.wxpay.v3.auth; | |||||
| import java.security.cert.X509Certificate; | |||||
| public interface Verifier { | |||||
| boolean verify(String serialNumber, byte[] message, String signature); | |||||
| X509Certificate getValidCertificate(); | |||||
| } | |||||
| @@ -0,0 +1,93 @@ | |||||
| package com.github.binarywang.wxpay.v3.auth; | |||||
| import com.github.binarywang.wxpay.v3.Credentials; | |||||
| import com.github.binarywang.wxpay.v3.WechatPayUploadHttpPost; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.apache.http.HttpEntityEnclosingRequest; | |||||
| import org.apache.http.client.methods.HttpRequestWrapper; | |||||
| import org.apache.http.util.EntityUtils; | |||||
| import java.io.IOException; | |||||
| import java.net.URI; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.security.SecureRandom; | |||||
| @Slf4j | |||||
| public class WxPayCredentials implements Credentials { | |||||
| private static final String SYMBOLS = | |||||
| "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |||||
| private static final SecureRandom RANDOM = new SecureRandom(); | |||||
| protected String merchantId; | |||||
| protected Signer signer; | |||||
| public WxPayCredentials(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(HttpRequestWrapper 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(StandardCharsets.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, HttpRequestWrapper 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.getOriginal() instanceof WechatPayUploadHttpPost) { | |||||
| body = ((WechatPayUploadHttpPost) request.getOriginal()).getMeta(); | |||||
| } else if (request instanceof HttpEntityEnclosingRequest) { | |||||
| body = EntityUtils.toString(((HttpEntityEnclosingRequest) request).getEntity()); | |||||
| } | |||||
| return request.getRequestLine().getMethod() + "\n" | |||||
| + canonicalUrl + "\n" | |||||
| + timestamp + "\n" | |||||
| + nonce + "\n" | |||||
| + body + "\n"; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,56 @@ | |||||
| package com.github.binarywang.wxpay.v3.auth; | |||||
| import com.github.binarywang.wxpay.v3.Validator; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.apache.http.Header; | |||||
| import org.apache.http.HttpEntity; | |||||
| import org.apache.http.client.methods.CloseableHttpResponse; | |||||
| import org.apache.http.entity.ContentType; | |||||
| import org.apache.http.util.EntityUtils; | |||||
| import java.io.IOException; | |||||
| @Slf4j | |||||
| public class WxPayValidator implements Validator { | |||||
| private Verifier verifier; | |||||
| public WxPayValidator(Verifier verifier) { | |||||
| this.verifier = verifier; | |||||
| } | |||||
| @Override | |||||
| public final boolean validate(CloseableHttpResponse response) throws IOException { | |||||
| if (!ContentType.APPLICATION_JSON.getMimeType().equals(ContentType.parse(String.valueOf(response.getFirstHeader("Content-Type").getValue())).getMimeType())) { | |||||
| return true; | |||||
| } | |||||
| 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,134 @@ | |||||
| package com.github.binarywang.wxpay.v3.util; | |||||
| import com.google.common.base.CharMatcher; | |||||
| import com.google.common.io.BaseEncoding; | |||||
| import org.apache.commons.lang3.StringUtils; | |||||
| import javax.crypto.Cipher; | |||||
| import javax.crypto.Mac; | |||||
| import javax.crypto.NoSuchPaddingException; | |||||
| import javax.crypto.spec.GCMParameterSpec; | |||||
| import javax.crypto.spec.SecretKeySpec; | |||||
| 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; | |||||
| 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 static byte[] decryptToByte(byte[] nonce, byte[] cipherData, byte[] key) | |||||
| throws GeneralSecurityException { | |||||
| return decryptToByte(null, nonce, cipherData, key); | |||||
| } | |||||
| public static byte[] decryptToByte(byte[] associatedData, byte[] nonce, byte[] cipherData, byte[] key) | |||||
| throws GeneralSecurityException { | |||||
| try { | |||||
| Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); | |||||
| SecretKeySpec secretKeySpec = new SecretKeySpec(key, "AES"); | |||||
| GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce); | |||||
| cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, spec); | |||||
| if (associatedData != null) { | |||||
| cipher.updateAAD(associatedData); | |||||
| } | |||||
| return cipher.doFinal(cipherData); | |||||
| } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { | |||||
| throw new IllegalStateException(e); | |||||
| } catch (InvalidKeyException | InvalidAlgorithmParameterException e) { | |||||
| throw new IllegalArgumentException(e); | |||||
| } | |||||
| } | |||||
| 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(BaseEncoding.base64().decode(CharMatcher.whitespace().removeFrom(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,58 @@ | |||||
| package com.github.binarywang.wxpay.v3.util; | |||||
| import me.chanjar.weixin.common.error.WxRuntimeException; | |||||
| 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.*; | |||||
| 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 WxRuntimeException("当前Java环境不支持RSA", e); | |||||
| } catch (InvalidKeySpecException e) { | |||||
| throw new WxRuntimeException("无效的密钥格式"); | |||||
| } catch (IOException e) { | |||||
| throw new WxRuntimeException("无效的密钥"); | |||||
| } | |||||
| } | |||||
| 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 WxRuntimeException("证书已过期", e); | |||||
| } catch (CertificateNotYetValidException e) { | |||||
| throw new WxRuntimeException("证书尚未生效", e); | |||||
| } catch (CertificateException e) { | |||||
| throw new WxRuntimeException("无效的证书", e); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,109 @@ | |||||
| package com.github.binarywang.wxpay.v3.util; | |||||
| import com.github.binarywang.wxpay.exception.WxPayException; | |||||
| import com.github.binarywang.wxpay.v3.SpecEncrypt; | |||||
| import me.chanjar.weixin.common.error.WxRuntimeException; | |||||
| import javax.crypto.BadPaddingException; | |||||
| import javax.crypto.Cipher; | |||||
| import javax.crypto.IllegalBlockSizeException; | |||||
| import javax.crypto.NoSuchPaddingException; | |||||
| import java.lang.reflect.Field; | |||||
| import java.nio.charset.StandardCharsets; | |||||
| import java.security.InvalidKeyException; | |||||
| import java.security.NoSuchAlgorithmException; | |||||
| import java.security.PrivateKey; | |||||
| import java.security.cert.X509Certificate; | |||||
| import java.util.Base64; | |||||
| import java.util.Collection; | |||||
| /** | |||||
| * 微信支付敏感信息加密 | |||||
| * 文档见: https://wechatpay-api.gitbook.io/wechatpay-api-v3/qian-ming-zhi-nan-1/min-gan-xin-xi-jia-mi | |||||
| * | |||||
| * @author zhouyongshen | |||||
| **/ | |||||
| public class RsaCryptoUtil { | |||||
| static String JAVA_LANG_STRING = "java.lang.String"; | |||||
| public static void encryptFields(Object encryptObject, X509Certificate certificate) throws WxPayException { | |||||
| try { | |||||
| encryptField(encryptObject, certificate); | |||||
| } catch (Exception e) { | |||||
| throw new WxPayException("敏感信息加密失败", e); | |||||
| } | |||||
| } | |||||
| private static void encryptField(Object encryptObject, X509Certificate certificate) throws IllegalAccessException, IllegalBlockSizeException { | |||||
| Class<?> infoClass = encryptObject.getClass(); | |||||
| Field[] infoFieldArray = infoClass.getDeclaredFields(); | |||||
| for (Field field : infoFieldArray) { | |||||
| if (field.isAnnotationPresent(SpecEncrypt.class)) { | |||||
| //字段使用了@SpecEncrypt进行标识 | |||||
| if (field.getType().getTypeName().equals(JAVA_LANG_STRING)) { | |||||
| field.setAccessible(true); | |||||
| Object oldValue = field.get(encryptObject); | |||||
| if (oldValue != null) { | |||||
| String oldStr = (String) oldValue; | |||||
| if (!oldStr.trim().equals("'")) { | |||||
| field.set(encryptObject, encryptOAEP(oldStr, certificate)); | |||||
| } | |||||
| } | |||||
| } else { | |||||
| field.setAccessible(true); | |||||
| Object obj = field.get(encryptObject); | |||||
| if (obj == null) { | |||||
| continue; | |||||
| } | |||||
| if (obj instanceof Collection) { | |||||
| Collection collection = (Collection) obj; | |||||
| for (Object o : collection) { | |||||
| if (o != null) { | |||||
| encryptField(o, certificate); | |||||
| } | |||||
| } | |||||
| } else { | |||||
| encryptField(obj, certificate); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||
| public static String encryptOAEP(String message, X509Certificate certificate) | |||||
| throws IllegalBlockSizeException { | |||||
| try { | |||||
| Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); | |||||
| cipher.init(Cipher.ENCRYPT_MODE, certificate.getPublicKey()); | |||||
| byte[] data = message.getBytes(StandardCharsets.UTF_8); | |||||
| byte[] ciphertext = cipher.doFinal(data); | |||||
| return Base64.getEncoder().encodeToString(ciphertext); | |||||
| } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { | |||||
| throw new WxRuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new IllegalArgumentException("无效的证书", e); | |||||
| } catch (IllegalBlockSizeException | BadPaddingException e) { | |||||
| throw new IllegalBlockSizeException("加密原串的长度不能超过214字节"); | |||||
| } | |||||
| } | |||||
| public static String decryptOAEP(String ciphertext, PrivateKey privateKey) | |||||
| throws BadPaddingException { | |||||
| try { | |||||
| Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); | |||||
| cipher.init(Cipher.DECRYPT_MODE, privateKey); | |||||
| byte[] data = Base64.getDecoder().decode(ciphertext); | |||||
| return new String(cipher.doFinal(data), StandardCharsets.UTF_8); | |||||
| } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { | |||||
| throw new WxRuntimeException("当前Java环境不支持RSA v1.5/OAEP", e); | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new IllegalArgumentException("无效的私钥", e); | |||||
| } catch (BadPaddingException | IllegalBlockSizeException e) { | |||||
| throw new BadPaddingException("解密失败"); | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,50 @@ | |||||
| package com.github.binarywang.wxpay.v3.util; | |||||
| import me.chanjar.weixin.common.error.WxRuntimeException; | |||||
| import java.security.*; | |||||
| import java.util.Base64; | |||||
| import java.util.Random; | |||||
| public class SignUtils { | |||||
| public static String sign(String string, PrivateKey privateKey) { | |||||
| try { | |||||
| Signature sign = Signature.getInstance("SHA256withRSA"); | |||||
| sign.initSign(privateKey); | |||||
| sign.update(string.getBytes()); | |||||
| return Base64.getEncoder().encodeToString(sign.sign()); | |||||
| } catch (NoSuchAlgorithmException e) { | |||||
| throw new WxRuntimeException("当前Java环境不支持SHA256withRSA", e); | |||||
| } catch (SignatureException e) { | |||||
| throw new WxRuntimeException("签名计算失败", e); | |||||
| } catch (InvalidKeyException e) { | |||||
| throw new WxRuntimeException("无效的私钥", e); | |||||
| } | |||||
| } | |||||
| /** | |||||
| * 随机生成32位字符串. | |||||
| */ | |||||
| public static String genRandomStr() { | |||||
| return genRandomStr(32); | |||||
| } | |||||
| /** | |||||
| * 生成随机字符串 | |||||
| * | |||||
| * @param length 字符串长度 | |||||
| * @return | |||||
| */ | |||||
| public static String genRandomStr(int length) { | |||||
| String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |||||
| Random random = new Random(); | |||||
| StringBuilder sb = new StringBuilder(); | |||||
| for (int i = 0; i < length; i++) { | |||||
| int number = random.nextInt(base.length()); | |||||
| sb.append(base.charAt(number)); | |||||
| } | |||||
| return sb.toString(); | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,79 @@ | |||||
| package com.github.binarywang.wxpay.service.impl; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.BusinessCircleNotifyData; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.PaidResult; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.PointsNotifyRequest; | |||||
| import com.github.binarywang.wxpay.bean.businesscircle.RefundResult; | |||||
| import com.github.binarywang.wxpay.bean.ecommerce.SignatureHeader; | |||||
| import com.github.binarywang.wxpay.exception.WxPayException; | |||||
| import com.github.binarywang.wxpay.service.WxPayService; | |||||
| import com.github.binarywang.wxpay.testbase.ApiTestModule; | |||||
| import com.google.gson.Gson; | |||||
| import com.google.gson.GsonBuilder; | |||||
| import com.google.inject.Inject; | |||||
| import lombok.extern.slf4j.Slf4j; | |||||
| import org.testng.annotations.Guice; | |||||
| import org.testng.annotations.Test; | |||||
| /** | |||||
| * <pre> | |||||
| * 智慧商圈测试类 | |||||
| * </pre> | |||||
| * | |||||
| * @author thinsstar | |||||
| */ | |||||
| @Slf4j | |||||
| @Test | |||||
| @Guice(modules = ApiTestModule.class) | |||||
| public class BusinessCircleServiceImplTest { | |||||
| @Inject | |||||
| private WxPayService wxPayService; | |||||
| private static final Gson GSON = new GsonBuilder().create(); | |||||
| @Test | |||||
| public void testNotifyPointsV3() throws WxPayException { | |||||
| PointsNotifyRequest request = new PointsNotifyRequest(); | |||||
| String subMchid = "商圈商户ID"; | |||||
| String transactionId = "微信订单号"; | |||||
| String appId = "公众号id"; | |||||
| String openId = "微信openid"; | |||||
| request.setSubMchid(subMchid); | |||||
| request.setTransactionId(transactionId); | |||||
| request.setAppid(appId); | |||||
| request.setOpenid(openId); | |||||
| request.setEarnPoints(true); | |||||
| request.setIncreasedPoints(10); | |||||
| request.setPointsUpdateTime("2021-03-03T13:29:35.120+08:00"); | |||||
| wxPayService.getBusinessCircleService().notifyPoints(request); | |||||
| } | |||||
| @Test | |||||
| public void testDecryptPaidNotifyDataResource() throws WxPayException { | |||||
| SignatureHeader header = new SignatureHeader(); | |||||
| header.setSerialNo("Wechatpay-Serial"); | |||||
| header.setTimeStamp("Wechatpay-Timestamp"); | |||||
| header.setNonce("Wechatpay-Nonce"); | |||||
| header.setSigned("Wechatpay-Signature"); | |||||
| String data = "body"; | |||||
| BusinessCircleNotifyData notifyData = wxPayService.getBusinessCircleService().parseNotifyData(data, header); | |||||
| PaidResult result = wxPayService.getBusinessCircleService().decryptPaidNotifyDataResource(notifyData); | |||||
| log.info("result: {}", GSON.toJson(result)); | |||||
| } | |||||
| @Test | |||||
| public void testDecryptRefundNotifyDataResource() throws WxPayException { | |||||
| SignatureHeader header = new SignatureHeader(); | |||||
| header.setSerialNo("Wechatpay-Serial"); | |||||
| header.setTimeStamp("Wechatpay-Timestamp"); | |||||
| header.setNonce("Wechatpay-Nonce"); | |||||
| header.setSigned("Wechatpay-Signature"); | |||||
| String data = "body"; | |||||
| BusinessCircleNotifyData notifyData = wxPayService.getBusinessCircleService().parseNotifyData(data, header); | |||||
| RefundResult result = wxPayService.getBusinessCircleService().decryptRefundNotifyDataResource(notifyData); | |||||
| log.info("result: {}", GSON.toJson(result)); | |||||
| } | |||||
| } | |||||