🆕 #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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 376 additions and 32 deletions

View File

@ -0,0 +1,28 @@
package com.github.binarywang.wxpay.bean.media;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.NoArgsConstructor;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
/**
* 媒体文件上传返回结果对象
* @author zhouyongshen
*/
@NoArgsConstructor
@Data
public class ImageUploadResult {
public static ImageUploadResult fromJson(String json) {
return WxGsonBuilder.create().fromJson(json, ImageUploadResult.class);
}
/**
* 媒体文件标识 Id
*
* 微信返回的媒体文件标识Id
* 示例值6uqyGjGrCf2GtyXP8bxrbuH9-aAoTjH-rKeSl3Lf4_So6kdkQu4w8BYVP3bzLtvR38lxt4PjtCDXsQpzqge_hQEovHzOhsLleGFQVRF-U_0
*
*/
@SerializedName("media_id")
private String mediaId;
}

View File

@ -2,10 +2,7 @@ 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.AutoUpdateCertificatesVerifier;
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 com.github.binarywang.wxpay.v3.auth.*;
import com.github.binarywang.wxpay.v3.util.PemUtils;
import jodd.util.ResourcesUtil;
import lombok.Data;
@ -153,6 +150,12 @@ 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;
/**
* 返回所设置的微信支付接口请求地址域名.
*
@ -297,14 +300,20 @@ public class WxPayConfig {
try {
PrivateKey merchantPrivateKey = PemUtils.loadPrivateKey(keyInputStream);
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
apiV3Key.getBytes(StandardCharsets.UTF_8));
CloseableHttpClient httpClient = WxPayV3HttpClientBuilder.create()
.withMerchant(mchId, certSerialNo, merchantPrivateKey)
.withWechatpay(Collections.singletonList(PemUtils.loadCertificate(certInputStream)))
.withValidator(new WxPayValidator(new AutoUpdateCertificatesVerifier(
new WxPayCredentials(mchId, new PrivateKeySigner(certSerialNo, merchantPrivateKey)),
apiV3Key.getBytes(StandardCharsets.UTF_8))))
.withValidator(new WxPayValidator(verifier))
.build();
this.apiV3HttpClient = httpClient;
this.verifier=verifier;
return httpClient;
} catch (Exception e) {
throw new WxPayException("v3请求构造异常", e);

View File

@ -0,0 +1,31 @@
package com.github.binarywang.wxpay.service;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import java.io.File;
import java.io.IOException;
/**
* <pre>
* 微信支付通用媒体接口.
* </pre>
*
* @author zhouyongshen
*/
public interface MerchantMediaService {
/**
* <pre>
* 通用接口-图片上传API
* 文档详见: https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/tool/chapter3_1.shtml
* 接口链接https://api.mch.weixin.qq.com/v3/merchant/media/upload
* </pre>
*
* @param imageFile 需要上传的图片文件
* @return ImageUploadResult 微信返回的媒体文件标识Id示例值6uqyGjGrCf2GtyXP8bxrbuH9-aAoTjH-rKeSl3Lf4_So6kdkQu4w8BYVP3bzLtvR38lxt4PjtCDXsQpzqge_hQEovHzOhsLleGFQVRF-U_0
* @throws WxPayException the wx pay exception
*/
ImageUploadResult imageUploadV3(File imageFile) throws WxPayException, IOException;
}

View File

