From 4604c15f0cbf3b89ed65eda8da84b9a85378c1ce Mon Sep 17 00:00:00 2001 From: gaoxq <376340421@qq.com> Date: Sat, 18 Apr 2026 11:23:28 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=B8=BB=E6=9C=BA=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/apps/Module/MonitorResult.java | 66 +++++++++++ .../com/jeesite/modules/utils/AesUtil.java | 109 ------------------ .../com/jeesite/modules/utils/KeyUtil.java | 69 +++++------ .../jeesite/modules/utils/MonitorUtil.java | 38 +++--- .../com/jeesite/modules/utils/PageUtil.java | 25 ++-- 5 files changed, 129 insertions(+), 178 deletions(-) create mode 100644 web-api/src/main/java/com/jeesite/modules/apps/Module/MonitorResult.java delete mode 100644 web-api/src/main/java/com/jeesite/modules/utils/AesUtil.java diff --git a/web-api/src/main/java/com/jeesite/modules/apps/Module/MonitorResult.java b/web-api/src/main/java/com/jeesite/modules/apps/Module/MonitorResult.java new file mode 100644 index 0000000..931933c --- /dev/null +++ b/web-api/src/main/java/com/jeesite/modules/apps/Module/MonitorResult.java @@ -0,0 +1,66 @@ +package com.jeesite.modules.apps.Module; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; +import java.util.Map; + +/** + * 主机系统监控结果 + * + * @author gaoxq + */ +@Data +public class MonitorResult implements Serializable { + + private boolean success; + private String message; + + /** 主机名 */ + private String hostname; + + /** 操作系统 */ + private String os; + + /** 在线时长描述 */ + private String uptime; + + /** CPU 占用率明细(key: us/sy/ni/id/wa/st) */ + private Map cpu; + + /** 内存明细(total/used/free/available/usagePercent) */ + private Map memory; + + /** 各挂载点磁盘信息 */ + private List> disk; + + // ---------------------- 构造工厂 ---------------------- + + public static MonitorResult ok(String message) { + MonitorResult r = new MonitorResult(); + r.success = true; + r.message = message; + return r; + } + + public static MonitorResult ok(String message, String hostname, String os, String uptime, + Map cpu, Map memory, + List> disk) { + MonitorResult r = ok(message); + r.hostname = hostname; + r.os = os; + r.uptime = uptime; + r.cpu = cpu; + r.memory = memory; + r.disk = disk; + return r; + } + + public static MonitorResult fail(String message) { + MonitorResult r = new MonitorResult(); + r.success = false; + r.message = message; + return r; + } +} diff --git a/web-api/src/main/java/com/jeesite/modules/utils/AesUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/AesUtil.java deleted file mode 100644 index b34f535..0000000 --- a/web-api/src/main/java/com/jeesite/modules/utils/AesUtil.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.jeesite.modules.utils; - -import javax.crypto.Cipher; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import java.nio.charset.StandardCharsets; -import java.util.Base64; -import java.util.Arrays; -import java.util.concurrent.ThreadLocalRandom; - -/** - * AES加密解密工具类(CBC/PKCS5Padding) - * 优化说明:修复密钥BUG+解决换行问题+性能优化+安全加固+零依赖 - */ -public class AesUtil { - - - private static final LoggerUtils logger = LoggerUtils.getInstance(); - private static final String AES_KEY_HEX = "AD42F6697B035B7580E4FEF93BE20BAD"; // 你的32位16进制密钥 - private static final String CHARSET = StandardCharsets.UTF_8.name(); - private static final int IV_LENGTH = 16; // AES CBC IV固定16字节 - private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding"; - private static final String ALGORITHM = "AES"; - - private static final ThreadLocal ENCRYPT_CIPHER = ThreadLocal.withInitial(() -> { - try { - return Cipher.getInstance(TRANSFORMATION); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - private static final ThreadLocal DECRYPT_CIPHER = ThreadLocal.withInitial(() -> { - try { - return Cipher.getInstance(TRANSFORMATION); - } catch (Exception e) { - throw new RuntimeException(e); - } - }); - - /** - * 加密 - */ - public static String encrypt(String content) { - return encrypt(content, hex2Bytes(AES_KEY_HEX)); - } - - /** - * 解密 - */ - public static String decrypt(String content) { - return decrypt(content, hex2Bytes(AES_KEY_HEX)); - } - - public static String encrypt(String content, byte[] key) { - if (content == null || content.isEmpty()) { - return ""; - } - try { - SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM); - IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(key, IV_LENGTH)); - Cipher cipher = ENCRYPT_CIPHER.get(); - cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv); - byte[] encryptBytes = cipher.doFinal(content.getBytes(CHARSET)); - return Base64.getEncoder().encodeToString(encryptBytes); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public static String decrypt(String content, byte[] key) { - if (content == null || content.isEmpty()) { - return ""; - } - try { - SecretKeySpec secretKey = new SecretKeySpec(key, ALGORITHM); - IvParameterSpec iv = new IvParameterSpec(Arrays.copyOf(key, IV_LENGTH)); - Cipher cipher = DECRYPT_CIPHER.get(); - cipher.init(Cipher.DECRYPT_MODE, secretKey, iv); - byte[] decryptBytes = cipher.doFinal(Base64.getDecoder().decode(content)); - String result = new String(decryptBytes, CHARSET); - return result.trim(); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** - * 32位16进制字符串转16字节数组(解决你的密钥长度BUG的核心方法) - */ - private static byte[] hex2Bytes(String hexStr) { - if (hexStr == null || hexStr.length() % 2 != 0) { - throw new IllegalArgumentException("密钥必须是偶数长度的16进制字符串"); - } - byte[] bytes = new byte[hexStr.length() / 2]; - for (int i = 0; i < bytes.length; i++) { - bytes[i] = (byte) Integer.parseInt(hexStr.substring(i * 2, i * 2 + 2), 16); - } - return bytes; - } - - /** - * 生成随机16字节IV向量(方案2:安全随机IV,新业务推荐使用) - */ - private static byte[] generateRandomIV() { - byte[] iv = new byte[IV_LENGTH]; - ThreadLocalRandom.current().nextBytes(iv); - return iv; - } -} \ No newline at end of file diff --git a/web-api/src/main/java/com/jeesite/modules/utils/KeyUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/KeyUtil.java index 2369090..19016a8 100644 --- a/web-api/src/main/java/com/jeesite/modules/utils/KeyUtil.java +++ b/web-api/src/main/java/com/jeesite/modules/utils/KeyUtil.java @@ -1,56 +1,41 @@ package com.jeesite.modules.utils; import java.awt.*; -import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; public class KeyUtil { + /** + * 生成随机 key + * + * @param length key 长度 + * @param type 1=纯数字, 2=大写字母+数字, 3=小写字母+数字, 4=混合大小写+数字 + * @return 随机字符串 + */ public static String ObjKey(int length, int type) { - Random random = new Random(); - StringBuffer key = new StringBuffer(); - if (type == 1) { - String str = "0123456789"; - for (int i = 0; i < length; ++i) { - //从62个的数字或字母中选择 - int number = random.nextInt(10); - //将产生的数字通过length次承载到key中 - key.append(str.charAt(number)); - } - return key.toString(); - } else if (type == 2) { - String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - for (int i = 0; i < length; ++i) { - //从62个的数字或字母中选择 - int number = random.nextInt(36); - //将产生的数字通过length次承载到key中 - key.append(str.charAt(number)); - } - return key.toString(); - } else if (type == 3) { - String str = "abcdefghijklmnopqrstuvwxyz0123456789"; - for (int i = 0; i < length; ++i) { - //从62个的数字或字母中选择 - int number = random.nextInt(36); - //将产生的数字通过length次承载到key中 - key.append(str.charAt(number)); - } - return key.toString(); - } else { - String str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for (int i = 0; i < length; ++i) { - //从62个的数字或字母中选择 - int number = random.nextInt(62); - //将产生的数字通过length次承载到key中 - key.append(str.charAt(number)); - } - return key.toString(); + String str; + switch (type) { + case 1: str = "0123456789"; break; + case 2: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; break; + case 3: str = "abcdefghijklmnopqrstuvwxyz0123456789"; break; + default: str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; break; } + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + StringBuilder key = new StringBuilder(length); + for (int i = 0; i < length; i++) { + key.append(str.charAt(rnd.nextInt(str.length()))); + } + return key.toString(); } + /** + * 生成随机柔和颜色(RGB 100-239) + */ public static String getColor() { - int r = 100 + (int) (Math.random() * 140); - int g = 100 + (int) (Math.random() * 140); - int b = 100 + (int) (Math.random() * 140); + ThreadLocalRandom rnd = ThreadLocalRandom.current(); + int r = 100 + rnd.nextInt(140); + int g = 100 + rnd.nextInt(140); + int b = 100 + rnd.nextInt(140); Color color = new Color(r, g, b); return "#" + Integer.toHexString(color.getRGB()).substring(2).toUpperCase(); } diff --git a/web-api/src/main/java/com/jeesite/modules/utils/MonitorUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/MonitorUtil.java index 6bc5c90..205adfd 100644 --- a/web-api/src/main/java/com/jeesite/modules/utils/MonitorUtil.java +++ b/web-api/src/main/java/com/jeesite/modules/utils/MonitorUtil.java @@ -1,7 +1,7 @@ package com.jeesite.modules.utils; import com.jcraft.jsch.*; -import com.jeesite.modules.apps.Module.SftpResult; +import com.jeesite.modules.apps.Module.MonitorResult; import com.jeesite.modules.biz.entity.MySftpAccounts; import io.micrometer.common.util.StringUtils; @@ -11,7 +11,7 @@ import java.util.*; /** * 远程主机系统监控工具类 - * 通过 SSH exec 采集 CPU、内存、磁盘、负载、在线时长等指标 + * 通过 SSH exec 采集 CPU、内存、磁盘、负载、在线时长等指标(仅支持 Linux) * * @author gaoxq */ @@ -24,10 +24,10 @@ public class MonitorUtil { /** * 采集远程主机系统信息 * - * @param account SSH账号(复用 MySftpAccounts 的连接信息) + * @param account SSH 账号(复用 MySftpAccounts 的连接信息) * @return MonitorResult */ - public static SftpResult monitor(MySftpAccounts account) { + public static MonitorResult monitor(MySftpAccounts account) { Session session = null; ChannelExec exec = null; try { @@ -41,12 +41,12 @@ public class MonitorUtil { String output = readStream(in); String err = readStream(errIn); if (!err.isEmpty() && output.isEmpty()) { - return SftpResult.fail("采集失败: " + err); + return MonitorResult.fail("采集失败: " + err); } - return SftpResult.ok("采集成功", parse(output)); + return parseResult(output); } catch (Exception e) { - return SftpResult.fail("采集失败: " + e.getMessage()); + return MonitorResult.fail("采集失败: " + e.getMessage()); } finally { if (exec != null && exec.isConnected()) exec.disconnect(); if (session != null && session.isConnected()) session.disconnect(); @@ -64,17 +64,17 @@ public class MonitorUtil { "echo '==MARK_DISK==' && df -h"; } - private static Map parse(String output) { - Map info = new LinkedHashMap<>(); + // ------------------------------------------------------------------ 解析 - info.put("hostname", extract(output, "MARK_HOSTNAME", true)); - info.put("os", extract(output, "MARK_OS", true)); - info.put("uptime", extract(output, "MARK_UPTIME", true)); - info.put("cpu", parseCpu(extract(output, "MARK_CPU", false))); - info.put("memory", parseMemory(extract(output, "MARK_MEM", false))); - info.put("disk", parseDisk(extract(output, "MARK_DISK", false))); + private static MonitorResult parseResult(String output) { + String hostname = extract(output, "MARK_HOSTNAME", true); + String os = extract(output, "MARK_OS", true); + String uptime = extract(output, "MARK_UPTIME", true); + Map cpu = parseCpu(extract(output, "MARK_CPU", false)); + Map memory = parseMemory(extract(output, "MARK_MEM", false)); + List> disk = parseDisk(extract(output, "MARK_DISK", false)); - return info; + return MonitorResult.ok("采集成功", hostname, os, uptime, cpu, memory, disk); } // ---- CPU ---- @@ -87,7 +87,7 @@ public class MonitorUtil { Map cpu = new LinkedHashMap<>(); for (String line : block.split("\n")) { if (line.startsWith("Cpu") || line.startsWith("%Cpu")) { - String data = line.replaceFirst(".*?:\\s*", "").replaceFirst("^%Cpu\\(s?\\):\\s*", ""); + String data = line.replaceFirst("^%?Cpu\\(s?\\):\\s*", ""); for (String part : data.split(",")) { part = part.trim(); String[] kv = part.split("\\s+", 2); @@ -101,6 +101,8 @@ public class MonitorUtil { return cpu; } + // ---- 内存 ---- + /** * 解析 free -m 输出 * Mem: total used free shared buff/cache available @@ -157,7 +159,7 @@ public class MonitorUtil { return disks; } - // ---- 段落提取 ---- + // ---- 工具方法 ---- private static String extract(String output, String mark, boolean singleLine) { String start = "==" + mark + "=="; diff --git a/web-api/src/main/java/com/jeesite/modules/utils/PageUtil.java b/web-api/src/main/java/com/jeesite/modules/utils/PageUtil.java index 6b839e0..ac692f2 100644 --- a/web-api/src/main/java/com/jeesite/modules/utils/PageUtil.java +++ b/web-api/src/main/java/com/jeesite/modules/utils/PageUtil.java @@ -10,18 +10,17 @@ import java.util.List; @Data public class PageUtil implements Serializable { - private List data; private Integer curPage;// 当前页 private int totalCount;// 总条数 private int pageSize; // 每页显示的条数 - //构造数据 + // 构造数据 public PageUtil(int curPage, int pageSize, List data) { - this.data = data; + this.data = data == null ? new ArrayList<>() : data; this.curPage = curPage; this.pageSize = pageSize; - this.totalCount = data.size(); + this.totalCount = this.data.size(); } public int beginIndex(int pageSize, int curPage) { @@ -32,12 +31,20 @@ public class PageUtil implements Serializable { return (pageSize * curPage) - 1; } + /** + * 返回当前页数据(含边界保护) + */ public List OkData() { - List list = new ArrayList(); - for (int i = beginIndex(pageSize, curPage); i <= endIndex(pageSize, curPage); i++) { - if (i < totalCount) { - list.add(data.get(i)); - } + // 页码保护:小于 1 → 第 1 页,超出范围 → 空列表 + int safePage = (curPage == null || curPage < 1) ? 1 : curPage; + int begin = beginIndex(pageSize, safePage); + if (begin >= totalCount || pageSize <= 0) { + return new ArrayList<>(); + } + List list = new ArrayList<>(); + int end = Math.min(endIndex(pageSize, safePage), totalCount - 1); + for (int i = begin; i <= end; i++) { + list.add(data.get(i)); } return list; }