Browse Source

update weixin pay

formao-live
xhxu 4 years ago
parent
commit
dd3240f8de
50 changed files with 2405 additions and 76 deletions
  1. +2
    -3
      pom.xml
  2. +8
    -0
      spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java
  3. +30
    -0
      spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java
  4. +23
    -0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java
  5. +3
    -2
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java
  6. +2
    -2
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java
  7. +2
    -2
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java
  8. +2
    -2
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java
  9. +26
    -0
      weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java
  10. +2
    -2
      weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java
  11. +1
    -1
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceJoddHttpImpl.java
  12. +2
    -2
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialDeleteJoddHttpRequestExecutor.java
  13. +2
    -2
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialNewsInfoJoddHttpRequestExecutor.java
  14. +2
    -2
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialUploadJoddHttpRequestExecutor.java
  15. +2
    -2
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialVideoInfoJoddHttpRequestExecutor.java
  16. +1
    -1
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialVoiceAndImageDownloadJoddHttpRequestExecutor.java
  17. +2
    -2
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/media/MediaImgUploadHttpRequestExecutor.java
  18. +3
    -3
      weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/qrcode/QrCodeJoddHttpRequestExecutor.java
  19. +3
    -3
      weixin-java-open/src/main/java/me/chanjar/weixin/open/executor/MaQrCodeJoddHttpRequestExecutor.java
  20. +12
    -0
      weixin-java-pay/pom.xml
  21. +93
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/BusinessCircleNotifyData.java
  22. +112
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PaidResult.java
  23. +154
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PointsNotifyRequest.java
  24. +121
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/RefundResult.java
  25. +35
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java
  26. +178
    -34
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/config/WxPayConfig.java
  27. +35
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/BusinessCircleService.java
  28. +70
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/WxPayService.java
  29. +7
    -4
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BaseWxPayServiceImpl.java
  30. +89
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImpl.java
  31. +169
    -7
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceApacheHttpImpl.java
  32. +33
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/service/impl/WxPayServiceJoddHttpImpl.java
  33. +12
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Credentials.java
  34. +89
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SignatureExec.java
  35. +16
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/SpecEncrypt.java
  36. +9
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/Validator.java
  37. +76
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WechatPayUploadHttpPost.java
  38. +75
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/WxPayV3HttpClientBuilder.java
  39. +198
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/AutoUpdateCertificatesVerifier.java
  40. +65
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/CertificatesVerifier.java
  41. +35
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/PrivateKeySigner.java
  42. +15
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Signer.java
  43. +10
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/Verifier.java
  44. +93
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayCredentials.java
  45. +56
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/auth/WxPayValidator.java
  46. +134
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/AesUtils.java
  47. +58
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/PemUtils.java
  48. +109
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/RsaCryptoUtil.java
  49. +50
    -0
      weixin-java-pay/src/main/java/com/github/binarywang/wxpay/v3/util/SignUtils.java
  50. +79
    -0
      weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java

+ 2
- 3
pom.xml View File

@@ -126,11 +126,10 @@
<artifactId>qrcode-utils</artifactId>
<version>1.1</version>
</dependency>
<!-- 由于jodd-http较新的3.8版本需要jdk8,故而此处采用较低版本 -->
<dependency>
<groupId>org.jodd</groupId>
<artifactId>jodd-http</artifactId>
<version>3.7.1</version>
<version>5.2.0</version>
<scope>provided</scope>
</dependency>
<dependency>
@@ -163,7 +162,7 @@
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.5</version>
<version>3.10</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>


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

@@ -50,6 +50,14 @@ public class WxPayAutoConfiguration {
payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));

//以下是apiv3以及支付分相关
payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId()));
payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl()));
payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo()));
payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiv3Key()));

wxPayService.setConfig(payConfig);
return wxPayService;
}


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

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

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

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

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

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

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

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

+ 23
- 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java View File

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

+ 3
- 2
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;
import me.chanjar.weixin.common.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -19,6 +18,8 @@ import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;