@ -10,6 +10,7 @@ import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.constant.WxPayConstants;
import com.github.binarywang.wxpay.exception.WxPayException;
import org.apache.http.client.methods.HttpPost;
import java.io.File;
import java.net.URI;
@ -65,6 +66,16 @@ public interface WxPayService {
*/
String postV3(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;
/**
* 发送get V3请求得到响应字符串.
*

View File

@ -0,0 +1,44 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MerchantMediaService;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.v3.WechatPayUploadHttpPost;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
/**
* 微信支付-媒体文件上传service
* @author zhouyongshen
*/
@Slf4j
@RequiredArgsConstructor
public class MerchantMediaServiceImpl implements MerchantMediaService {
private final WxPayService payService;
@Override
public ImageUploadResult imageUploadV3(File imageFile) throws WxPayException,IOException {
String url = String.format("%s/v3/merchant/media/upload", this.payService.getPayBaseUrl());
try (FileInputStream s1 = new FileInputStream(imageFile)) {
String sha256 = DigestUtils.sha256Hex(s1);
try (InputStream s2 = new FileInputStream(imageFile)) {
WechatPayUploadHttpPost request = new WechatPayUploadHttpPost.Builder(URI.create(url))
.withImage(imageFile.getName(), sha256, s2)
.build();
String result = this.payService.postV3(url, request);
return ImageUploadResult.fromJson(result);
}
}
}
}

View File

@ -116,6 +116,36 @@ public class WxPayServiceApacheHttpImpl extends BaseWxPayServiceImpl {
}
@Override
public String postV3(String url, HttpPost httpPost) throws WxPayException {
httpPost.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(httpPost)) {
//v3已经改为通过状态码判断200 204 成功
int statusCode = response.getStatusLine().getStatusCode();
String responseString = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
if (HttpStatus.SC_OK == statusCode || HttpStatus.SC_NO_CONTENT == statusCode) {
this.log.info("\n【请求地址】{}\n【响应数据】{}", url, responseString);
return responseString;
} else {
//有错误提示信息返回
JsonObject jsonObject = GsonParser.parse(responseString);
throw new WxPayException(jsonObject.get("message").getAsString());
}
} catch (Exception e) {
this.log.error("\n【请求地址】{}\n【异常信息】{}", url, e.getMessage());
throw new WxPayException(e.getMessage(), e);
} finally {
httpPost.releaseConnection();
}
}
@Override
public String getV3(URI url) throws WxPayException {
CloseableHttpClient httpClient = this.createApiV3HttpClient();

View File

@ -16,6 +16,7 @@ import jodd.http.ProxyInfo.ProxyType;
import jodd.http.net.SSLSocketHttpConnectionProvider;
import jodd.http.net.SocketHttpConnectionProvider;
import jodd.util.Base64;
import org.apache.http.client.methods.HttpPost;
/**
* 微信支付请求实现类jodd-http实现.
@ -65,6 +66,11 @@ public class WxPayServiceJoddHttpImpl extends BaseWxPayServiceImpl {
return null;
}
@Override
public String postV3(String url, HttpPost httpPost) throws WxPayException {
return null;
}
@Override
public String getV3(URI url) throws WxPayException {
return null;

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"

View File

@ -0,0 +1,54 @@
package com.github.binarywang.wxpay.service.impl;
import com.github.binarywang.wxpay.bean.media.ImageUploadResult;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.MerchantMediaService;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.testbase.ApiTestModule;
import com.google.inject.Inject;
import lombok.extern.slf4j.Slf4j;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import java.io.File;
import java.io.IOException;
/**
* <pre>
* 媒体文件上传测试类
* </pre>
*
* @author zhouyongshen
*/
@Slf4j
@Test
@Guice(modules = ApiTestModule.class)
public class MerchantMediaServiceImplTest {
@Inject
private WxPayService wxPayService;
@Test
public void testImageUploadV3() throws WxPayException, IOException {
MerchantMediaService merchantMediaService=new MerchantMediaServiceImpl(wxPayService);
String filePath="你的图片文件的路径地址";
// String filePath="WxJava/images/banners/wiki.jpg";
File file=new File(filePath);
ImageUploadResult imageUploadResult = merchantMediaService.imageUploadV3(file);
String mediaId = imageUploadResult.getMediaId();
log.info("mediaId1[{}]",mediaId);
File file2=new File(filePath);
ImageUploadResult imageUploadResult2 = merchantMediaService.imageUploadV3(file2);
String mediaId2 = imageUploadResult2.getMediaId();
log.info("mediaId2[{}]",mediaId2);
}
}

View File

@ -10,4 +10,12 @@
-->
<keyPath>商户平台的证书文件地址</keyPath>
<openid>某个openId</openid>
<!--
apiv3 模式下所需配置
<privateKeyPath>apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径.</privateKeyPath>
<privateCertPath>apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径.</privateCertPath>
<apiV3Key> apiV3 秘钥值.</apiV3Key>
<certSerialNo>apiV3 证书序列号值</certSerialNo>
-->
</xml>