From d6fc03b199f6ab58a259db4e9fdbcfb1a9386807 Mon Sep 17 00:00:00 2001 From: FCL Date: Fri, 28 Nov 2025 17:07:40 +0800 Subject: [PATCH] =?UTF-8?q?fix:=E8=87=AA=E5=8A=A8=E7=81=AB=E8=AF=95?= =?UTF-8?q?=E9=87=91=E6=8E=A5=E5=8F=A3=E7=AD=BE=E5=90=8D=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../plat/module/qms/CryptoSignatureUtils.java | 213 ++++++++++++++++++ .../qms/DatabusApiInvocationExample.java | 19 +- 2 files changed, 216 insertions(+), 16 deletions(-) create mode 100644 zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/CryptoSignatureUtils.java diff --git a/zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/CryptoSignatureUtils.java b/zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/CryptoSignatureUtils.java new file mode 100644 index 0000000..5d44521 --- /dev/null +++ b/zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/CryptoSignatureUtils.java @@ -0,0 +1,213 @@ +package com.zt.plat.module.qms; + +import cn.hutool.crypto.SecureUtil; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.*; + +/** + * 通用的签名、加解密工具类 + */ +public final class CryptoSignatureUtils { + + public static final String ENCRYPT_TYPE_AES = "AES"; + public static final String ENCRYPT_TYPE_DES = "DES"; + public static final String SIGNATURE_TYPE_MD5 = "MD5"; + public static final String SIGNATURE_TYPE_SHA256 = "SHA256"; + + private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding"; + public static final String SIGNATURE_FIELD = "signature"; + + private CryptoSignatureUtils() { + } + + /** + * 生成 AES 密钥(SecretKeySpec) + * + * @param password 密钥字符串 + * @return SecretKeySpec + */ + public static SecretKeySpec getSecretKey(String password) { + try { + KeyGenerator kg = KeyGenerator.getInstance("AES"); + SecureRandom random = SecureRandom.getInstance("SHA1PRNG"); + random.setSeed(password.getBytes(StandardCharsets.UTF_8)); + kg.init(128, random); + SecretKey secretKey = kg.generateKey(); + return new SecretKeySpec(secretKey.getEncoded(), "AES"); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Failed to generate AES secret key", ex); + } + } + + /** + * 对称加密(Base64 格式输出) + * + * @param plaintext 明文内容 + * @param key 密钥 + * @param type 加密类型,支持 AES、DES + * @return 密文(Base64 格式) + */ + public static String encrypt(String plaintext, String key, String type) { + if (ENCRYPT_TYPE_AES.equalsIgnoreCase(type)) { + try { + Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION); + cipher.init(Cipher.ENCRYPT_MODE, getSecretKey(key)); + byte[] result = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(result); + } catch (Exception ex) { + throw new IllegalStateException("Failed to encrypt using AES", ex); + } + } else if (ENCRYPT_TYPE_DES.equalsIgnoreCase(type)) { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + byte[] desKey = new byte[8]; + System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length)); + byte[] encrypted = SecureUtil.des(desKey).encrypt(plaintext.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(encrypted); + } else { + throw new IllegalArgumentException("Unsupported encryption type: " + type); + } + } + + /** + * 对称解密(输入为 Base64 格式密文) + * + * @param ciphertext 密文内容(Base64 格式) + * @param key 密钥 + * @param type 加密类型,支持 AES、DES + * @return 明文内容 + */ + public static String decrypt(String ciphertext, String key, String type) { + if (ciphertext == null) { + return null; + } + if (ENCRYPT_TYPE_AES.equalsIgnoreCase(type)) { + try { + Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION); + cipher.init(Cipher.DECRYPT_MODE, getSecretKey(key)); + byte[] decoded = decodeBase64Ciphertext(ciphertext); + byte[] result = cipher.doFinal(decoded); + return new String(result, StandardCharsets.UTF_8); + } catch (Exception ex) { + throw new IllegalStateException("Failed to decrypt using AES", ex); + } + } else if (ENCRYPT_TYPE_DES.equalsIgnoreCase(type)) { + byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); + byte[] desKey = new byte[8]; + System.arraycopy(keyBytes, 0, desKey, 0, Math.min(keyBytes.length, desKey.length)); + byte[] decoded = decodeBase64Ciphertext(ciphertext); + byte[] decrypted = SecureUtil.des(desKey).decrypt(decoded); + return new String(decrypted, StandardCharsets.UTF_8); + } else { + throw new IllegalArgumentException("Unsupported encryption type: " + type); + } + } + + /** + * 验证请求签名 + * + * @param reqMap 请求参数 Map + * @param type 签名算法类型,支持 MD5、SHA256 + * @return 签名是否有效 + */ + public static boolean verifySignature(Map reqMap, String type) { + Map sortedMap = new TreeMap<>(reqMap); + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : sortedMap.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + if (SIGNATURE_FIELD.equals(key) || value == null) { + continue; + } + sb.append(key).append('='); + sb.append(value); + sb.append('&'); + } + if (sb.length() > 0) { + sb.deleteCharAt(sb.length() - 1); + } + String provided = (String) reqMap.get(SIGNATURE_FIELD); + if (provided == null) { + return false; + } + String computed; + if (SIGNATURE_TYPE_MD5.equalsIgnoreCase(type)) { + computed = SecureUtil.md5(sb.toString()); + } else if (SIGNATURE_TYPE_SHA256.equalsIgnoreCase(type)) { + computed = SecureUtil.sha256(sb.toString()); + } else { + throw new IllegalArgumentException("Unsupported signature type: " + type); + } + return provided.equalsIgnoreCase(computed); + } + + private static byte[] decodeBase64Ciphertext(String ciphertext) { + IllegalArgumentException last = null; + for (String candidate : buildBase64Candidates(ciphertext)) { + if (candidate == null || candidate.isEmpty()) { + continue; + } + try { + return Base64.getDecoder().decode(candidate); + } catch (IllegalArgumentException ex) { + last = ex; + } + } + throw last != null ? last : new IllegalArgumentException("Invalid Base64 content"); + } + + private static Set buildBase64Candidates(String ciphertext) { + Set candidates = new LinkedHashSet<>(); + if (ciphertext == null) { + return candidates; + } + String trimmed = ciphertext.trim(); + candidates.add(trimmed); + + String withoutWhitespace = stripWhitespace(trimmed); + candidates.add(withoutWhitespace); + + if (trimmed.indexOf(' ') >= 0) { + String restoredPlus = trimmed.replace(' ', '+'); + candidates.add(restoredPlus); + candidates.add(stripWhitespace(restoredPlus)); + } + + String urlNormalised = withoutWhitespace + .replace('-', '+') + .replace('_', '/'); + candidates.add(urlNormalised); + + return candidates; + } + + private static String stripWhitespace(String value) { + if (value == null) { + return null; + } + boolean hasWhitespace = false; + for (int i = 0; i < value.length(); i++) { + if (Character.isWhitespace(value.charAt(i))) { + hasWhitespace = true; + break; + } + } + if (!hasWhitespace) { + return value; + } + StringBuilder sb = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + char ch = value.charAt(i); + if (!Character.isWhitespace(ch)) { + sb.append(ch); + } + } + return sb.toString(); + } +} diff --git a/zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/DatabusApiInvocationExample.java b/zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/DatabusApiInvocationExample.java index 47990dd..35bd96a 100644 --- a/zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/DatabusApiInvocationExample.java +++ b/zt-module-qms/zt-module-qms-server/src/test/java/com/zt/plat/module/qms/DatabusApiInvocationExample.java @@ -4,10 +4,6 @@ import com.alibaba.fastjson.JSONObject; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import com.zt.plat.framework.common.util.security.CryptoSignatureUtils; -import org.springframework.util.StringUtils; - -import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.net.URI; @@ -17,9 +13,6 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.Duration; @@ -31,9 +24,7 @@ public class DatabusApiInvocationExample { private static final String APP_SECRET = "tjDKCUGNEDR9yNgbxIsvtXsRMuQK+tj1HNEMpgjJOPU="; private static final String ENCRYPTION_TYPE = CryptoSignatureUtils.ENCRYPT_TYPE_AES; private static final String TARGET_API = "http://172.16.46.62:30081/admin-api/databus/api/portal"; - private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder() - .connectTimeout(Duration.ofSeconds(5)) - .build(); + private static final HttpClient HTTP_CLIENT = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(5)).build(); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); private static final PrintStream OUT = buildConsolePrintStream(); public static final String ZT_APP_ID = "ZT-App-Id"; @@ -47,10 +38,10 @@ public class DatabusApiInvocationExample { public static void main(String[] args) throws Exception { OUT.println("=== GET 请求示例 ==="); - executeGetExample(); +// executeGetExample(); // OUT.println(); // OUT.println("=== POST 请求示例 ==="); -// executePostExample(); + executePostExample(); } private static void executeGetExample() throws Exception { @@ -72,16 +63,12 @@ public class DatabusApiInvocationExample { // .header("ZT-Auth-Token", "a75c0ea94c7f4a88b86b60bbc0b432c3") .GET() .build(); - HttpResponse response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); printResponse(response); } private static void executePostExample() throws Exception { Map queryParams = new LinkedHashMap<>(); - - - String jsonStr = "{\n" + " \"taskId\": \"1994323064365080578\",\n" + " \"sampleList\": [\n" +