🆕 #1639 微信支付增加v3图片上传接口

1. 实现v3上传图片功能
文档地址: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/chapter3_1.shtml
2. 将接口获取到的证书保存到PayConfig中,v3接口中部分字段是敏感数据,在对这些数据加密时会用到
This commit is contained in:
叶枫
2020-08-07 13:50:07 +08:00
committed by GitHub
parent a9f9e30089
commit e7f2378f49
16 changed files with 376 additions and 32 deletions

View File

@@ -1,11 +1,12 @@
package com.github.binarywang.wxpay.v3;
import java.io.IOException;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.methods.HttpRequestWrapper;
public interface Credentials {
String getSchema();
String getToken(HttpUriRequest request) throws IOException;
String getToken(HttpRequestWrapper request) throws IOException;
}

View File

@@ -2,6 +2,7 @@ package com.github.binarywang.wxpay.v3;
import java.io.IOException;
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;
@@ -12,6 +13,7 @@ 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.BufferedHttpEntity;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.execchain.ClientExecChain;
import org.apache.http.util.EntityUtils;
@@ -43,11 +45,11 @@ public class SignatureExec implements ClientExecChain {
}
}
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));
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));
}
}
}
@@ -64,15 +66,16 @@ public class SignatureExec implements ClientExecChain {
private CloseableHttpResponse executeWithSignature(HttpRoute route, HttpRequestWrapper request,
HttpClientContext context, HttpExecutionAware execAware) throws IOException, HttpException {
HttpUriRequest newRequest = RequestBuilder.copy(request.getOriginal()).build();
convertToRepeatableRequestEntity(newRequest);
// 上传类不需要消耗两次故不做转换
if (!(request.getOriginal() instanceof WechatPayUploadHttpPost)) {
convertToRepeatableRequestEntity(request);
}
// 添加认证信息
newRequest.addHeader("Authorization",
credentials.getSchema() + " " + credentials.getToken(newRequest));
request.addHeader("Authorization",
credentials.getSchema() + " " + credentials.getToken(request));
// 执行
CloseableHttpResponse response = mainExec.execute(
route, HttpRequestWrapper.wrap(newRequest), context, execAware);
CloseableHttpResponse response = mainExec.execute(route, request, context, execAware);
// 对成功应答验签
StatusLine statusLine = response.getStatusLine();

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

View File

@@ -100,6 +100,14 @@ public class AutoUpdateCertificatesVerifier implements Verifier {
@Override
public boolean verify(String serialNumber, byte[] message, String signature) {
checkAndAutoUpdateCert();
return verifier.verify(serialNumber, message, signature);
}
/**
* 检查证书是否在有效期内,如果不在有效期内则进行更新
*/
private void checkAndAutoUpdateCert() {
if (instant == null || Minutes.minutesBetween(instant, Instant.now()).getMinutes() >= minutesInterval) {
if (lock.tryLock()) {
try {
@@ -113,7 +121,6 @@ public class AutoUpdateCertificatesVerifier implements Verifier {
}
}
}
return verifier.verify(serialNumber, message, signature);
}
private void autoUpdateCert() throws IOException, GeneralSecurityException {
@@ -179,4 +186,11 @@ public class AutoUpdateCertificatesVerifier implements Verifier {
return newCertList;
}
@Override
public X509Certificate getValidCertificate() {
checkAndAutoUpdateCert();
return verifier.getValidCertificate();
}
}

View File

@@ -5,10 +5,13 @@ 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<>();
@@ -40,4 +43,21 @@ public class CertificatesVerifier implements Verifier {
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("没有有效的微信支付平台证书");
}
}

View File

@@ -1,5 +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();
}

View File

@@ -1,16 +1,18 @@
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;
import com.github.binarywang.wxpay.v3.Credentials;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.util.EntityUtils;
@Slf4j
public class WxPayCredentials implements Credentials {
private static final String SYMBOLS =
@@ -46,14 +48,14 @@ public class WxPayCredentials implements Credentials {
}
@Override
public final String getToken(HttpUriRequest request) throws IOException {
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("utf-8"));
Signer.SignatureResult signature = signer.sign(message.getBytes(StandardCharsets.UTF_8));
String token = "mchid=\"" + getMerchantId() + "\","
+ "nonce_str=\"" + nonceStr + "\","
@@ -65,7 +67,7 @@ public class WxPayCredentials implements Credentials {
return token;
}
protected final String buildMessage(String nonce, long timestamp, HttpUriRequest request)
protected final String buildMessage(String nonce, long timestamp, HttpRequestWrapper request)
throws IOException {
URI uri = request.getURI();
String canonicalUrl = uri.getRawPath();
@@ -75,8 +77,10 @@ public class WxPayCredentials implements Credentials {
String body = "";
// PATCH,POST,PUT
if (request instanceof HttpEntityEnclosingRequestBase) {
body = EntityUtils.toString(((HttpEntityEnclosingRequestBase) request).getEntity());
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"