/**
* .
@@ -47,7 +48,7 @@ public class JoddHttpMediaDownloadRequestExecutor extends BaseMediaDownloadReque
request.withConnectionProvider(requestHttp.getRequestHttpClient());

HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());

String contentType = response.header("Content-Type");
if (contentType != null && contentType.startsWith("application/json")) {


+ 2
- 2
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;
import me.chanjar.weixin.common.WxType;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
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.IOException;
import java.nio.charset.StandardCharsets;

/**
* .
@@ -35,7 +35,7 @@ public class JoddHttpMediaUploadRequestExecutor extends MediaUploadRequestExecut
request.withConnectionProvider(requestHttp.getRequestHttpClient());
request.form("media", file);
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());

String responseContent = response.bodyText();
WxError error = WxError.fromJson(responseContent, wxType);


+ 2
- 2
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;
import me.chanjar.weixin.common.WxType;
import me.chanjar.weixin.common.error.WxError;
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 java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
* .
@@ -39,7 +39,7 @@ public class JoddHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor<H
}
request.withConnectionProvider(requestHttp.getRequestHttpClient());
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());

String responseContent = response.bodyText();



+ 2
- 2
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;
import me.chanjar.weixin.common.WxType;
import me.chanjar.weixin.common.error.WxError;
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 java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
* .
@@ -38,7 +38,7 @@ public class JoddHttpSimplePostRequestExecutor extends SimplePostRequestExecutor
request.bodyText(postEntity);
}
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());

String responseContent = response.bodyText();
if (responseContent.isEmpty()) {


+ 26
- 0
weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java View File

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

+ 2
- 2
weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java View File

@@ -3,8 +3,8 @@ package me.chanjar.weixin.cp.api.impl;
import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.JoddHttp;
import jodd.http.ProxyInfo;
import jodd.http.net.SocketHttpConnectionProvider;
import me.chanjar.weixin.common.WxType;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.error.WxError;
@@ -68,7 +68,7 @@ public class WxCpServiceJoddHttpImpl extends BaseWxCpServiceImpl<HttpConnectionP
configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
}

httpClient = JoddHttp.httpConnectionProvider;
httpClient = new SocketHttpConnectionProvider();
}

@Override


+ 1
- 1
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/api/impl/WxMpServiceJoddHttpImpl.java View File

@@ -46,7 +46,7 @@ public class WxMpServiceJoddHttpImpl extends BaseWxMpServiceImpl<HttpConnectionP
httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
}

httpClient = JoddHttp.httpConnectionProvider;
httpClient = new SocketHttpConnectionProvider();
}

@Override


+ 2
- 2
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialDeleteJoddHttpRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;

import me.chanjar.weixin.common.WxType;
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 java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
* Created by ecoolper on 2017/5/5.
@@ -31,7 +31,7 @@ public class MaterialDeleteJoddHttpRequestExecutor extends MaterialDeleteRequest

request.query("media_id", materialId);
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());
String responseContent = response.bodyText();
WxError error = WxError.fromJson(responseContent, WxType.MP);
if (error.getErrorCode() != 0) {


+ 2
- 2
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialNewsInfoJoddHttpRequestExecutor.java View File

@@ -5,7 +5,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;

import me.chanjar.weixin.common.WxType;
import me.chanjar.weixin.common.error.WxError;
@@ -18,6 +17,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
* Created by ecoolper on 2017/5/5.
@@ -38,7 +38,7 @@ public class MaterialNewsInfoJoddHttpRequestExecutor extends MaterialNewsInfoReq
.withConnectionProvider(requestHttp.getRequestHttpClient())
.body(WxGsonBuilder.create().toJson(ImmutableMap.of("media_id", materialId)));
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());

String responseContent = response.bodyText();
this.logger.debug("响应原始数据:{}", responseContent);


+ 2
- 2
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialUploadJoddHttpRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;

import me.chanjar.weixin.common.WxType;
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.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Map;

/**
@@ -50,7 +50,7 @@ public class MaterialUploadJoddHttpRequestExecutor extends MaterialUploadRequest
}

HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());
String responseContent = response.bodyText();
WxError error = WxError.fromJson(responseContent, WxType.MP);
if (error.getErrorCode() != 0) {


+ 2
- 2
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialVideoInfoJoddHttpRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;

import me.chanjar.weixin.common.WxType;
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 java.io.IOException;
import java.nio.charset.StandardCharsets;

/**
* Created by ecoolper on 2017/5/5.
@@ -32,7 +32,7 @@ public class MaterialVideoInfoJoddHttpRequestExecutor extends MaterialVideoInfoR

request.query("media_id", materialId);
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());
String responseContent = response.bodyText();
WxError error = WxError.fromJson(responseContent, WxType.MP);
if (error.getErrorCode() != 0) {


+ 1
- 1
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/material/MaterialVoiceAndImageDownloadJoddHttpRequestExecutor.java View File

@@ -37,7 +37,7 @@ public class MaterialVoiceAndImageDownloadJoddHttpRequestExecutor extends Materi

request.query("media_id", materialId);
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());
try (InputStream inputStream = new ByteArrayInputStream(response.bodyBytes())) {
// 下载媒体文件出错
byte[] responseContent = IOUtils.toByteArray(inputStream);


+ 2
- 2
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/media/MediaImgUploadHttpRequestExecutor.java View File

@@ -4,7 +4,6 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.util.StringPool;

import me.chanjar.weixin.common.WxType;
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.IOException;
import java.nio.charset.StandardCharsets;

/**
* Created by ecoolper on 2017/5/5.
@@ -39,7 +39,7 @@ public class MediaImgUploadHttpRequestExecutor extends MediaImgUploadRequestExec

request.form("media", data);
HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());
String responseContent = response.bodyText();
WxError error = WxError.fromJson(responseContent, WxType.MP);
if (error.getErrorCode() != 0) {


+ 3
- 3
weixin-java-mp/src/main/java/me/chanjar/weixin/mp/util/requestexecuter/qrcode/QrCodeJoddHttpRequestExecutor.java View File

@@ -4,9 +4,8 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
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.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -19,6 +18,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;

/**
@@ -47,7 +47,7 @@ public class QrCodeJoddHttpRequestExecutor extends QrCodeRequestExecutor<HttpCon
request.withConnectionProvider(requestHttp.getRequestHttpClient());

HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());
String contentTypeHeader = response.header("Content-Type");
if (MimeTypes.MIME_TEXT_PLAIN.equals(contentTypeHeader)) {
String responseContent = response.bodyText();


+ 3
- 3
weixin-java-open/src/main/java/me/chanjar/weixin/open/executor/MaQrCodeJoddHttpRequestExecutor.java View File

@@ -4,8 +4,7 @@ import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
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.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -19,6 +18,7 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.UUID;

/**
@@ -49,7 +49,7 @@ public class MaQrCodeJoddHttpRequestExecutor extends MaQrCodeRequestExecutor<Htt
request.withConnectionProvider(requestHttp.getRequestHttpClient());

HttpResponse response = request.send();
response.charset(StringPool.UTF_8);
response.charset(StandardCharsets.UTF_8.name());
String contentTypeHeader = response.header("Content-Type");
if (MimeTypes.MIME_TEXT_PLAIN.equals(contentTypeHeader)) {
String responseContent = response.bodyText();


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

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

<dependencies>
<dependency>


+ 93
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/BusinessCircleNotifyData.java View File

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

+ 112
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PaidResult.java View File

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

+ 154
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/PointsNotifyRequest.java View File

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

+ 121
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/businesscircle/RefundResult.java View File

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

+ 35
- 0
weixin-java-pay/src/main/java/com/github/binarywang/wxpay/bean/ecommerce/SignatureHeader.java View File

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

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

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

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.EqualsAndHashCode;
import lombok.SneakyThrows;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.RegExUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.ssl.SSLContexts;

import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
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)
*/
@Data
@EqualsAndHashCode(exclude = "verifier")
public class WxPayConfig {
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;

/**
* 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证书文件内容的字节数组.
*/
@@ -106,8 +167,15 @@ public class WxPayConfig {
private String httpProxyUsername;
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 微信支付接口请求地址域名
*/
public String getPayBaseUrl() {
@@ -118,6 +186,15 @@ public class WxPayConfig {
return this.payBaseUrl;
}

@SneakyThrows
public Verifier getVerifier() {
if (verifier == null) {
//当改对象为null时,初始化api v3的请求头
initApiV3HttpClient();
}
return verifier;
}

/**
* 初始化ssl.
*
@@ -136,40 +213,7 @@ public class WxPayConfig {
if (StringUtils.isBlank(this.getKeyPath())) {
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 {
@@ -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;
}
}

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

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

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

@@ -9,8 +9,11 @@ import com.github.binarywang.wxpay.bean.request.*;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.config.WxPayConfig;
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.InputStream;
import java.util.Date;
import java.util.Map;

@@ -53,6 +56,66 @@ public interface WxPayService {
*/
String post(String url, String requestStr, boolean useKey) throws WxPayException;

/**
* 发送post请求,得到响应字符串.
*
* @param url 请求地址
* @param requestStr 请求信息
* @return 返回请求结果字符串 string
* @throws WxPayException the wx pay exception
*/
String postV3(String url, String requestStr) throws WxPayException;

/**
* 发送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();

/**
* 获取微信支付智慧商圈服务类
*
* @return the business circle service
*/
BusinessCircleService getBusinessCircleService();

/**
* 设置企业付款服务类,允许开发者自定义实现类.
*


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

@@ -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.TradeType;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.EntPayService;
import com.github.binarywang.wxpay.service.ProfitSharingService;
import com.github.binarywang.wxpay.service.RedpackService;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.*;
import com.github.binarywang.wxpay.util.SignUtils;
import com.github.binarywang.wxpay.util.XmlConfig;
import com.google.common.base.Joiner;
@@ -63,6 +60,7 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
private EntPayService entPayService = new EntPayServiceImpl(this);
private ProfitSharingService profitSharingService = new ProfitSharingServiceImpl(this);
private RedpackService redpackService = new RedpackServiceImpl(this);
private BusinessCircleService businessCircleService = new BusinessCircleServiceImpl(this);

/**
* The Config.
@@ -79,6 +77,11 @@ public abstract class BaseWxPayServiceImpl implements WxPayService {
return profitSharingService;
}

@Override
public BusinessCircleService getBusinessCircleService() {
return this.businessCircleService;
}

@Override
public RedpackService getRedpackService() {
return this.redpackService;


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

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

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

@@ -1,26 +1,31 @@
package com.github.binarywang.wxpay.service.impl;

import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLContext;

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.google.gson.JsonElement;
import com.google.gson.JsonObject;
import me.chanjar.weixin.common.util.json.GsonParser;
import jodd.util.Base64;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpStatus;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
@@ -28,9 +33,6 @@ import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
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>
@@ -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) {
try {
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);
}

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

}

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

@@ -1,5 +1,6 @@
package com.github.binarywang.wxpay.service.impl;

import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import javax.net.ssl.SSLContext;

@@ -20,6 +21,8 @@ import jodd.http.net.SSLSocketHttpConnectionProvider;
import jodd.http.net.SocketHttpConnectionProvider;
import jodd.util.Base64;
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 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 {
HttpRequest request = HttpRequest
.post(url)


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

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

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

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

}

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

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

}

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

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

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

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

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

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

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

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

}

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

@@ -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("没有有效的微信支付平台证书");
}

}

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

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

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

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

public interface Signer {
SignatureResult sign(byte[] message);

class SignatureResult {
String sign;
String certificateSerialNumber;

public SignatureResult(String sign, String serialNumber) {
this.sign = sign;
this.certificateSerialNumber = serialNumber;
}
}
}

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

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

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

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

}

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

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

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

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


}

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

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

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

@@ -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("解密失败");
}
}
}

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

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

+ 79
- 0
weixin-java-pay/src/test/java/com/github/binarywang/wxpay/service/impl/BusinessCircleServiceImplTest.java View File

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

Loading…
Cancel
Save