mirror of
https://gitee.com/binary/weixin-java-tools.git
synced 2026-03-10 00:13:40 +08:00
🆕 #1090 增加微信支付分和免押租借相关接口
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
package com.github.binarywang.wxpay.v3;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
|
||||
public interface Credentials {
|
||||
|
||||
String getSchema();
|
||||
|
||||
String getToken(HttpUriRequest request) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.github.binarywang.wxpay.v3;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.HttpException;
|
||||
import org.apache.http.StatusLine;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import org.apache.http.client.methods.HttpExecutionAware;
|
||||
import org.apache.http.client.methods.HttpRequestWrapper;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.client.methods.RequestBuilder;
|
||||
import org.apache.http.client.protocol.HttpClientContext;
|
||||
import org.apache.http.conn.routing.HttpRoute;
|
||||
import org.apache.http.entity.ByteArrayEntity;
|
||||
import org.apache.http.impl.execchain.ClientExecChain;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
public class SignatureExec implements ClientExecChain {
|
||||
final ClientExecChain mainExec;
|
||||
final Credentials credentials;
|
||||
final Validator validator;
|
||||
|
||||
SignatureExec(Credentials credentials, Validator validator, ClientExecChain mainExec) {
|
||||
this.credentials = credentials;
|
||||
this.validator = validator;
|
||||
this.mainExec = mainExec;
|
||||
}
|
||||
|
||||
protected HttpEntity newRepeatableEntity(HttpEntity entity) throws IOException {
|
||||
byte[] content = EntityUtils.toByteArray(entity);
|
||||
ByteArrayEntity newEntity = new ByteArrayEntity(content);
|
||||
newEntity.setContentEncoding(entity.getContentEncoding());
|
||||
newEntity.setContentType(entity.getContentType());
|
||||
|
||||
return newEntity;
|
||||
}
|
||||
|
||||
protected void convertToRepeatableResponseEntity(CloseableHttpResponse response) throws IOException {
|
||||
HttpEntity entity = response.getEntity();
|
||||
if (entity != null && !entity.isRepeatable()) {
|
||||
response.setEntity(newRepeatableEntity(entity));
|
||||
}
|
||||
}
|
||||
|
||||
protected void convertToRepeatableRequestEntity(HttpUriRequest request) throws IOException {
|
||||
if (request instanceof HttpEntityEnclosingRequestBase) {
|
||||
HttpEntity entity = ((HttpEntityEnclosingRequestBase) request).getEntity();
|
||||
if (entity != null && !entity.isRepeatable()) {
|
||||
((HttpEntityEnclosingRequestBase) request).setEntity(newRepeatableEntity(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloseableHttpResponse execute(HttpRoute route, HttpRequestWrapper request,
|
||||
HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException {
|
||||
if (request.getURI().getHost().endsWith(".mch.weixin.qq.com")) {
|
||||
return executeWithSignature(route, request, context, execAware);
|
||||
} else {
|
||||
return mainExec.execute(route, request, context, execAware);
|
||||
}
|
||||
}
|
||||
|
||||
private CloseableHttpResponse executeWithSignature(HttpRoute route, HttpRequestWrapper request,
|
||||
HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException {
|
||||
HttpUriRequest newRequest = RequestBuilder.copy(request.getOriginal()).build();
|
||||
convertToRepeatableRequestEntity(newRequest);
|
||||
// 添加认证信息
|
||||
newRequest.addHeader("Authorization",
|
||||
credentials.getSchema() + " " + credentials.getToken(newRequest));
|
||||
|
||||
// 执行
|
||||
CloseableHttpResponse response = mainExec.execute(
|
||||
route, HttpRequestWrapper.wrap(newRequest), context, execAware);
|
||||
|
||||
// 对成功应答验签
|
||||
StatusLine statusLine = response.getStatusLine();
|
||||
if (statusLine.getStatusCode() >= 200 && statusLine.getStatusCode() < 300) {
|
||||
convertToRepeatableResponseEntity(response);
|
||||
if (!validator.validate(response)) {
|
||||
throw new HttpException("应答的微信支付签名验证失败");
|
||||
}
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.binarywang.wxpay.v3;
|
||||
|
||||
import java.io.IOException;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
|
||||
public interface Validator {
|
||||
boolean validate(CloseableHttpResponse response) throws IOException;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.github.binarywang.wxpay.v3;
|
||||
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.List;
|
||||
|
||||
import com.github.binarywang.wxpay.v3.auth.CertificatesVerifier;
|
||||
import com.github.binarywang.wxpay.v3.auth.PrivateKeySigner;
|
||||
import com.github.binarywang.wxpay.v3.auth.WechatPay2Credentials;
|
||||
import com.github.binarywang.wxpay.v3.auth.WechatPay2Validator;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.impl.client.HttpClientBuilder;
|
||||
import org.apache.http.impl.execchain.ClientExecChain;
|
||||
|
||||
public class WechatPayHttpClientBuilder extends HttpClientBuilder {
|
||||
private Credentials credentials;
|
||||
private Validator validator;
|
||||
|
||||
static final String os = System.getProperty("os.name") + "/" + System.getProperty("os.version");
|
||||
static final String version = System.getProperty("java.version");
|
||||
|
||||
private WechatPayHttpClientBuilder() {
|
||||
super();
|
||||
|
||||
String userAgent = String.format(
|
||||
"WechatPay-Apache-HttpClient/%s (%s) Java/%s",
|
||||
getClass().getPackage().getImplementationVersion(),
|
||||
os,
|
||||
version == null ? "Unknown" : version);
|
||||
setUserAgent(userAgent);
|
||||
}
|
||||
|
||||
public static WechatPayHttpClientBuilder create() {
|
||||
return new WechatPayHttpClientBuilder();
|
||||
}
|
||||
|
||||
public WechatPayHttpClientBuilder withMerchant(String merchantId, String serialNo, PrivateKey privateKey) {
|
||||
this.credentials =
|
||||
new WechatPay2Credentials(merchantId, new PrivateKeySigner(serialNo, privateKey));
|
||||
return this;
|
||||
}
|
||||
|
||||
public WechatPayHttpClientBuilder withCredentials(Credentials credentials) {
|
||||
this.credentials = credentials;
|
||||
return this;
|
||||
}
|
||||
|
||||
public WechatPayHttpClientBuilder withWechatpay(List<X509Certificate> certificates) {
|
||||
this.validator = new WechatPay2Validator(new CertificatesVerifier(certificates));
|
||||
return this;
|
||||
}
|
||||
|
||||
public WechatPayHttpClientBuilder withValidator(Validator validator) {
|
||||
this.validator = validator;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CloseableHttpClient build() {
|
||||
if (credentials == null) {
|
||||
throw new IllegalArgumentException("缺少身份认证信息");
|
||||
}
|
||||
if (validator == null) {
|
||||
throw new IllegalArgumentException("缺少签名验证信息");
|
||||
}
|
||||
|
||||
return super.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ClientExecChain decorateProtocolExec(final ClientExecChain requestExecutor) {
|
||||
return new SignatureExec(this.credentials, this.validator, requestExecutor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.github.binarywang.wxpay.v3.Credentials;
|
||||
import com.github.binarywang.wxpay.v3.WechatPayHttpClientBuilder;
|
||||
import com.github.binarywang.wxpay.v3.util.AesUtils;
|
||||
import com.github.binarywang.wxpay.v3.util.PemUtils;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.client.methods.HttpGet;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* 在原有CertificatesVerifier基础上,增加自动更新证书功能
|
||||
*/
|
||||
public class AutoUpdateCertificatesVerifier implements Verifier {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(AutoUpdateCertificatesVerifier.class);
|
||||
|
||||
//证书下载地址
|
||||
private static final String CertDownloadPath = "https://api.mch.weixin.qq.com/v3/certificates";
|
||||
|
||||
//上次更新时间
|
||||
private volatile Instant instant;
|
||||
|
||||
//证书更新间隔时间,单位为分钟
|
||||
private int minutesInterval;
|
||||
|
||||
private CertificatesVerifier verifier;
|
||||
|
||||
private Credentials credentials;
|
||||
|
||||
private byte[] apiV3Key;
|
||||
|
||||
private ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
//时间间隔枚举,支持一小时、六小时以及十二小时
|
||||
public enum TimeInterval {
|
||||
OneHour(60), SixHours(60 * 6), TwelveHours(60 * 12);
|
||||
|
||||
private int minutes;
|
||||
|
||||
TimeInterval(int minutes) {
|
||||
this.minutes = minutes;
|
||||
}
|
||||
|
||||
public int getMinutes() {
|
||||
return minutes;
|
||||
}
|
||||
}
|
||||
|
||||
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key) {
|
||||
this(credentials, apiV3Key, TimeInterval.OneHour.getMinutes());
|
||||
}
|
||||
|
||||
public AutoUpdateCertificatesVerifier(Credentials credentials, byte[] apiV3Key, int minutesInterval) {
|
||||
this.credentials = credentials;
|
||||
this.apiV3Key = apiV3Key;
|
||||
this.minutesInterval = minutesInterval;
|
||||
//构造时更新证书
|
||||
try {
|
||||
autoUpdateCert();
|
||||
instant = Instant.now();
|
||||
} catch (IOException | GeneralSecurityException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String serialNumber, byte[] message, String signature) {
|
||||
if (instant == null || Duration.between(instant, Instant.now()).toMinutes() >= minutesInterval) {
|
||||
if (lock.tryLock()) {
|
||||
try {
|
||||
autoUpdateCert();
|
||||
//更新时间
|
||||
instant = Instant.now();
|
||||
} catch (GeneralSecurityException | IOException e) {
|
||||
log.warn("Auto update cert failed, exception = " + e);
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
return verifier.verify(serialNumber, message, signature);
|
||||
}
|
||||
|
||||
private void autoUpdateCert() throws IOException, GeneralSecurityException {
|
||||
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
|
||||
.withCredentials(credentials)
|
||||
.withValidator(verifier == null ? (response) -> true : new WechatPay2Validator(verifier))
|
||||
.build();
|
||||
|
||||
HttpGet httpGet = new HttpGet(CertDownloadPath);
|
||||
httpGet.addHeader("Accept", "application/json");
|
||||
|
||||
CloseableHttpResponse response = httpClient.execute(httpGet);
|
||||
int statusCode = response.getStatusLine().getStatusCode();
|
||||
String body = EntityUtils.toString(response.getEntity());
|
||||
if (statusCode == 200) {
|
||||
List<X509Certificate> newCertList = deserializeToCerts(apiV3Key, body);
|
||||
if (newCertList.isEmpty()) {
|
||||
log.warn("Cert list is empty");
|
||||
return;
|
||||
}
|
||||
this.verifier = new CertificatesVerifier(newCertList);
|
||||
} else {
|
||||
log.warn("Auto update cert failed, statusCode = " + statusCode + ",body = " + body);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 反序列化证书并解密
|
||||
*/
|
||||
private List<X509Certificate> deserializeToCerts(byte[] apiV3Key, String body)
|
||||
throws GeneralSecurityException, IOException {
|
||||
AesUtils decryptor = new AesUtils(apiV3Key);
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode dataNode = mapper.readTree(body).get("data");
|
||||
List<X509Certificate> newCertList = new ArrayList<>();
|
||||
if (dataNode != null) {
|
||||
for (int i = 0, count = dataNode.size(); i < count; i++) {
|
||||
JsonNode encryptCertificateNode = dataNode.get(i).get("encrypt_certificate");
|
||||
//解密
|
||||
String cert = decryptor.decryptToString(
|
||||
encryptCertificateNode.get("associated_data").toString().replaceAll("\"", "")
|
||||
.getBytes("utf-8"),
|
||||
encryptCertificateNode.get("nonce").toString().replaceAll("\"", "")
|
||||
.getBytes("utf-8"),
|
||||
encryptCertificateNode.get("ciphertext").toString().replaceAll("\"", ""));
|
||||
|
||||
X509Certificate x509Cert = PemUtils
|
||||
.loadCertificate(new ByteArrayInputStream(cert.getBytes("utf-8")));
|
||||
try {
|
||||
x509Cert.checkValidity();
|
||||
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
|
||||
continue;
|
||||
}
|
||||
newCertList.add(x509Cert);
|
||||
}
|
||||
}
|
||||
return newCertList;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class CertificatesVerifier implements Verifier {
|
||||
private final HashMap<BigInteger, X509Certificate> certificates = new HashMap<>();
|
||||
|
||||
public CertificatesVerifier(List<X509Certificate> list) {
|
||||
|
||||
for (X509Certificate item : list) {
|
||||
certificates.put(item.getSerialNumber(), item);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean verify(X509Certificate certificate, byte[] message, String signature) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance("SHA256withRSA");
|
||||
sign.initVerify(certificate);
|
||||
sign.update(message);
|
||||
return sign.verify(Base64.getDecoder().decode(signature));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException("签名验证过程发生了错误", e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new RuntimeException("无效的证书", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String serialNumber, byte[] message, String signature) {
|
||||
BigInteger val = new BigInteger(serialNumber, 16);
|
||||
return certificates.containsKey(val) && verify(certificates.get(val), message, signature);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PrivateKeySigner implements Signer {
|
||||
private String certificateSerialNumber;
|
||||
|
||||
private PrivateKey privateKey;
|
||||
|
||||
public PrivateKeySigner(String serialNumber, PrivateKey privateKey) {
|
||||
this.certificateSerialNumber = serialNumber;
|
||||
this.privateKey = privateKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SignatureResult sign(byte[] message) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance("SHA256withRSA");
|
||||
sign.initSign(privateKey);
|
||||
sign.update(message);
|
||||
|
||||
return new SignatureResult(
|
||||
Base64.getEncoder().encodeToString(sign.sign()), certificateSerialNumber);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("当前Java环境不支持SHA256withRSA", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException("签名计算失败", e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new RuntimeException("无效的私钥", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
public interface Signer {
|
||||
SignatureResult sign(byte[] message);
|
||||
|
||||
class SignatureResult {
|
||||
String sign;
|
||||
String certificateSerialNumber;
|
||||
|
||||
public SignatureResult(String sign, String serialNumber) {
|
||||
this.sign = sign;
|
||||
this.certificateSerialNumber = serialNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
public interface Verifier {
|
||||
boolean verify(String serialNumber, byte[] message, String signature);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import com.github.binarywang.wxpay.v3.Credentials;
|
||||
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
|
||||
import org.apache.http.client.methods.HttpUriRequest;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class WechatPay2Credentials implements Credentials {
|
||||
private static final Logger log = LoggerFactory.getLogger(WechatPay2Credentials.class);
|
||||
|
||||
private static final String SYMBOLS =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
private static final SecureRandom RANDOM = new SecureRandom();
|
||||
protected String merchantId;
|
||||
protected Signer signer;
|
||||
|
||||
public WechatPay2Credentials(String merchantId, Signer signer) {
|
||||
this.merchantId = merchantId;
|
||||
this.signer = signer;
|
||||
}
|
||||
|
||||
public String getMerchantId() {
|
||||
return merchantId;
|
||||
}
|
||||
|
||||
protected long generateTimestamp() {
|
||||
return System.currentTimeMillis() / 1000;
|
||||
}
|
||||
|
||||
protected String generateNonceStr() {
|
||||
char[] nonceChars = new char[32];
|
||||
for (int index = 0; index < nonceChars.length; ++index) {
|
||||
nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
|
||||
}
|
||||
return new String(nonceChars);
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getSchema() {
|
||||
return "WECHATPAY2-SHA256-RSA2048";
|
||||
}
|
||||
|
||||
@Override
|
||||
public final String getToken(HttpUriRequest request) throws IOException {
|
||||
String nonceStr = generateNonceStr();
|
||||
long timestamp = generateTimestamp();
|
||||
|
||||
String message = buildMessage(nonceStr, timestamp, request);
|
||||
log.debug("authorization message=[{}]", message);
|
||||
|
||||
Signer.SignatureResult signature = signer.sign(message.getBytes("utf-8"));
|
||||
|
||||
String token = "mchid=\"" + getMerchantId() + "\","
|
||||
+ "nonce_str=\"" + nonceStr + "\","
|
||||
+ "timestamp=\"" + timestamp + "\","
|
||||
+ "serial_no=\"" + signature.certificateSerialNumber + "\","
|
||||
+ "signature=\"" + signature.sign + "\"";
|
||||
log.debug("authorization token=[{}]", token);
|
||||
|
||||
return token;
|
||||
}
|
||||
|
||||
protected final String buildMessage(String nonce, long timestamp, HttpUriRequest request)
|
||||
throws IOException {
|
||||
URI uri = request.getURI();
|
||||
String canonicalUrl = uri.getRawPath();
|
||||
if (uri.getQuery() != null) {
|
||||
canonicalUrl += "?" + uri.getRawQuery();
|
||||
}
|
||||
|
||||
String body = "";
|
||||
// PATCH,POST,PUT
|
||||
if (request instanceof HttpEntityEnclosingRequestBase) {
|
||||
body = EntityUtils.toString(((HttpEntityEnclosingRequestBase) request).getEntity());
|
||||
}
|
||||
|
||||
return request.getRequestLine().getMethod() + "\n"
|
||||
+ canonicalUrl + "\n"
|
||||
+ timestamp + "\n"
|
||||
+ nonce + "\n"
|
||||
+ body + "\n";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.github.binarywang.wxpay.v3.auth;
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.github.binarywang.wxpay.v3.Validator;
|
||||
import org.apache.http.Header;
|
||||
import org.apache.http.HttpEntity;
|
||||
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||
import org.apache.http.util.EntityUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class WechatPay2Validator implements Validator {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
|
||||
|
||||
private Verifier verifier;
|
||||
|
||||
public WechatPay2Validator(Verifier verifier) {
|
||||
this.verifier = verifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final boolean validate(CloseableHttpResponse response) throws IOException {
|
||||
Header serialNo = response.getFirstHeader("Wechatpay-Serial");
|
||||
Header sign = response.getFirstHeader("Wechatpay-Signature");
|
||||
Header timestamp = response.getFirstHeader("Wechatpay-TimeStamp");
|
||||
Header nonce = response.getFirstHeader("Wechatpay-Nonce");
|
||||
|
||||
// todo: check timestamp
|
||||
if (timestamp == null || nonce == null || serialNo == null || sign == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String message = buildMessage(response);
|
||||
return verifier.verify(serialNo.getValue(), message.getBytes("utf-8"), sign.getValue());
|
||||
}
|
||||
|
||||
protected final String buildMessage(CloseableHttpResponse response) throws IOException {
|
||||
String timestamp = response.getFirstHeader("Wechatpay-TimeStamp").getValue();
|
||||
String nonce = response.getFirstHeader("Wechatpay-Nonce").getValue();
|
||||
|
||||
String body = getResponseBody(response);
|
||||
return timestamp + "\n"
|
||||
+ nonce + "\n"
|
||||
+ body + "\n";
|
||||
}
|
||||
|
||||
protected final String getResponseBody(CloseableHttpResponse response) throws IOException {
|
||||
HttpEntity entity = response.getEntity();
|
||||
|
||||
return (entity != null && entity.isRepeatable()) ? EntityUtils.toString(entity) : "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package com.github.binarywang.wxpay.v3.util;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.SortedMap;
|
||||
import java.util.TreeMap;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class AesUtils {
|
||||
|
||||
static final int KEY_LENGTH_BYTE = 32;
|
||||
static final int TAG_LENGTH_BIT = 128;
|
||||
private final byte[] aesKey;
|
||||
|
||||
public AesUtils(byte[] key) {
|
||||
if (key.length != KEY_LENGTH_BYTE) {
|
||||
throw new IllegalArgumentException("无效的ApiV3Key,长度必须为32个字节");
|
||||
}
|
||||
this.aesKey = key;
|
||||
}
|
||||
|
||||
public String decryptToString(byte[] associatedData, byte[] nonce, String ciphertext)
|
||||
throws GeneralSecurityException, IOException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
|
||||
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
|
||||
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
cipher.updateAAD(associatedData);
|
||||
|
||||
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String decryptToString(String associatedData, String nonce, String ciphertext,String apiV3Key)
|
||||
throws GeneralSecurityException, IOException {
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
|
||||
|
||||
SecretKeySpec key = new SecretKeySpec(apiV3Key.getBytes(), "AES");
|
||||
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce.getBytes());
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, key, spec);
|
||||
cipher.updateAAD(associatedData.getBytes());
|
||||
|
||||
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), "utf-8");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static String createSign(Map<String, String> map, String mchKey) {
|
||||
Map<String, String> params = map;
|
||||
SortedMap<String, String> sortedMap = new TreeMap<>(params);
|
||||
|
||||
StringBuilder toSign = new StringBuilder();
|
||||
for (String key : sortedMap.keySet()) {
|
||||
String value = params.get(key);
|
||||
if ("sign".equals(key) || StringUtils.isEmpty(value)) {
|
||||
continue;
|
||||
}
|
||||
toSign.append(key).append("=").append(value).append("&");
|
||||
}
|
||||
toSign.append("key=" + mchKey);
|
||||
return HMACSHA256(toSign.toString(), mchKey);
|
||||
|
||||
}
|
||||
|
||||
public static String HMACSHA256(String data, String key) {
|
||||
try {
|
||||
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
|
||||
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
|
||||
sha256_HMAC.init(secret_key);
|
||||
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte item : array) {
|
||||
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
|
||||
}
|
||||
return sb.toString().toUpperCase();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.github.binarywang.wxpay.v3.util;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateExpiredException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.cert.CertificateNotYetValidException;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
public class PemUtils {
|
||||
|
||||
public static PrivateKey loadPrivateKey(InputStream inputStream) {
|
||||
try {
|
||||
ByteArrayOutputStream array = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = inputStream.read(buffer)) != -1) {
|
||||
array.write(buffer, 0, length);
|
||||
}
|
||||
|
||||
String privateKey = array.toString("utf-8")
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
return kf.generatePrivate(
|
||||
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(privateKey)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException("当前Java环境不支持RSA", e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new RuntimeException("无效的密钥格式");
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("无效的密钥");
|
||||
}
|
||||
}
|
||||
|
||||
public static X509Certificate loadCertificate(InputStream inputStream) {
|
||||
try {
|
||||
CertificateFactory cf = CertificateFactory.getInstance("X509");
|
||||
X509Certificate cert = (X509Certificate) cf.generateCertificate(inputStream);
|
||||
cert.checkValidity();
|
||||
return cert;
|
||||
} catch (CertificateExpiredException e) {
|
||||
throw new RuntimeException("证书已过期", e);
|
||||
} catch (CertificateNotYetValidException e) {
|
||||
throw new RuntimeException("证书尚未生效", e);
|
||||
} catch (CertificateException e) {
|
||||
throw new RuntimeException("无效的证书", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user