11 changed files with 513 additions and 16 deletions
@ -0,0 +1,167 @@ |
|||||
|
package com.lnsoft.sdhxgk.point.inspect.utils; |
||||
|
|
||||
|
|
||||
|
//import com.alibaba.fastjson2.JSON;
|
||||
|
//import com.alibaba.fastjson2.JSONObject;
|
||||
|
|
||||
|
import com.alibaba.fastjson.JSON; |
||||
|
import com.alibaba.fastjson.JSONObject; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.apache.http.HttpResponse; |
||||
|
import org.apache.http.client.config.RequestConfig; |
||||
|
import org.apache.http.client.methods.HttpPost; |
||||
|
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.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
@Slf4j |
||||
|
@Component |
||||
|
public class GoApiClient { |
||||
|
|
||||
|
private static final int TIMEOUT = 100000; |
||||
|
|
||||
|
/** |
||||
|
* 调用 Go 接口(使用加密通信) |
||||
|
* @param url Go接口地址 |
||||
|
* @param param 请求参数 |
||||
|
* @param userKey User-Key |
||||
|
* @param userToken User-Token |
||||
|
* @return 解密后的响应字符串 |
||||
|
* @throws IOException |
||||
|
*/ |
||||
|
public String callGoApi(String url, Object param, String userKey, String userToken) throws IOException { |
||||
|
// 1. 生成随机 Auth-key(16位)
|
||||
|
String authKey = RandomStringUtil.generateRandomString(16); |
||||
|
|
||||
|
// 2. 加密请求数据
|
||||
|
String encryptedData; |
||||
|
try { |
||||
|
String jsonParam = JSON.toJSONString(param); |
||||
|
encryptedData = SM4Utils.encrypt(jsonParam, authKey); |
||||
|
} catch (Exception e) { |
||||
|
log.error("加密请求数据失败", e); |
||||
|
throw new IOException("加密请求数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
|
||||
|
// 3. 构建加密请求体
|
||||
|
Map<String, String> encryptedBody = new HashMap<>(); |
||||
|
encryptedBody.put("data", encryptedData); // Go后端期望data字段
|
||||
|
String requestBody = JSON.toJSONString(encryptedBody); |
||||
|
|
||||
|
// 4. 发送 HTTP 请求
|
||||
|
CloseableHttpClient httpClient = null; |
||||
|
try { |
||||
|
RequestConfig requestConfig = RequestConfig.custom() |
||||
|
.setConnectTimeout(TIMEOUT) |
||||
|
.setSocketTimeout(TIMEOUT) |
||||
|
.setConnectionRequestTimeout(TIMEOUT) |
||||
|
.build(); |
||||
|
|
||||
|
httpClient = HttpClients.custom() |
||||
|
.setDefaultRequestConfig(requestConfig) |
||||
|
.build(); |
||||
|
|
||||
|
HttpPost httpPost = new HttpPost(url); |
||||
|
|
||||
|
// 设置请求头
|
||||
|
// httpPost.setHeader("User-Key", userKey);
|
||||
|
// httpPost.setHeader("User-Token", userToken);
|
||||
|
httpPost.setHeader("Origin", "http://120.224.6.6"); |
||||
|
httpPost.setHeader("number", userKey); |
||||
|
httpPost.setHeader("token", userToken); |
||||
|
httpPost.setHeader("Auth-key", authKey); |
||||
|
httpPost.setHeader("Content-Type", "application/json"); |
||||
|
httpPost.setHeader("User-Agent", "PostmanRuntime/7.26.8"); |
||||
|
|
||||
|
// 设置请求体
|
||||
|
StringEntity entity = new StringEntity(requestBody, StandardCharsets.UTF_8); |
||||
|
httpPost.setEntity(entity); |
||||
|
|
||||
|
// 执行请求
|
||||
|
HttpResponse response = httpClient.execute(httpPost); |
||||
|
int statusCode = response.getStatusLine().getStatusCode(); |
||||
|
|
||||
|
if (statusCode != 200) { |
||||
|
String errorResponse = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); |
||||
|
throw new IOException("HTTP 请求失败,状态码: " + statusCode + ", 响应: " + errorResponse); |
||||
|
} |
||||
|
|
||||
|
// 获取响应体
|
||||
|
String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); |
||||
|
|
||||
|
// 获取响应头中的 Auth-key
|
||||
|
org.apache.http.Header[] authKeyHeaders = response.getHeaders("Auth-key"); |
||||
|
String responseAuthKey = null; |
||||
|
if (authKeyHeaders != null && authKeyHeaders.length > 0) { |
||||
|
responseAuthKey = authKeyHeaders[0].getValue(); |
||||
|
} |
||||
|
|
||||
|
// 5. 解密响应数据
|
||||
|
if (responseAuthKey != null && !responseAuthKey.isEmpty()) { |
||||
|
return decryptGoResponse(responseBody, responseAuthKey); |
||||
|
} else { |
||||
|
log.warn("响应头中没有找到 Auth-key,返回原始响应"); |
||||
|
return responseBody; |
||||
|
} |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("HTTP 请求异常", e); |
||||
|
throw new IOException("HTTP 请求异常: " + e.getMessage(), e); |
||||
|
} finally { |
||||
|
if (httpClient != null) { |
||||
|
try { |
||||
|
httpClient.close(); |
||||
|
} catch (IOException e) { |
||||
|
log.error("关闭 HTTP 客户端失败", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解密 Go 后端的响应 |
||||
|
*/ |
||||
|
private String decryptGoResponse(String responseBody, String authKey) throws Exception { |
||||
|
try { |
||||
|
// 解析 Go 的响应
|
||||
|
JSONObject responseJson = JSON.parseObject(responseBody); |
||||
|
int code = responseJson.getIntValue("code"); |
||||
|
String msg = responseJson.getString("msg"); |
||||
|
String encryptedData = responseJson.getString("data"); |
||||
|
|
||||
|
if (encryptedData != null && !encryptedData.isEmpty()) { |
||||
|
// 解密数据
|
||||
|
String decryptedData = SM4Utils.decrypt(encryptedData, authKey); |
||||
|
|
||||
|
// 重新构建响应
|
||||
|
JSONObject result = new JSONObject(); |
||||
|
result.put("code", code); |
||||
|
result.put("msg", msg); |
||||
|
|
||||
|
try { |
||||
|
// 尝试将解密后的字符串解析为 JSON 对象
|
||||
|
Object dataObj = JSON.parse(decryptedData); |
||||
|
result.put("data", dataObj); |
||||
|
} catch (Exception e) { |
||||
|
// 如果解析失败,直接使用字符串
|
||||
|
result.put("data", decryptedData); |
||||
|
} |
||||
|
|
||||
|
return result.toJSONString(); |
||||
|
} else { |
||||
|
return responseBody; |
||||
|
} |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("解密 Go 响应数据失败", e); |
||||
|
throw new Exception("解密 Go 响应数据失败: " + e.getMessage(), e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,49 @@ |
|||||
|
package com.lnsoft.sdhxgk.point.inspect.utils; |
||||
|
|
||||
|
|
||||
|
import org.apache.commons.codec.binary.Hex; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.security.SecureRandom; |
||||
|
import java.util.Random; |
||||
|
|
||||
|
@Component |
||||
|
public class RandomStringUtil { |
||||
|
|
||||
|
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; |
||||
|
private static final SecureRandom SECURE_RANDOM = new SecureRandom(); |
||||
|
|
||||
|
/** |
||||
|
* 生成指定长度的随机字符串 |
||||
|
*/ |
||||
|
public static String generateRandomString(int length) { |
||||
|
StringBuilder sb = new StringBuilder(length); |
||||
|
for (int i = 0; i < length; i++) { |
||||
|
int index = SECURE_RANDOM.nextInt(CHARACTERS.length()); |
||||
|
sb.append(CHARACTERS.charAt(index)); |
||||
|
} |
||||
|
return sb.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 获取UUID |
||||
|
*/ |
||||
|
public static String generateUUID() { |
||||
|
byte[] randomBytes = new byte[16]; |
||||
|
SECURE_RANDOM.nextBytes(randomBytes); |
||||
|
randomBytes[6] &= 0x0f; /* clear version */ |
||||
|
randomBytes[6] |= 0x40; /* set to version 4 */ |
||||
|
randomBytes[8] &= 0x3f; /* clear variant */ |
||||
|
randomBytes[8] |= 0x80; /* set to IETF variant */ |
||||
|
|
||||
|
char[] uuidChars = Hex.encodeHex(randomBytes, false); |
||||
|
StringBuilder uuid = new StringBuilder(36); |
||||
|
for (int i = 0; i < uuidChars.length; i++) { |
||||
|
if (i == 8 || i == 12 || i == 16 || i == 20) { |
||||
|
uuid.append('-'); |
||||
|
} |
||||
|
uuid.append(uuidChars[i]); |
||||
|
} |
||||
|
return uuid.toString(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,216 @@ |
|||||
|
package com.lnsoft.sdhxgk.point.inspect.utils; |
||||
|
|
||||
|
import org.bouncycastle.crypto.engines.SM4Engine; |
||||
|
import org.bouncycastle.crypto.modes.CBCBlockCipher; |
||||
|
import org.bouncycastle.crypto.paddings.PKCS7Padding; |
||||
|
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; |
||||
|
import org.bouncycastle.crypto.params.KeyParameter; |
||||
|
import org.bouncycastle.crypto.params.ParametersWithIV; |
||||
|
import org.springframework.beans.factory.annotation.Value; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.util.Base64; |
||||
|
|
||||
|
@Component |
||||
|
public class SM4Utils { |
||||
|
|
||||
|
// Go 后端的 Sm4Key - 用作加密密钥
|
||||
|
@Value("${sm4.key:hengxingaokeApp1}") |
||||
|
private String sm4Key; |
||||
|
|
||||
|
// Go 后端的 Sm4Token - 默认用作 IV
|
||||
|
@Value("${sm4.token:04TzMuvkHm_EZnHm}") |
||||
|
private String sm4Token; |
||||
|
|
||||
|
// 前端使用的 systemKey(仅用于与前端对比验证)
|
||||
|
@Value("${sm4.system-key:appKeyQinDongV01}") |
||||
|
private String systemKey; |
||||
|
|
||||
|
private static String SM4_KEY_PROCESSED; |
||||
|
private static String SM4_TOKEN_PROCESSED; |
||||
|
private static String SYSTEM_KEY_HEX; |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void init() { |
||||
|
// Go 的方式:处理为16字节(不是转16进制!)
|
||||
|
SM4_KEY_PROCESSED = processString(sm4Key, 16, "s"); |
||||
|
SM4_TOKEN_PROCESSED = processString(sm4Token, 16, "s"); |
||||
|
|
||||
|
// 前端的 systemKey 转16进制(仅用于验证)
|
||||
|
SYSTEM_KEY_HEX = stringToHex(systemKey); |
||||
|
|
||||
|
System.out.println("=== SM4Utils 初始化 ==="); |
||||
|
System.out.println("Go 的 Sm4Key: " + sm4Key); |
||||
|
System.out.println("Go 的 Sm4Key(处理后16字节): " + SM4_KEY_PROCESSED); |
||||
|
System.out.println("Go 的 Sm4Token: " + sm4Token); |
||||
|
System.out.println("Go 的 Sm4Token(处理后16字节): " + SM4_TOKEN_PROCESSED); |
||||
|
System.out.println("前端的 VITE_APP_SYSTEM_KEY: " + systemKey); |
||||
|
System.out.println("前端的 systemKey(hex): " + SYSTEM_KEY_HEX); |
||||
|
System.out.println("注意:Java 应该使用 Go 的 Sm4Key,而不是前端的 systemKey!"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 处理字符串为指定长度(与 Go 的 ProcessString 完全一致) |
||||
|
* 超过截断,不足用 pad 补全 |
||||
|
*/ |
||||
|
private static String processString(String s, int length, String pad) { |
||||
|
// 如果字符串长度已经符合要求,直接返回
|
||||
|
if (s.length() == length) { |
||||
|
return s; |
||||
|
} |
||||
|
// 如果字符串长度超过了要求,截取指定长度的部分
|
||||
|
if (s.length() > length) { |
||||
|
return s.substring(0, length); |
||||
|
} |
||||
|
// 如果字符串长度不足,使用pad字符串进行补充
|
||||
|
StringBuilder sb = new StringBuilder(s); |
||||
|
while (sb.length() < length) { |
||||
|
sb.append(pad); |
||||
|
} |
||||
|
return sb.substring(0, length); // 确保不会超过长度
|
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 字符串转16进制(仅用于与前端对比) |
||||
|
*/ |
||||
|
private static String stringToHex(String str) { |
||||
|
byte[] bytes = str.getBytes(StandardCharsets.UTF_8); |
||||
|
StringBuilder hex = new StringBuilder(); |
||||
|
for (byte b : bytes) { |
||||
|
hex.append(String.format("%02x", b)); |
||||
|
} |
||||
|
return hex.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 判断是否为32位16进制字符串 |
||||
|
*/ |
||||
|
private static boolean isValidHex32(String str) { |
||||
|
if (str == null || str.length() != 32) return false; |
||||
|
return str.matches("[0-9a-fA-F]{32}"); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 加密方法(与 Go 的 SM4Encrypt 完全一致) |
||||
|
* @param data 要加密的字符串数据 |
||||
|
* @param customKey 自定义密钥(Auth-key),如果提供则用作 IV |
||||
|
* @return 加密后的16进制字符串 |
||||
|
*/ |
||||
|
public static String encrypt(String data, String customKey) throws Exception { |
||||
|
// 1. 确定加密密钥(使用 Go 的 Sm4Key)
|
||||
|
String keyStr = SM4_KEY_PROCESSED; |
||||
|
byte[] keyBytes = keyStr.getBytes(StandardCharsets.UTF_8); |
||||
|
|
||||
|
// 2. 确定 IV(如果有 customKey 则用 customKey,否则用 Sm4Token)
|
||||
|
String ivStr = SM4_TOKEN_PROCESSED; |
||||
|
if (customKey != null && !customKey.isEmpty()) { |
||||
|
// Go 的逻辑:如果有 customKey,处理为16字节
|
||||
|
ivStr = processString(customKey, 16, "s"); |
||||
|
} |
||||
|
byte[] ivBytes = ivStr.getBytes(StandardCharsets.UTF_8); |
||||
|
|
||||
|
/*System.out.println("=== Java 加密参数 ==="); |
||||
|
System.out.println("加密密钥: " + keyStr + " (UTF-8字节: " + bytesToHex(keyBytes) + ")"); |
||||
|
System.out.println("IV: " + ivStr + " (UTF-8字节: " + bytesToHex(ivBytes) + ")"); |
||||
|
System.out.println("原始数据: " + data);*/ |
||||
|
|
||||
|
// 3. 使用 BouncyCastle 进行 SM4-CBC 加密
|
||||
|
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( |
||||
|
new CBCBlockCipher(new SM4Engine()), new PKCS7Padding() |
||||
|
); |
||||
|
|
||||
|
cipher.init(true, new ParametersWithIV(new KeyParameter(keyBytes), ivBytes)); |
||||
|
|
||||
|
byte[] inputBytes = data.getBytes(StandardCharsets.UTF_8); |
||||
|
byte[] outputBytes = new byte[cipher.getOutputSize(inputBytes.length)]; |
||||
|
|
||||
|
int length1 = cipher.processBytes(inputBytes, 0, inputBytes.length, outputBytes, 0); |
||||
|
int length2 = cipher.doFinal(outputBytes, length1); |
||||
|
|
||||
|
byte[] encryptedBytes = new byte[length1 + length2]; |
||||
|
System.arraycopy(outputBytes, 0, encryptedBytes, 0, encryptedBytes.length); |
||||
|
|
||||
|
String result = bytesToHex(encryptedBytes); |
||||
|
//System.out.println("加密结果(hex): " + result);
|
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 解密方法(与 Go 的 SM4Decrypt 完全一致) |
||||
|
* @param encryptedHex 加密后的16进制字符串 |
||||
|
* @param customKey 自定义密钥(Auth-key),如果提供则用作 IV |
||||
|
* @return 解密后的字符串 |
||||
|
*/ |
||||
|
public static String decrypt(String encryptedHex, String customKey) throws Exception { |
||||
|
// 1. 确定加密密钥(使用 Go 的 Sm4Key)
|
||||
|
String keyStr = SM4_KEY_PROCESSED; |
||||
|
byte[] keyBytes = keyStr.getBytes(StandardCharsets.UTF_8); |
||||
|
|
||||
|
// 2. 确定 IV(如果有 customKey 则用 customKey,否则用 Sm4Token)
|
||||
|
String ivStr = SM4_TOKEN_PROCESSED; |
||||
|
if (customKey != null && !customKey.isEmpty()) { |
||||
|
// Go 的逻辑:如果有 customKey,处理为16字节
|
||||
|
ivStr = processString(customKey, 16, "s"); |
||||
|
} |
||||
|
byte[] ivBytes = ivStr.getBytes(StandardCharsets.UTF_8); |
||||
|
|
||||
|
/*System.out.println("=== Java 解密参数 ==="); |
||||
|
System.out.println("解密密钥: " + keyStr + " (UTF-8字节: " + bytesToHex(keyBytes) + ")"); |
||||
|
System.out.println("IV: " + ivStr + " (UTF-8字节: " + bytesToHex(ivBytes) + ")"); |
||||
|
System.out.println("加密数据(hex): " + encryptedHex);*/ |
||||
|
|
||||
|
// 3. 转换加密数据
|
||||
|
byte[] encryptedBytes = hexToBytes(encryptedHex); |
||||
|
|
||||
|
// 4. 使用 BouncyCastle 进行 SM4-CBC 解密
|
||||
|
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( |
||||
|
new CBCBlockCipher(new SM4Engine()), new PKCS7Padding() |
||||
|
); |
||||
|
|
||||
|
cipher.init(false, new ParametersWithIV(new KeyParameter(keyBytes), ivBytes)); |
||||
|
|
||||
|
byte[] outputBytes = new byte[cipher.getOutputSize(encryptedBytes.length)]; |
||||
|
|
||||
|
int length1 = cipher.processBytes(encryptedBytes, 0, encryptedBytes.length, outputBytes, 0); |
||||
|
int length2 = cipher.doFinal(outputBytes, length1); |
||||
|
|
||||
|
byte[] decryptedBytes = new byte[length1 + length2]; |
||||
|
System.arraycopy(outputBytes, 0, decryptedBytes, 0, decryptedBytes.length); |
||||
|
|
||||
|
String result = new String(decryptedBytes, StandardCharsets.UTF_8); |
||||
|
System.out.println("解密结果: " + result); |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 字节数组转16进制字符串 |
||||
|
*/ |
||||
|
private static String bytesToHex(byte[] bytes) { |
||||
|
StringBuilder hex = new StringBuilder(); |
||||
|
for (byte b : bytes) { |
||||
|
hex.append(String.format("%02x", b)); |
||||
|
} |
||||
|
return hex.toString(); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 16进制字符串转字节数组 |
||||
|
*/ |
||||
|
private static byte[] hexToBytes(String hex) { |
||||
|
if (hex.length() % 2 != 0) { |
||||
|
throw new IllegalArgumentException("Hex string length must be even"); |
||||
|
} |
||||
|
byte[] bytes = new byte[hex.length() / 2]; |
||||
|
for (int i = 0; i < bytes.length; i++) { |
||||
|
int index = i * 2; |
||||
|
bytes[i] = (byte) Integer.parseInt(hex.substring(index, index + 2), 16); |
||||
|
} |
||||
|
return bytes; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
} |
||||
Loading…
Reference in new issue