Commit b95df660 authored by licc's avatar licc

微信支付

parent 588bd17d
......@@ -48,19 +48,18 @@
</dependency>
<!--微信支付-->
<!-- IJPay -->
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-All</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>3.6.0</version>
</dependency>
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.2.1</version>
</dependency>
<!--pdf导出 -->
<dependency>
<groupId>com.itextpdf</groupId>
......
......@@ -2,7 +2,10 @@ package cn.wisenergy.service.app;
import cn.wisenergy.common.utils.R;
import cn.wisenergy.model.dto.PayPageDto;
import cn.wisenergy.model.dto.PayQueryDto;
import javax.crypto.IllegalBlockSizeException;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
......@@ -12,7 +15,7 @@ import java.security.spec.InvalidKeySpecException;
/**
* @author 86187
*/
public interface PayService {
public interface WxPayService {
/**
* 微行支付接口
......@@ -20,4 +23,16 @@ public interface PayService {
* @return
*/
R<String> wxPay(PayPageDto payPageDto) throws UnsupportedEncodingException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException;
/**
* 微信支付-交易查询
* @param payQueryDto 入参
* @return
*/
R<String> queryWx(PayQueryDto payQueryDto) throws UnsupportedEncodingException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException;
R<String> wx_Pay(PayPageDto payPageDto) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException, IllegalBlockSizeException;
}
package cn.wisenergy.service.app.impl;
import cn.wisenergy.common.utils.R;
import cn.wisenergy.model.dto.PayPageDto;
import cn.wisenergy.service.app.PayService;
import cn.wisenergy.service.util.SignDemo;
import cn.wisenergy.service.util.SignUtil;
import cn.wisenergy.service.wxpay.WxCommon;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import okhttp3.HttpUrl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
/**
* @author 86187
*/
@Service
@Slf4j
public class PayServiceImpl implements PayService {
@Autowired
private RestTemplate restTemplate;
@Override
public R<String> wxPay(PayPageDto payPageDto) throws UnsupportedEncodingException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
if (null == payPageDto || null == payPageDto.getTotal()) {
return R.error("入参不能为空!");
}
final HttpHeaders headers = new HttpHeaders();
MediaType type = MediaType.parseMediaType("application/json; charset=UTF-8");
headers.setContentType(type);
long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String url = "v3/pay/transactions/native";
String method = "POST";
String tradeNo = "21" + System.currentTimeMillis();
HttpUrl httpurl = HttpUrl.parse(WxCommon.WX_PAY_URL);
//构造签名参数
JSONObject jsonObject = new JSONObject();
jsonObject.put("appid", WxCommon.APP_ID);
jsonObject.put("mchid", WxCommon.MCHID);
jsonObject.put("timestamp", timestamp);
jsonObject.put("nonce_str", nonceStr);
jsonObject.put("url", url);
jsonObject.put("method", method);
jsonObject.put("description", "充值");
jsonObject.put("out_trade_no", tradeNo);
jsonObject.put("notify_url", WxCommon.NOTIFY_URL);
jsonObject.put("amount", payPageDto);
String sign = SignDemo.getToken(method, httpurl, jsonObject.toJSONString(),nonceStr,timestamp);
headers.add("Wechatpay-Signature", sign);
HttpEntity<JSONObject> formEntity = new HttpEntity<>(jsonObject, headers);
ResponseEntity<JSONObject> responseEntity = this.restTemplate.postForEntity(WxCommon.WX_PAY_URL, formEntity, JSONObject.class);
return R.ok(Objects.requireNonNull(responseEntity.getBody()).toJSONString());
}
}
package cn.wisenergy.service.httpClient;
import org.apache.http.client.methods.HttpRequestWrapper;
import java.io.IOException;
public interface Credentials {
String getSchema();
String getToken(HttpRequestWrapper request) throws IOException;
}
package cn.wisenergy.service.httpClient;
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.impl.execchain.ClientExecChain;
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 void convertToRepeatableResponseEntity(CloseableHttpResponse response)
throws IOException {
HttpEntity entity = response.getEntity();
if (entity != null) {
response.setEntity(new BufferedHttpEntity(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;
}
}
package cn.wisenergy.service.httpClient;
import org.apache.http.client.methods.CloseableHttpResponse;
import java.io.IOException;
public interface Validator {
boolean validate(CloseableHttpResponse response) throws IOException;
}
package cn.wisenergy.service.httpClient;
import cn.wisenergy.service.httpClient.auth.CertificatesVerifier;
import cn.wisenergy.service.httpClient.auth.PrivateKeySigner;
import cn.wisenergy.service.httpClient.auth.WechatPay2Credentials;
import cn.wisenergy.service.httpClient.auth.WechatPay2Validator;
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 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);
}
}
package cn.wisenergy.service.httpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import java.io.InputStream;
import java.net.URI;
import java.net.URLConnection;
/**
* @author 86187
*/
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 org.apache.http.entity.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, org.apache.http.entity.ContentType.APPLICATION_JSON);
//
// request.setEntity(entityBuilder.build());
// request.addHeader("Accept", org.apache.http.entity.ContentType.APPLICATION_JSON.toString());
return request;
}
}
}
package cn.wisenergy.service.httpClient.auth;
import cn.wisenergy.service.httpClient.Credentials;
import cn.wisenergy.service.httpClient.WechatPayHttpClientBuilder;
import cn.wisenergy.service.httpClient.util.AesUtil;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
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;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
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;
/**
* 在原有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 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);
}
}
public AutoUpdateCertificatesVerifier(WechatPay2Credentials wechatPay2Credentials, byte[] bytes) {
}
@Override
public X509Certificate getValidCertificate() {
return verifier.getValidCertificate();
}
@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();
try {
HttpGet httpGet = new HttpGet(CertDownloadPath);
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response = httpClient.execute(httpGet);
try {
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);
}
} finally {
response.close();
}
} finally {
httpClient.close();
}
}
/**
* 反序列化证书并解密
*/
private List<X509Certificate> deserializeToCerts(byte[] apiV3Key, String body)
throws GeneralSecurityException, IOException {
AesUtil decryptor = new AesUtil(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("\"", ""));
CertificateFactory cf = CertificateFactory.getInstance("X509");
X509Certificate x509Cert = (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream(cert.getBytes("utf-8"))
);
try {
x509Cert.checkValidity();
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
continue;
}
newCertList.add(x509Cert);
}
}
return newCertList;
}
/**
* 时间间隔枚举,支持一小时、六小时以及十二小时
*/
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;
}
}
}
package cn.wisenergy.service.httpClient.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.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;
/**
* @author 86187
*/
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);
}
@Override
public X509Certificate getValidCertificate() {
for (X509Certificate x509Cert : certificates.values()) {
try {
x509Cert.checkValidity();
return x509Cert;
} catch (CertificateExpiredException | CertificateNotYetValidException e) {
continue;
}
}
throw new NoSuchElementException("没有有效的微信支付平台证书");
}
}
package cn.wisenergy.service.httpClient.auth;
import java.security.*;
import java.util.Base64;
/**
* @author 86187
*/
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);
}
}
}
package cn.wisenergy.service.httpClient.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;
}
}
}
package cn.wisenergy.service.httpClient.auth;
import java.security.cert.X509Certificate;
/**
* @author 86187
*/
public interface Verifier {
boolean verify(String serialNumber, byte[] message, String signature);
X509Certificate getValidCertificate();
}
package cn.wisenergy.service.httpClient.auth;
import cn.wisenergy.service.httpClient.Credentials;
import cn.wisenergy.service.httpClient.WechatPayUploadHttpPost;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
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(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";
}
}
package cn.wisenergy.service.httpClient.auth;
import cn.wisenergy.service.httpClient.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;
import java.io.IOException;
import java.time.DateTimeException;
import java.time.Duration;
import java.time.Instant;
/**
* @author 86187
*/
public class WechatPay2Validator implements Validator {
private static final Logger log = LoggerFactory.getLogger(WechatPay2Validator.class);
private Verifier verifier;
public WechatPay2Validator(Verifier verifier) {
this.verifier = verifier;
}
static RuntimeException parameterError(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("parameter error: " + message);
}
static RuntimeException verifyFail(String message, Object... args) {
message = String.format(message, args);
return new IllegalArgumentException("signature verify fail: " + message);
}
@Override
public final boolean validate(CloseableHttpResponse response) throws IOException {
try {
validateParameters(response);
String message = buildMessage(response);
String serial = response.getFirstHeader("Wechatpay-Serial").getValue();
String signature = response.getFirstHeader("Wechatpay-Signature").getValue();
if (!verifier.verify(serial, message.getBytes("utf-8"), signature)) {
throw verifyFail("serial=[%s] message=[%s] sign=[%s], request-id=[%s]",
serial, message, signature,
response.getFirstHeader("Request-ID").getValue());
}
} catch (IllegalArgumentException e) {
log.warn(e.getMessage());
return false;
}
return true;
}
protected final void validateParameters(CloseableHttpResponse response) {
String requestId;
if (!response.containsHeader("Request-ID")) {
throw parameterError("empty Request-ID");
} else {
requestId = response.getFirstHeader("Request-ID").getValue();
}
if (!response.containsHeader("Wechatpay-Serial")) {
throw parameterError("empty Wechatpay-Serial, request-id=[%s]", requestId);
} else if (!response.containsHeader("Wechatpay-Signature")){
throw parameterError("empty Wechatpay-Signature, request-id=[%s]", requestId);
} else if (!response.containsHeader("Wechatpay-Timestamp")) {
throw parameterError("empty Wechatpay-Timestamp, request-id=[%s]", requestId);
} else if (!response.containsHeader("Wechatpay-Nonce")) {
throw parameterError("empty Wechatpay-Nonce, request-id=[%s]", requestId);
} else {
Header timestamp = response.getFirstHeader("Wechatpay-Timestamp");
try {
Instant instant = Instant.ofEpochSecond(Long.parseLong(timestamp.getValue()));
// 拒绝5分钟之外的应答
if (Duration.between(instant, Instant.now()).abs().toMinutes() >= 5) {
throw parameterError("timestamp=[%s] expires, request-id=[%s]",
timestamp.getValue(), requestId);
}
} catch (DateTimeException | NumberFormatException e) {
throw parameterError("invalid timestamp=[%s], request-id=[%s]",
timestamp.getValue(), requestId);
}
}
}
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) : "";
}
}
package cn.wisenergy.service.httpClient.util;
import javax.crypto.Cipher;
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;
public class AesUtil {
static final int KEY_LENGTH_BYTE = 32;
static final int TAG_LENGTH_BIT = 128;
private final byte[] aesKey;
public AesUtil(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);
}
}
}
package cn.wisenergy.service.httpClient.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.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class PemUtil {
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);
}
}
}
package cn.wisenergy.service.httpClient.util;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
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;
public class RsaCryptoUtil {
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 RuntimeException("当前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 RuntimeException("当前Java环境不支持RSA v1.5/OAEP", e);
} catch (InvalidKeyException e) {
throw new IllegalArgumentException("无效的私钥", e);
} catch (BadPaddingException | IllegalBlockSizeException e) {
throw new BadPaddingException("解密失败");
}
}
}
package cn.wisenergy.service.util;
import org.apache.commons.codec.digest.DigestUtils;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Map;
/**
* @author 86187
*/
public class SignUtil {
public static String genSignature(String secretKey, Map<String, Object> params) throws UnsupportedEncodingException {
//secretKey为商户平台设置的密钥key
if (secretKey == null || params == null || params.size() == 0) {
return "";
}
// 1. 参数名按照ASCII码表升序排序
String[] keys = params.keySet().toArray(new String[0]);
Arrays.sort(keys);
// 2. 按照排序拼接参数名与参数值
StringBuffer paramBuffer = new StringBuffer();
for (String key : keys) {
paramBuffer.append("&" + key).append(params.get(key) == null ? "" : "=" + params.get(key));
}
// 3. 将secretKey拼接到最后
paramBuffer = paramBuffer.append("&key=" + secretKey);
String pa = paramBuffer.substring(1);
// 4. MD5是128位长度的摘要算法,用16进制表示,一个十六进制的字符能表示4个位,所以签名后的字符串长度固定为32个十六进制字符。
return DigestUtils.md5Hex(pa.getBytes("UTF-8")).toUpperCase();
}
}
package cn.wisenergy.service.util;
import cn.wisenergy.common.utils.R;
import cn.wisenergy.model.dto.PayPageDto;
import cn.wisenergy.service.httpClient.WechatPayHttpClientBuilder;
import cn.wisenergy.service.httpClient.auth.AutoUpdateCertificatesVerifier;
import cn.wisenergy.service.httpClient.auth.PrivateKeySigner;
import cn.wisenergy.service.httpClient.auth.WechatPay2Credentials;
import cn.wisenergy.service.httpClient.auth.WechatPay2Validator;
import cn.wisenergy.service.httpClient.util.PemUtil;
import cn.wisenergy.service.wxpay.WxCommon;
import com.alibaba.fastjson.JSONObject;
import okhttp3.HttpUrl;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.After;
import org.junit.Before;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.util.UUID;
public class WxPayUtil {
/**
* 商户号
*/
private static String mchId = WxCommon.MCHID;
// 商户证书序列号
private static String mchSerialNo = WxCommon.SERIAL_NO;
// api密钥
private static String apiV3Key = WxCommon.SECRET_KEY;
// 你的商户私钥
private static String privateKey = "-----BEGIN PRIVATE KEY-----\n" +WxCommon.PRIVATE_KEY
+ "-----END PRIVATE KEY-----\n";
private static CloseableHttpClient httpClient;
private static AutoUpdateCertificatesVerifier verifier;
static {
PrivateKey merchantPrivateKey = null;
try {
merchantPrivateKey = PemUtil.loadPrivateKey(
new ByteArrayInputStream(privateKey.getBytes("utf-8")));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
//使用自动更新的签名验证器,不需要传入证书
try {
verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),
apiV3Key.getBytes("utf-8"));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
.build();
}
@After
public void after() throws IOException {
httpClient.close();
}
public static void main(String[] args) throws IOException, NoSuchAlgorithmException, SignatureException, InvalidKeySpecException, InvalidKeyException {
HttpPost httpPost = new HttpPost(WxCommon.WX_PAY_URL);
long timestamp = System.currentTimeMillis() / 1000;
String nonceStr = UUID.randomUUID().toString().replace("-", "");
String method = "POST";
String tradeNo = "21" + System.currentTimeMillis();
HttpUrl httpurl = HttpUrl.parse(WxCommon.WX_PAY_URL);
// 请求body参数
String reqdata = "{"
+ "\"time_expire\":\"2021-02-07T10:34:56+08:00\","
+ "\"amount\": {"
+ "\"total\":100,"
+ "\"currency\":\"CNY\""
+ "},"
+ "\"mchid\":\"" + WxCommon.MCHID + "\","
+ "\"description\":\"Image形象店-深圳腾大-QQ公仔\","
+ "\"notify_url\":\"" + WxCommon.NOTIFY_URL + "\","
+ "\"out_trade_no\":\"" + tradeNo + "\","
+ "\"goods_tag\":\"WXG\","
+ "\"appid\":\"" + WxCommon.APP_ID + "\","
+ "\"attach\":\"自定义数据说明\","
+ "\"detail\": {"
+ "\"invoice_id\":\"wx123\","
+ "\"goods_detail\": ["
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "},"
+ "{"
+ "\"goods_name\":\"iPhoneX 256G\","
+ "\"wechatpay_goods_id\":\"1001\","
+ "\"quantity\":1,"
+ "\"merchant_goods_id\":\"商品编码\","
+ "\"unit_price\":828800"
+ "}"
+ "],"
+ "\"cost_price\":608800"
+ "},"
+ "\"scene_info\": {"
+ "\"store_info\": {"
+ "\"address\":\"广东省深圳市南山区科技中一道10000号\","
+ "\"area_code\":\"440305\","
+ "\"name\":\"腾讯大厦分店\","
+ "\"id\":\"0001\""
+ "},"
+ "\"device_id\":\"013467007045764\","
+ "\"payer_client_ip\":\"14.23.150.211\""
+ "}"
+ "}";
StringEntity reqEntity = new StringEntity(
reqdata, ContentType.create("application/json", "utf-8"));
httpPost.setEntity(reqEntity);
httpPost.addHeader("Accept", "application/json");
// //构造签名参数
// //构造签名参数
// JSONObject jsonObject = new JSONObject();
// jsonObject.put("appid", WxCommon.APP_ID);
// jsonObject.put("mchid", WxCommon.MCHID);
// jsonObject.put("description", "充值");
// jsonObject.put("out_trade_no", tradeNo);
// jsonObject.put("notify_url", WxCommon.NOTIFY_URL);
// PayPageDto payPageDto=new PayPageDto();
// payPageDto.setTotal(100);
// jsonObject.put("amount", payPageDto);
// String token = SignDemo.getToken(method, httpurl, jsonObject.toJSONString(), nonceStr, timestamp);
// httpPost.setHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " " + token);
//完成签名并执行请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
if (response.getStatusLine().getStatusCode() == 200) {
HttpEntity httpEntity = response.getEntity();
String content = EntityUtils.toString(httpEntity, "utf8");
System.out.println(content.length());
}
} catch (IOException e) {
e.printStackTrace();
}finally {
response.close();
}
}
}
......@@ -16,6 +16,7 @@ public class WxCommon {
public static final String WX_PAY_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
public static final String WX_PAY_QUERY = " https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/";
public static final String SECRET_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhGq+iGQueP8EU" +
"3qj0T0Otnha0XboVcgmeDkgbe08H54WiF9d3R4aLAo+wAkAj/R7nRw2yWeaaMEgb" +
"ZvUz03IioVKwLhaMVEtwE5sNFCMGDDh9jGjm66j+BYgVk02P5hUAcYLcJYeo9iHA" +
......@@ -52,4 +53,9 @@ public class WxCommon {
* 认证类型
*/
public static final String SCHEMA = "WECHATPAY2-SHA256-RSA2048";
/**
* 认证类型
*/
public static final String PRIVATE_KEY = "efef4a06a1654e0f78d113377ea37aed";
}
......@@ -3,7 +3,7 @@ package cn.wisenergy.web.admin.controller.app;
import cn.wisenergy.common.utils.R;
import cn.wisenergy.model.dto.PayPageDto;
import cn.wisenergy.model.dto.PayQueryDto;
import cn.wisenergy.service.app.PayService;
import cn.wisenergy.service.app.WxPayService;
import cn.wisenergy.service.common.Common;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
......@@ -22,6 +22,7 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.crypto.IllegalBlockSizeException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
......@@ -45,7 +46,7 @@ public class PayController {
private static final String CHARSET = "UTF-8";
@Autowired
private PayService payService;
private WxPayService wxPayService;
@ApiOperation(value = "PC支付宝-支付接口", notes = "PC支付宝-支付接口", httpMethod = "POST")
@ApiImplicitParam(name = "payPageDto", value = "参数", dataType = "PayPageDto")
......@@ -193,6 +194,25 @@ public class PayController {
@ApiImplicitParam(name = "payPageDto", value = "支付入参", dataType = "PayPageDto")
@PostMapping("/wxPay")
public R<String> wxPay(@RequestBody PayPageDto payPageDto) throws AlipayApiException, UnsupportedEncodingException, InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, SignatureException {
return payService.wxPay(payPageDto);
return wxPayService.wxPay(payPageDto);
}
@ApiOperation(value = "微信支付-交易查询", notes = "微信支付-交易查询", httpMethod = "POST")
@ApiImplicitParam(name = "payQueryDto", value = "查询参数", dataType = "PayQueryDto")
@PostMapping("/queryWx")
public R<String> queryWx(@RequestBody PayQueryDto payQueryDto) throws AlipayApiException, InvalidKeySpecException, SignatureException, NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
if (null == payQueryDto || StringUtils.isBlank(payQueryDto.getOutTradeNo())) {
return R.error("操作错误!");
}
return wxPayService.queryWx(payQueryDto);
}
@ApiOperation(value = "测试-微行支付-统一下单", notes = "测试-微行支付-统一下单", httpMethod = "POST")
@ApiImplicitParam(name = "payPageDto", value = "支付入参", dataType = "PayPageDto")
@PostMapping("/wx_Pay")
public R<String> wx_Pay(@RequestBody PayPageDto payPageDto) throws AlipayApiException, IOException, InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, SignatureException, IllegalBlockSizeException {
return wxPayService.wx_Pay(payPageDto);
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment