修改启动问题
This commit is contained in:
parent
5c603c6151
commit
65198b9cbb
@ -361,7 +361,7 @@ wechat:
|
||||
mch-id: 1658665710 # 商户号
|
||||
mch-serial-no: 6BA681D9B219034D6F7851F57D61BE9317AB48FD # 商户证书序列号
|
||||
api-v3-key: T9iE71aHSmjtM35z4bDLuU3gFX8s2I2h # APIv3密钥
|
||||
private-key-path: D:\wzj-boot\ruoyi-admin\src\main\resources\apiclient_key.pem # 商户私钥文件路径
|
||||
private-key-path: "classpath:cert/apiclient_key.pem" # 商户私钥文件路径
|
||||
transfer-notify-url: https://a94aeb5582c2.ngrok-free.app/no-auth/wechat/notify # 转账回调地址
|
||||
app-id: wxebcdaea31881caab # 应用ID
|
||||
secret: your_wechat_secret # 应用密钥
|
||||
|
@ -73,12 +73,10 @@ public class SysTenant extends BaseEntity {
|
||||
private LocalDateTime joinTime;
|
||||
|
||||
@Schema(description = "营业执照附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> attachmentimg;
|
||||
private String attachmentimg;
|
||||
|
||||
@Schema(description = "推广附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> promoteListimg;
|
||||
private String promoteListimg;
|
||||
|
||||
@Schema(description = "营业执照有效期")
|
||||
private LocalDateTime expireTime;
|
||||
@ -90,12 +88,10 @@ public class SysTenant extends BaseEntity {
|
||||
private String companyType;
|
||||
|
||||
@Schema(description = "经营类型")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> businessTypes;
|
||||
private String businessTypes;
|
||||
|
||||
@Schema(description = "相关资质名称及附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> relatedimg;
|
||||
private String relatedimg;
|
||||
|
||||
@Schema(description = "联系人")
|
||||
private String contactUserName;
|
||||
|
@ -33,8 +33,7 @@ public class SysTenantExtend extends BaseAudit {
|
||||
private String idCardType;
|
||||
|
||||
@Schema(description = "证件图片")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> certificateimg;
|
||||
private String certificateimg;
|
||||
|
||||
@Schema(description = "负责人姓名")
|
||||
private String personName;
|
||||
@ -82,8 +81,7 @@ public class SysTenantExtend extends BaseAudit {
|
||||
private String contractYear;
|
||||
|
||||
@Schema(description = "签约附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> contractAttachmentimg;
|
||||
private String contractAttachmentimg;
|
||||
|
||||
@Schema(description = "签约状态")
|
||||
private String signStatus;
|
||||
|
@ -53,12 +53,10 @@ public class SysTenantExtendBo {
|
||||
private LocalDateTime joinTime;
|
||||
|
||||
@Schema(description = "营业执照附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> attachmentimg;
|
||||
private String attachmentimg;
|
||||
|
||||
@Schema(description = "推广附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> promoteListimg;
|
||||
private String promoteListimg;
|
||||
|
||||
@Schema(description = "营业执照有效期")
|
||||
private LocalDateTime expireTime;
|
||||
@ -70,12 +68,10 @@ public class SysTenantExtendBo {
|
||||
private String companyType;
|
||||
|
||||
@Schema(description = "经营类型")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> businessTypes;
|
||||
private String businessTypes;
|
||||
|
||||
@Schema(description = "相关资质名称及附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> relatedimg;
|
||||
private String relatedimg;
|
||||
|
||||
@Schema(description = "店铺id")
|
||||
private Long storeId;
|
||||
@ -87,8 +83,7 @@ public class SysTenantExtendBo {
|
||||
private String idCardType;
|
||||
|
||||
@Schema(description = "证件图片")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> certificateimg;
|
||||
private String certificateimg;
|
||||
|
||||
@Schema(description = "负责人姓名")
|
||||
private String personName;
|
||||
@ -143,8 +138,7 @@ public class SysTenantExtendBo {
|
||||
private String contractYear;
|
||||
|
||||
@Schema(description = "签约附件")
|
||||
@TableField(typeHandler = JacksonTypeHandler.class)
|
||||
private List<String> contractAttachmentimg;
|
||||
private String contractAttachmentimg;
|
||||
|
||||
@Schema(description = "签约状态")
|
||||
private String signStatus;
|
||||
|
@ -57,10 +57,10 @@ public interface SysTenantMapper extends BaseMapperPlus<SysTenant, SysTenantVo>
|
||||
List<TenantDTO> getAll(SysTenantBo bo);
|
||||
|
||||
|
||||
@Select("SELECT t.*, e.*, m.nickname AS inviteUserName \n" +
|
||||
"FROM sys_tenant t\n" +
|
||||
"LEFT JOIN sys_tenant_extend e ON t.id = e.store_id\n" +
|
||||
"LEFT JOIN ums_member m ON e.invite_user_id = m.id\n" +
|
||||
"WHERE t.id = #{id}")
|
||||
TenantDTO getById(Long id);
|
||||
@Select("SELECT t.*, e.*, m.nickname AS inviteUserName \n" +
|
||||
"FROM sys_tenant t\n" +
|
||||
"LEFT JOIN sys_tenant_extend e ON t.id = e.store_id\n" +
|
||||
"LEFT JOIN ums_member m ON e.invite_user_id = m.id\n" +
|
||||
"WHERE t.id = #{id}")
|
||||
TenantDTO getById(Long id);
|
||||
}
|
||||
|
@ -1,455 +1,343 @@
|
||||
package com.wzj.soopin.transaction.wechat;
|
||||
|
||||
import com.google.gson.ExclusionStrategy;
|
||||
import com.google.gson.FieldAttributes;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.*;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.wechat.pay.java.core.util.GsonUtil;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSource;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import javax.crypto.*;
|
||||
import java.io.*;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Duration;
|
||||
import java.nio.file.Path;
|
||||
import java.security.*;
|
||||
import java.security.spec.*;
|
||||
import java.time.Instant;
|
||||
import java.time.Duration;
|
||||
import java.time.DateTimeException;
|
||||
import java.util.Base64;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WXPayUtility {
|
||||
private static final Gson gson = new GsonBuilder()
|
||||
.disableHtmlEscaping()
|
||||
.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.serialize();
|
||||
private static final Gson gson = new GsonBuilder()
|
||||
.disableHtmlEscaping()
|
||||
.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.addDeserializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.deserialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.create();
|
||||
|
||||
private static final char[] SYMBOLS =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
/* JSON 处理方法保持不变 */
|
||||
public static String toJson(Object object) {
|
||||
return gson.toJson(object);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
|
||||
return gson.fromJson(json, classOfT);
|
||||
}
|
||||
|
||||
/* 新增:支持从InputStream加载密钥 */
|
||||
public static PrivateKey loadPrivateKey(InputStream inputStream) throws IOException {
|
||||
String keyContent = readKeyContent(inputStream);
|
||||
return parsePrivateKey(keyContent);
|
||||
}
|
||||
|
||||
/* 优化后的路径处理方法 */
|
||||
public static PrivateKey loadPrivateKeyFromPath(String keyPath) throws IOException {
|
||||
// 处理 classpath: 前缀
|
||||
if (keyPath.startsWith("classpath:")) {
|
||||
String path = keyPath.substring("classpath:".length());
|
||||
try (InputStream is = WXPayUtility.class.getClassLoader().getResourceAsStream(path)) {
|
||||
if (is == null) {
|
||||
throw new FileNotFoundException("Classpath resource not found: " + path);
|
||||
}
|
||||
return loadPrivateKey(is);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
// 处理 file: 前缀或普通路径
|
||||
String filePath = keyPath.replaceFirst("^file:", "");
|
||||
try (InputStream inputStream = Files.newInputStream(Path.of(filePath))) {
|
||||
return loadPrivateKey(inputStream);
|
||||
}
|
||||
})
|
||||
.addDeserializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.deserialize();
|
||||
}
|
||||
|
||||
/* 优化后的密钥内容读取方法 */
|
||||
private static String readKeyContent(InputStream inputStream) throws IOException {
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
return reader.lines()
|
||||
.filter(line -> !line.startsWith("-----"))
|
||||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.create();
|
||||
private static final char[] SYMBOLS =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
/**
|
||||
* 将 Object 转换为 JSON 字符串
|
||||
*/
|
||||
public static String toJson(Object object) {
|
||||
return gson.toJson(object);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串解析为特定类型的实例
|
||||
*/
|
||||
public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
|
||||
return gson.fromJson(json, classOfT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从公私钥文件路径中读取文件内容
|
||||
*
|
||||
* @param keyPath 文件路径
|
||||
* @return 文件内容
|
||||
*/
|
||||
private static String readKeyStringFromPath(String keyPath) {
|
||||
try {
|
||||
return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 PKCS#8 格式的私钥字符串并加载为私钥对象
|
||||
*
|
||||
* @param keyString 私钥文件内容,以 -----BEGIN PRIVATE KEY----- 开头
|
||||
* @return PrivateKey 对象
|
||||
*/
|
||||
public static PrivateKey loadPrivateKeyFromString(String keyString) {
|
||||
try {
|
||||
keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return KeyFactory.getInstance("RSA").generatePrivate(
|
||||
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PKCS#8 格式的私钥文件中加载私钥
|
||||
*
|
||||
* @param keyPath 私钥文件路径
|
||||
* @return PrivateKey 对象
|
||||
*/
|
||||
public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
|
||||
return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 PKCS#8 格式的公钥字符串并加载为公钥对象
|
||||
*
|
||||
* @param keyString 公钥文件内容,以 -----BEGIN PUBLIC KEY----- 开头
|
||||
* @return PublicKey 对象
|
||||
*/
|
||||
public static PublicKey loadPublicKeyFromString(String keyString) {
|
||||
try {
|
||||
keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return KeyFactory.getInstance("RSA").generatePublic(
|
||||
new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 PKCS#8 格式的公钥文件中加载公钥
|
||||
*
|
||||
* @param keyPath 公钥文件路径
|
||||
* @return PublicKey 对象
|
||||
*/
|
||||
public static PublicKey loadPublicKeyFromPath(String keyPath) {
|
||||
return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建指定长度的随机字符串,字符集为[0-9a-zA-Z],可用于安全相关用途
|
||||
*/
|
||||
public static String createNonce(int length) {
|
||||
char[] buf = new char[length];
|
||||
for (int i = 0; i < length; ++i) {
|
||||
buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
|
||||
}
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥按照 RSA_PKCS1_OAEP_PADDING 算法进行加密
|
||||
*
|
||||
* @param publicKey 加密用公钥对象
|
||||
* @param plaintext 待加密明文
|
||||
* @return 加密后密文
|
||||
*/
|
||||
public static String encrypt(PublicKey publicKey, String plaintext) {
|
||||
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException("Plaintext is too long", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用私钥按照指定算法进行签名
|
||||
*
|
||||
* @param message 待签名串
|
||||
* @param algorithm 签名算法,如 SHA256withRSA
|
||||
* @param privateKey 签名用私钥对象
|
||||
* @return 签名结果
|
||||
*/
|
||||
public static String sign(String message, String algorithm, PrivateKey privateKey) {
|
||||
byte[] sign;
|
||||
try {
|
||||
Signature signature = Signature.getInstance(algorithm);
|
||||
signature.initSign(privateKey);
|
||||
signature.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
sign = signature.sign();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException("An error occurred during the sign process.", e);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(sign);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥按照特定算法验证签名
|
||||
*
|
||||
* @param message 待签名串
|
||||
* @param signature 待验证的签名内容
|
||||
* @param algorithm 签名算法,如:SHA256withRSA
|
||||
* @param publicKey 验签用公钥对象
|
||||
* @return 签名验证是否通过
|
||||
*/
|
||||
public static boolean verify(String message, String signature, String algorithm,
|
||||
PublicKey publicKey) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance(algorithm);
|
||||
sign.initVerify(publicKey);
|
||||
sign.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
return sign.verify(Base64.getDecoder().decode(signature));
|
||||
} catch (SignatureException e) {
|
||||
return false;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("verify uses an illegal publickey.", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信支付APIv3请求签名规则构造 Authorization 签名
|
||||
*
|
||||
* @param mchid 商户号
|
||||
* @param certificateSerialNo 商户API证书序列号
|
||||
* @param privateKey 商户API证书私钥
|
||||
* @param method 请求接口的HTTP方法,请使用全大写表述,如 GET、POST、PUT、DELETE
|
||||
* @param uri 请求接口的URL
|
||||
* @param body 请求接口的Body
|
||||
* @return 构造好的微信支付APIv3 Authorization 头
|
||||
*/
|
||||
public static String buildAuthorization(String mchid, String certificateSerialNo,
|
||||
PrivateKey privateKey,
|
||||
String method, String uri, String body) {
|
||||
String nonce = createNonce(32);
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
|
||||
String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
|
||||
body == null ? "" : body);
|
||||
|
||||
String signature = sign(message, "SHA256withRSA", privateKey);
|
||||
|
||||
return String.format(
|
||||
"WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
|
||||
"timestamp=\"%d\",serial_no=\"%s\"",
|
||||
mchid, nonce, signature, timestamp, certificateSerialNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对参数进行 URL 编码
|
||||
*
|
||||
* @param content 参数内容
|
||||
* @return 编码后的内容
|
||||
*/
|
||||
public static String urlEncode(String content) {
|
||||
try {
|
||||
return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对参数Map进行 URL 编码,生成 QueryString
|
||||
*
|
||||
* @param params Query参数Map
|
||||
* @return QueryString
|
||||
*/
|
||||
public static String urlEncode(Map<String, Object> params) {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
int index = 0;
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
result.append(entry.getKey())
|
||||
.append("=")
|
||||
.append(urlEncode(entry.getValue().toString()));
|
||||
index++;
|
||||
if (index < params.size()) {
|
||||
result.append("&");
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从应答中提取 Body
|
||||
*
|
||||
* @param response HTTP 请求应答对象
|
||||
* @return 应答中的Body内容,Body为空时返回空字符串
|
||||
*/
|
||||
public static String extractBody(Response response) {
|
||||
if (response.body() == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedSource source = response.body().source();
|
||||
return source.readUtf8();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据微信支付APIv3应答验签规则对应答签名进行验证,验证不通过时抛出异常
|
||||
*
|
||||
* @param wechatpayPublicKeyId 微信支付公钥ID
|
||||
* @param wechatpayPublicKey 微信支付公钥对象
|
||||
* @param headers 微信支付应答 Header 列表
|
||||
* @param body 微信支付应答 Body
|
||||
*/
|
||||
public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
|
||||
Headers headers,
|
||||
String body) {
|
||||
String timestamp = headers.get("Wechatpay-Timestamp");
|
||||
try {
|
||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
|
||||
// 拒绝过期请求
|
||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate http response,timestamp[%s] of httpResponse is expires, "
|
||||
+ "request-id[%s]",
|
||||
timestamp, headers.get("Request-ID")));
|
||||
}
|
||||
} catch (DateTimeException | NumberFormatException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " +
|
||||
"request-id[%s]", timestamp,
|
||||
headers.get("Request-ID")));
|
||||
}
|
||||
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
|
||||
body == null ? "" : body);
|
||||
String serialNumber = headers.get("Wechatpay-Serial");
|
||||
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId,
|
||||
serialNumber));
|
||||
}
|
||||
String signature = headers.get("Wechatpay-Signature");
|
||||
|
||||
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
|
||||
if (!success) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed,the WechatPay signature is incorrect.%n"
|
||||
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
|
||||
headers.get("Request-ID"), headers, body));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信支付API错误异常,发送HTTP请求成功,但返回状态码不是 2XX 时抛出本异常
|
||||
*/
|
||||
public static class ApiException extends RuntimeException {
|
||||
private static final long serialVersionUID = 2261086748874802175L;
|
||||
|
||||
private final int statusCode;
|
||||
private final String body;
|
||||
private final Headers headers;
|
||||
private final String errorCode;
|
||||
private final String errorMessage;
|
||||
|
||||
public ApiException(int statusCode, String body, Headers headers) {
|
||||
super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers));
|
||||
this.statusCode = statusCode;
|
||||
this.body = body;
|
||||
this.headers = headers;
|
||||
|
||||
if (body != null && !body.isEmpty()) {
|
||||
JsonElement code;
|
||||
JsonElement message;
|
||||
|
||||
/* 原有密钥加载方法保持不变但标记为过时 */
|
||||
@Deprecated
|
||||
private static String readKeyStringFromPath(String keyPath) {
|
||||
try {
|
||||
JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class);
|
||||
code = jsonObject.get("code");
|
||||
message = jsonObject.get("message");
|
||||
} catch (JsonSyntaxException ignored) {
|
||||
code = null;
|
||||
message = null;
|
||||
return new String(Files.readAllBytes(Path.of(keyPath)), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
this.errorCode = code == null ? null : code.getAsString();
|
||||
this.errorMessage = message == null ? null : message.getAsString();
|
||||
} else {
|
||||
this.errorCode = null;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 应答状态码
|
||||
*/
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
/* 密钥解析方法 */
|
||||
private static PrivateKey parsePrivateKey(String keyContent) {
|
||||
try {
|
||||
byte[] decoded = Base64.getDecoder().decode(keyContent);
|
||||
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(decoded);
|
||||
return KeyFactory.getInstance("RSA").generatePrivate(keySpec);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 应答包体内容
|
||||
*/
|
||||
public String getBody() {
|
||||
return body;
|
||||
/* 原有公钥加载方法 */
|
||||
public static PublicKey loadPublicKeyFromString(String keyString) {
|
||||
try {
|
||||
keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return KeyFactory.getInstance("RSA").generatePublic(
|
||||
new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 HTTP 应答 Header
|
||||
*/
|
||||
public Headers getHeaders() {
|
||||
return headers;
|
||||
public static PublicKey loadPublicKeyFromPath(String keyPath) {
|
||||
return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 错误码 (错误应答中的 code 字段)
|
||||
*/
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
/* 其他原有方法保持不变 */
|
||||
public static String createNonce(int length) {
|
||||
char[] buf = new char[length];
|
||||
for (int i = 0; i < length; ++i) {
|
||||
buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
|
||||
}
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 错误消息 (错误应答中的 message 字段)
|
||||
*/
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
public static String encrypt(PublicKey publicKey, String plaintext) {
|
||||
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException("Plaintext is too long", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String sign(String message, String algorithm, PrivateKey privateKey) {
|
||||
byte[] sign;
|
||||
try {
|
||||
Signature signature = Signature.getInstance(algorithm);
|
||||
signature.initSign(privateKey);
|
||||
signature.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
sign = signature.sign();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException("An error occurred during the sign process.", e);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(sign);
|
||||
}
|
||||
|
||||
public static boolean verify(String message, String signature, String algorithm,
|
||||
PublicKey publicKey) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance(algorithm);
|
||||
sign.initVerify(publicKey);
|
||||
sign.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
return sign.verify(Base64.getDecoder().decode(signature));
|
||||
} catch (SignatureException e) {
|
||||
return false;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("verify uses an illegal publickey.", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String buildAuthorization(String mchid, String certificateSerialNo,
|
||||
PrivateKey privateKey,
|
||||
String method, String uri, String body) {
|
||||
String nonce = createNonce(32);
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
|
||||
body == null ? "" : body);
|
||||
String signature = sign(message, "SHA256withRSA", privateKey);
|
||||
return String.format(
|
||||
"WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
|
||||
"timestamp=\"%d\",serial_no=\"%s\"",
|
||||
mchid, nonce, signature, timestamp, certificateSerialNo);
|
||||
}
|
||||
|
||||
public static String urlEncode(String content) {
|
||||
try {
|
||||
return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String urlEncode(Map<String, Object> params) {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
int index = 0;
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, Object> entry : params.entrySet()) {
|
||||
result.append(entry.getKey())
|
||||
.append("=")
|
||||
.append(urlEncode(entry.getValue().toString()));
|
||||
index++;
|
||||
if (index < params.size()) {
|
||||
result.append("&");
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
public static String extractBody(Response response) {
|
||||
if (response.body() == null) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
BufferedSource source = response.body().source();
|
||||
return source.readUtf8();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(String.format("An error occurred during reading response body. Status: %d", response.code()), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
|
||||
Headers headers,
|
||||
String body) {
|
||||
String timestamp = headers.get("Wechatpay-Timestamp");
|
||||
try {
|
||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
|
||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate http response,timestamp[%s] of httpResponse is expires, "
|
||||
+ "request-id[%s]",
|
||||
timestamp, headers.get("Request-ID")));
|
||||
}
|
||||
} catch (DateTimeException | NumberFormatException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate http response,timestamp[%s] of httpResponse is invalid, " +
|
||||
"request-id[%s]", timestamp,
|
||||
headers.get("Request-ID")));
|
||||
}
|
||||
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
|
||||
body == null ? "" : body);
|
||||
String serialNumber = headers.get("Wechatpay-Serial");
|
||||
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Invalid Wechatpay-Serial, Local: %s, Remote: %s", wechatpayPublicKeyId,
|
||||
serialNumber));
|
||||
}
|
||||
String signature = headers.get("Wechatpay-Signature");
|
||||
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
|
||||
if (!success) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed,the WechatPay signature is incorrect.%n"
|
||||
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
|
||||
headers.get("Request-ID"), headers, body));
|
||||
}
|
||||
}
|
||||
|
||||
public static class ApiException extends RuntimeException {
|
||||
private static final long serialVersionUID = 2261086748874802175L;
|
||||
|
||||
private final int statusCode;
|
||||
private final String body;
|
||||
private final Headers headers;
|
||||
private final String errorCode;
|
||||
private final String errorMessage;
|
||||
|
||||
public ApiException(int statusCode, String body, Headers headers) {
|
||||
super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode, body, headers));
|
||||
this.statusCode = statusCode;
|
||||
this.body = body;
|
||||
this.headers = headers;
|
||||
|
||||
if (body != null && !body.isEmpty()) {
|
||||
JsonElement code;
|
||||
JsonElement message;
|
||||
try {
|
||||
JsonObject jsonObject = GsonUtil.getGson().fromJson(body, JsonObject.class);
|
||||
code = jsonObject.get("code");
|
||||
message = jsonObject.get("message");
|
||||
} catch (JsonSyntaxException ignored) {
|
||||
code = null;
|
||||
message = null;
|
||||
}
|
||||
this.errorCode = code == null ? null : code.getAsString();
|
||||
this.errorMessage = message == null ? null : message.getAsString();
|
||||
} else {
|
||||
this.errorCode = null;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public Headers getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,14 @@
|
||||
package com.wzj.soopin.transaction.wechat;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.PrivateKey;
|
||||
|
||||
@Data
|
||||
@ -15,13 +19,27 @@ public class WechatPayConfig {
|
||||
private String mchId;
|
||||
private String mchSerialNo;
|
||||
private String apiV3Key;
|
||||
private String privateKeyPath;
|
||||
private String privateKeyPath; // 支持 classpath: 或 file: 前缀
|
||||
private String transferNotifyUrl;
|
||||
private String appId;
|
||||
private String secret;
|
||||
|
||||
@Bean
|
||||
public PrivateKey merchantPrivateKey() {
|
||||
return WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
|
||||
public PrivateKey merchantPrivateKey() throws IOException {
|
||||
if (privateKeyPath.startsWith("classpath:")) {
|
||||
// 处理类路径资源
|
||||
String path = privateKeyPath.substring("classpath:".length());
|
||||
Resource resource = new ClassPathResource(path);
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
return WXPayUtility.loadPrivateKey(inputStream);
|
||||
}
|
||||
} else if (privateKeyPath.startsWith("file:")) {
|
||||
// 处理文件系统路径
|
||||
String filePath = privateKeyPath.substring("file:".length());
|
||||
return WXPayUtility.loadPrivateKeyFromPath(filePath);
|
||||
} else {
|
||||
// 默认按文件系统路径处理
|
||||
return WXPayUtility.loadPrivateKeyFromPath(privateKeyPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user