From bbd9c436f23f5fdbe712c4a22d90b457066bdf38 Mon Sep 17 00:00:00 2001
From: k94314517 <8417338+k94314517@user.noreply.gitee.com>
Date: 星期四, 17 七月 2025 19:25:47 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master'
---
server/services/src/main/java/com/doumee/config/wx/WXPayUtility.java | 456 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 456 insertions(+), 0 deletions(-)
diff --git a/server/services/src/main/java/com/doumee/config/wx/WXPayUtility.java b/server/services/src/main/java/com/doumee/config/wx/WXPayUtility.java
new file mode 100644
index 0000000..bf29e52
--- /dev/null
+++ b/server/services/src/main/java/com/doumee/config/wx/WXPayUtility.java
@@ -0,0 +1,456 @@
+package com.doumee.config.wx;
+
+
+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.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 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.time.Instant;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Objects;
+
+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();
+ }
+
+ @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();
+
+ /**
+ * 灏� 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 绛惧悕绠楁硶锛屽锛歋HA256withRSA
+ * @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 璇锋眰鎺ュ彛鐨凥TTP鏂规硶锛岃浣跨敤鍏ㄥぇ鍐欒〃杩帮紝濡� GET銆丳OST銆丳UT銆丏ELETE
+ * @param uri 璇锋眰鎺ュ彛鐨刄RL
+ * @param body 璇锋眰鎺ュ彛鐨凚ody
+ * @return 鏋勯�犲ソ鐨勫井淇℃敮浠楢PIv3 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);
+ }
+ }
+
+ /**
+ * 瀵瑰弬鏁癕ap杩涜 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鍐呭锛孊ody涓虹┖鏃惰繑鍥炵┖瀛楃涓�
+ */
+ 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閿欒寮傚父锛屽彂閫丠TTP璇锋眰鎴愬姛锛屼絾杩斿洖鐘舵�佺爜涓嶆槸 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璁块棶澶辫触锛孲tatusCode: [%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;
+ }
+ }
+
+ /**
+ * 鑾峰彇 HTTP 搴旂瓟鐘舵�佺爜
+ */
+ public int getStatusCode() {
+ return statusCode;
+ }
+
+ /**
+ * 鑾峰彇 HTTP 搴旂瓟鍖呬綋鍐呭
+ */
+ public String getBody() {
+ return body;
+ }
+
+ /**
+ * 鑾峰彇 HTTP 搴旂瓟 Header
+ */
+ public Headers getHeaders() {
+ return headers;
+ }
+
+ /**
+ * 鑾峰彇 閿欒鐮� 锛堥敊璇簲绛斾腑鐨� code 瀛楁锛�
+ */
+ public String getErrorCode() {
+ return errorCode;
+ }
+
+ /**
+ * 鑾峰彇 閿欒娑堟伅 锛堥敊璇簲绛斾腑鐨� message 瀛楁锛�
+ */
+ public String getErrorMessage() {
+ return errorMessage;
+ }
+ }
+}
\ No newline at end of file
--
Gitblit v1.9.3