diff --git a/doc/ipBlackMenu.sql b/doc/ipBlackMenu.sql deleted file mode 100644 index 027c3d3e..00000000 --- a/doc/ipBlackMenu.sql +++ /dev/null @@ -1,19 +0,0 @@ --- 菜单 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853670150475777, 'ip黑名单', '1738084052270563330', '1', 'ipBlack', 'cai/ipBlack/index', 1, 0, 'C', '0', '0', 'cai:ipBlack:list', '#', 'admin', sysdate(), '', null, 'ip黑名单菜单'); - --- 按钮 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853670150475778, 'ip黑名单查询', 1996853670150475777, '1', '#', '', 1, 0, 'F', '0', '0', 'cai:ipBlack:query', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853670150475779, 'ip黑名单新增', 1996853670150475777, '2', '#', '', 1, 0, 'F', '0', '0', 'cai:ipBlack:add', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853670150475780, 'ip黑名单修改', 1996853670150475777, '3', '#', '', 1, 0, 'F', '0', '0', 'cai:ipBlack:edit', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853670150475781, 'ip黑名单删除', 1996853670150475777, '4', '#', '', 1, 0, 'F', '0', '0', 'cai:ipBlack:remove', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853670150475782, 'ip黑名单导出', 1996853670150475777, '5', '#', '', 1, 0, 'F', '0', '0', 'cai:ipBlack:export', '#', 'admin', sysdate(), '', null, ''); diff --git a/doc/ipRecordMenu.sql b/doc/ipRecordMenu.sql deleted file mode 100644 index 4ac48857..00000000 --- a/doc/ipRecordMenu.sql +++ /dev/null @@ -1,19 +0,0 @@ --- 菜单 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853672289570817, 'ip访问记录', '1738084052270563330', '1', 'ipRecord', 'cai/ipRecord/index', 1, 0, 'C', '0', '0', 'cai:ipRecord:list', '#', 'admin', sysdate(), '', null, 'ip访问记录菜单'); - --- 按钮 SQL -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853672289570818, 'ip访问记录查询', 1996853672289570817, '1', '#', '', 1, 0, 'F', '0', '0', 'cai:ipRecord:query', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853672289570819, 'ip访问记录新增', 1996853672289570817, '2', '#', '', 1, 0, 'F', '0', '0', 'cai:ipRecord:add', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853672289570820, 'ip访问记录修改', 1996853672289570817, '3', '#', '', 1, 0, 'F', '0', '0', 'cai:ipRecord:edit', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853672289570821, 'ip访问记录删除', 1996853672289570817, '4', '#', '', 1, 0, 'F', '0', '0', 'cai:ipRecord:remove', '#', 'admin', sysdate(), '', null, ''); - -insert into sys_menu (menu_id, menu_name, parent_id, order_num, path, component, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, update_by, update_time, remark) -values(1996853672289570822, 'ip访问记录导出', 1996853672289570817, '5', '#', '', 1, 0, 'F', '0', '0', 'cai:ipRecord:export', '#', 'admin', sysdate(), '', null, ''); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/cai/admin/LoginLogController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/cai/admin/LoginLogController.java index e948bb90..c4da44d1 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/cai/admin/LoginLogController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/cai/admin/LoginLogController.java @@ -12,7 +12,6 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -35,12 +34,12 @@ public class LoginLogController extends BaseController { @SaCheckPermission("cai:loginLog:list") @GetMapping("/list") public R> list(String mobile) { -// List list = LoginLogByFileUtil.getLog(mobile); - List list = new ArrayList<>(); - list.add("2025-12-21 08:31:43 [XNIO-1 task-193] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.45.250;url=/api/auth/login;method=POST;title=登陆;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result=null;exception=您的账号已被封禁;;StartTime:2025-12-21 08:31:43,EndTime:2025-12-21 08:31:43,CostTime:5ms"); - list.add("2025-12-21 09:14:34 [XNIO-1 task-8] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.44.24;url=/api/auth/login;method=POST;title=登陆;currentUserId=44554;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result=null;exception=您的账号已被封禁;;StartTime:2025-12-21 09:14:34,EndTime:2025-12-21 09:14:34,CostTime:7ms"); - list.add("2025-12-21 09:55:08 [XNIO-1 task-193] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.44.156;url=/api/auth/login;method=POST;title=登陆;currentUserId=6501;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result={\"code\":200,\"msg\":\"操作成功\",\"data\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJhcHBfdXNlcjo2NTAxIiwicm5TdHIiOiJYR3pFcngxOWxXcm1MSEZhMGRhV0dDWFNERFhPaDBGeiIsInVzZXJJZCI6NjUwMX0.Coy1DgKMgtyEIpEpLKrvm_3w8SEjeKujfaTKu3l9AyI\",\"userInfo\":{\"userId\":6501,\"inviteId\":4387,\"type\":0,\"usercode\":\"6953\",\"nickname\":\"用户6953\",\"mobile\":\"13588246608\",\"avatar\":\"images/avatar/man.png\",\"avatarState\":0,\"gender\":2,\"birthday\":null,\"age\":18,\"cityId\":0,\"city\":null,\"isAnchor\":0,\"openVideoStatus\":1,\"status\":0,\"finishStatus\":0,\"imToken\":\"716d4d9bbe1b4441b88065b6bc0c543d\",\"userAccount\":{\"userId\":6501,\"coin\":2537,\"incomeCoin\":0},\"userCount\":{\"userId\":6501,\"newFansCount\":266,\"newVisitorCount\":5302,\"fansCount\":36,\"followCount\":24,\"footCount\":6691,\"visitorCount\":5302}}}};;StartTime:2025-12-21 09:55:07,EndTime:2025-12-21 09:55:08,CostTime:148ms"); - list.add("2025-12-21 10:30:21 [XNIO-1 task-46] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.45.234;url=/api/auth/login;method=POST;title=登陆;currentUserId=6501;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result={\"code\":200,\"msg\":\"操作成功\",\"data\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJhcHBfdXNlcjo2NTAxIiwicm5TdHIiOiI4U2dKZ3hiWVF0VHpjNmhSRzdXV3NYSGMyaGFQYmJ3eCIsInVzZXJJZCI6NjUwMX0.2KrY8jHwHl67A25JtRCmAldnOvSpcU1mtnYlcUFLhy0\",\"userInfo\":{\"userId\":6501,\"inviteId\":4387,\"type\":0,\"usercode\":\"6953\",\"nickname\":\"用户6953\",\"mobile\":\"13588246608\",\"avatar\":\"images/avatar/man.png\",\"avatarState\":0,\"gender\":2,\"birthday\":null,\"age\":18,\"cityId\":0,\"city\":null,\"isAnchor\":0,\"openVideoStatus\":1,\"status\":0,\"finishStatus\":0,\"imToken\":\"716d4d9bbe1b4441b88065b6bc0c543d\",\"userAccount\":{\"userId\":6501,\"coin\":2517,\"incomeCoin\":0},\"userCount\":{\"userId\":6501,\"newFansCount\":266,\"newVisitorCount\":5302,\"fansCount\":36,\"followCount\":24,\"footCount\":6692,\"visitorCount\":5302}}}};;StartTime:2025-12-21 10:30:21,EndTime:2025-12-21 10:30:21,CostTime:148ms"); + List list = LoginLogByFileUtil.getLog(mobile); +// List list = new ArrayList<>(); +// list.add("2025-12-21 08:31:43 [XNIO-1 task-193] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.45.250;url=/api/auth/login;method=POST;title=登陆;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result=null;exception=您的账号已被封禁;;StartTime:2025-12-21 08:31:43,EndTime:2025-12-21 08:31:43,CostTime:5ms"); +// list.add("2025-12-21 09:14:34 [XNIO-1 task-8] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.44.24;url=/api/auth/login;method=POST;title=登陆;currentUserId=44554;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result=null;exception=您的账号已被封禁;;StartTime:2025-12-21 09:14:34,EndTime:2025-12-21 09:14:34,CostTime:7ms"); +// list.add("2025-12-21 09:55:08 [XNIO-1 task-193] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.44.156;url=/api/auth/login;method=POST;title=登陆;currentUserId=6501;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result={\"code\":200,\"msg\":\"操作成功\",\"data\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJhcHBfdXNlcjo2NTAxIiwicm5TdHIiOiJYR3pFcngxOWxXcm1MSEZhMGRhV0dDWFNERFhPaDBGeiIsInVzZXJJZCI6NjUwMX0.Coy1DgKMgtyEIpEpLKrvm_3w8SEjeKujfaTKu3l9AyI\",\"userInfo\":{\"userId\":6501,\"inviteId\":4387,\"type\":0,\"usercode\":\"6953\",\"nickname\":\"用户6953\",\"mobile\":\"13588246608\",\"avatar\":\"images/avatar/man.png\",\"avatarState\":0,\"gender\":2,\"birthday\":null,\"age\":18,\"cityId\":0,\"city\":null,\"isAnchor\":0,\"openVideoStatus\":1,\"status\":0,\"finishStatus\":0,\"imToken\":\"716d4d9bbe1b4441b88065b6bc0c543d\",\"userAccount\":{\"userId\":6501,\"coin\":2537,\"incomeCoin\":0},\"userCount\":{\"userId\":6501,\"newFansCount\":266,\"newVisitorCount\":5302,\"fansCount\":36,\"followCount\":24,\"footCount\":6691,\"visitorCount\":5302}}}};;StartTime:2025-12-21 09:55:07,EndTime:2025-12-21 09:55:08,CostTime:148ms"); +// list.add("2025-12-21 10:30:21 [XNIO-1 task-46] INFO c.ruoyi.framework.aspectj.LogAspect - record logs:ip=172.56.45.234;url=/api/auth/login;method=POST;title=登陆;currentUserId=6501;user-agent=yan yu/1.0.6 (iPhone; iOS 26.2; Scale/3.00);params={\"username\":\"13588246608\"};result={\"code\":200,\"msg\":\"操作成功\",\"data\":{\"token\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpblR5cGUiOiJsb2dpbiIsImxvZ2luSWQiOiJhcHBfdXNlcjo2NTAxIiwicm5TdHIiOiI4U2dKZ3hiWVF0VHpjNmhSRzdXV3NYSGMyaGFQYmJ3eCIsInVzZXJJZCI6NjUwMX0.2KrY8jHwHl67A25JtRCmAldnOvSpcU1mtnYlcUFLhy0\",\"userInfo\":{\"userId\":6501,\"inviteId\":4387,\"type\":0,\"usercode\":\"6953\",\"nickname\":\"用户6953\",\"mobile\":\"13588246608\",\"avatar\":\"images/avatar/man.png\",\"avatarState\":0,\"gender\":2,\"birthday\":null,\"age\":18,\"cityId\":0,\"city\":null,\"isAnchor\":0,\"openVideoStatus\":1,\"status\":0,\"finishStatus\":0,\"imToken\":\"716d4d9bbe1b4441b88065b6bc0c543d\",\"userAccount\":{\"userId\":6501,\"coin\":2517,\"incomeCoin\":0},\"userCount\":{\"userId\":6501,\"newFansCount\":266,\"newVisitorCount\":5302,\"fansCount\":36,\"followCount\":24,\"footCount\":6692,\"visitorCount\":5302}}}};;StartTime:2025-12-21 10:30:21,EndTime:2025-12-21 10:30:21,CostTime:148ms"); List collect = list.stream().map(LoginLogInfo::new).collect(Collectors.toList()); return R.ok(collect); } diff --git a/ruoyi-cai/src/main/java/com/ruoyi/cai/util/LoginLogByFileUtil.java b/ruoyi-cai/src/main/java/com/ruoyi/cai/util/LoginLogByFileUtil.java index eea9d5be..b55c49eb 100644 --- a/ruoyi-cai/src/main/java/com/ruoyi/cai/util/LoginLogByFileUtil.java +++ b/ruoyi-cai/src/main/java/com/ruoyi/cai/util/LoginLogByFileUtil.java @@ -7,71 +7,171 @@ import org.apache.commons.exec.environment.EnvironmentUtils; import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; +/** + * 登录日志查询工具类(Linux环境专用,UTF-8编码日志) + * 功能:执行Linux命令查询指定手机号的登录日志,最多返回10条 + * 核心优化:解决换行符/引号/流覆盖问题,适配UTF-8编码,增强安全&健壮性 + */ @Slf4j public class LoginLogByFileUtil { + private static final String LOG_FILE_PATH = "/home/server/api/logs/sys-console.log"; + private static final int COMMAND_TIMEOUT_MS = 5000; + private static final Charset LOG_FILE_CHARSET = StandardCharsets.UTF_8; // 确认日志为UTF-8编码 + private static final int MAX_LOG_LINES = 10; - public static List getLog(String mobile){ - // 1. 定义命令(带管道的Linux命令,需用/bin/sh -c包裹) - String commandFormat = "tac /home/server/api/logs/sys-console.log | grep \"%s\" | grep -m 10 \"auth/login\""; - String command = String.format(commandFormat, mobile); - CommandLine cmdLine = CommandLine.parse("/bin/sh -c " + command); // 自动处理参数分隔 + /** + * 获取指定手机号的登录日志(按时间倒序,最多10条) + * @param mobile 手机号(支持纯数字/带+86等格式) + * @return 非空日志行列表(无数据返回空列表,不会返回null) + */ + public static List getLog(String mobile) { + // 1. 入参校验(避免无效执行) + if (mobile == null || mobile.trim().isEmpty()) { + log.warn("查询登录日志失败:手机号为空"); + return Collections.emptyList(); + } + String cleanMobile = mobile.trim(); - // 2. 配置执行器(设置超时、流处理) - DefaultExecutor executor = new DefaultExecutor(); - executor.setExitValue(0); // 期望的退出码(0表示成功) + // 2. 构建安全的管道命令(防注入+引号兼容) + String pipeCommand = buildSafePipeCommand(cleanMobile); + CommandLine cmdLine = buildCommandLine(pipeCommand); - // 设置进程超时(5秒,适配你的快速执行场景) - ExecuteWatchdog watchdog = new ExecuteWatchdog(5000); // 5000ms = 5s - executor.setWatchdog(watchdog); - - // 3. 处理输出(ByteArrayOutputStream存储10行结果,或用PumpStreamHandler实时处理) - // 场景1:存储结果到内存(仅10行,内存无压力) - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, outputStream); // 合并标准输出/错误流 - executor.setStreamHandler(streamHandler); - - // 场景2:实时处理每行输出(推荐,避免存储) - // PumpStreamHandler streamHandler = new PumpStreamHandler(new LogOutputStream() { - // @Override - // protected void processLine(String line, int level) { - // // 实时处理每行数据(如输出到前端、写入日志) - // System.out.println(line); - // } - // }); - // executor.setStreamHandler(streamHandler); + // 3. 初始化执行器&流处理(分开捕获stdout/stderr,避免覆盖) + DefaultExecutor executor = initExecutor(); + ByteArrayOutputStream stdout = new ByteArrayOutputStream(); + ByteArrayOutputStream stderr = new ByteArrayOutputStream(); + executor.setStreamHandler(new PumpStreamHandler(stdout, stderr)); try { - // 4. 执行命令(环境变量使用系统默认,可自定义) + // 4. 执行命令(获取系统默认环境变量) Map env = EnvironmentUtils.getProcEnvironment(); - executor.execute(cmdLine, env); - - // 5. 解析结果(场景1的方式) - String result = outputStream.toString(StandardCharsets.UTF_8.name()); - String[] split = result.split(System.lineSeparator()); - List lines = Arrays.stream(split).collect(Collectors.toList()); - System.out.println("匹配的行数:" + lines.size()); - lines.forEach(System.out::println); + int exitCode = executor.execute(cmdLine, env); + // 5. 解析结果(核心修复:UTF-8编码+Linux换行符+过滤) + return parseCommandResult(stdout, stderr, exitCode); } catch (ExecuteException e) { - if (watchdog.killedProcess()) { - log.error("命令执行超时,已终止进程"); - } else { - log.error("命令执行失败,退出码:" + e.getExitValue()); - } - log.error(e.getMessage(),e); - throw new BaseException("命令执行失败"); + handleExecuteException(e, executor.getWatchdog()); } catch (IOException e) { - log.error("IO异常:" + e.getMessage()); - log.error(e.getMessage(),e); - throw new BaseException("IO异常"); + log.error("日志查询IO异常:手机号={}, 异常信息={}", cleanMobile, e.getMessage(), e); + throw new BaseException("日志查询失败:系统IO异常"); + } finally { + // 6. 资源释放(避免内存泄漏) + closeStream(stdout); + closeStream(stderr); } return Collections.emptyList(); } + + /** + * 构建安全的管道命令(防注入+适配grep固定字符串匹配) + */ + private static String buildSafePipeCommand(String mobile) { + // 转义手机号特殊字符(防注入)+ 用单引号包裹(适配Shell) + String escapedMobile = escapeShellSpecialChars(mobile); + // 命令逻辑:tac倒序读文件 → grep -F固定字符串匹配(避免正则干扰)→ 限制行数 + return String.format("grep 'auth/login.*%s' %s | tail -10 | tac", escapedMobile,LOG_FILE_PATH); +// return String.format( +// "tac %s | grep -F '%s' | grep -m %d 'auth/login'", +// LOG_FILE_PATH, escapedMobile, MAX_LOG_LINES +// ); + } + + /** + * 构建CommandLine(避免parse方法的引号解析问题) + */ + private static CommandLine buildCommandLine(String pipeCommand) { + CommandLine cmdLine = new CommandLine("/bin/sh"); + cmdLine.addArgument("-c"); + cmdLine.addArgument(pipeCommand, false); // 关键:不自动转义,保持命令原样 + return cmdLine; + } + + /** + * 初始化执行器(超时+退出码兼容) + */ + private static DefaultExecutor initExecutor() { + DefaultExecutor executor = new DefaultExecutor(); + // 兼容grep无匹配的退出码1(仅0/1视为正常) + executor.setExitValues(new int[]{0, 1}); + // 设置超时看门狗 + ExecuteWatchdog watchdog = new ExecuteWatchdog(COMMAND_TIMEOUT_MS); + executor.setWatchdog(watchdog); + return executor; + } + + /** + * 解析命令结果(核心:UTF-8编码+Linux换行符\n分割) + */ + private static List parseCommandResult(ByteArrayOutputStream stdout, + ByteArrayOutputStream stderr, + int exitCode) { + // 打印错误流(排查问题用,不影响结果) + String stderrStr = new String(stderr.toByteArray(), LOG_FILE_CHARSET).trim(); + if (!stderrStr.isEmpty()) { + log.warn("命令执行错误流输出:{}", stderrStr); + } + + // 解析标准输出(核心:UTF-8编码 + 强制用\n分割Linux输出) + String stdoutStr = new String(stdout.toByteArray(), LOG_FILE_CHARSET); + // 分割+过滤:强制用\n分割(Linux命令输出固定是\n),仅过滤纯空行 + List logLines = Arrays.stream(stdoutStr.split("\n")) + .map(String::trim) + .filter(line -> !line.isEmpty()) // 仅过滤纯空行,保留有效内容 + .collect(Collectors.toList()); + // 退出码1表示无匹配结果,返回空列表 + if (exitCode == 1 && logLines.isEmpty()) { + log.info("未查询到匹配的登录日志"); + return Collections.emptyList(); + } + return logLines; + } + + /** + * 转义Shell特殊字符(防命令注入) + */ + private static String escapeShellSpecialChars(String str) { + if (str == null) return ""; + // 转义Shell中特殊字符:单引号/反斜杠/分号/管道符等 + return str.replace("\\", "\\\\") + .replace("'", "\\'") + .replace("\"", "\\\"") + .replace("`", "\\`") + .replace("$", "\\$") + .replace(";", "\\;") + .replace("|", "\\|") + .replace("<", "\\<") + .replace(">", "\\>"); + } + + /** + * 处理命令执行异常 + */ + private static void handleExecuteException(ExecuteException e, ExecuteWatchdog watchdog) { + if (watchdog != null && watchdog.killedProcess()) { + log.error("命令执行超时({}ms),已终止进程", COMMAND_TIMEOUT_MS); + throw new BaseException("日志查询超时:请稍后重试"); + } else { + log.error("命令执行失败,退出码:{},异常信息:{}", + e.getExitValue(), e.getMessage(), e); + throw new BaseException("日志查询失败:命令执行异常(退出码:" + e.getExitValue() + ")"); + } + } + + /** + * 关闭字节输出流(静默关闭,避免IO异常) + */ + private static void closeStream(ByteArrayOutputStream stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + log.warn("关闭字节输出流失败,异常信息:{}", e.getMessage()); + } + } + } }