diff --git a/backend/pom.xml b/backend/pom.xml index 09af9f5e..0e8b3e1f 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -177,6 +177,13 @@ org.springframework.retry spring-retry + + + + com.hierynomus + sshj + 0.38.0 + org.springframework.boot spring-boot-starter-aop diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/ServerApiController.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/ServerApiController.java index 1f2b2987..c285e52d 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/ServerApiController.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/api/ServerApiController.java @@ -41,6 +41,15 @@ public class ServerApiController return Response.success(result); } + @Operation(summary = "测试SSH连接", description = "测试服务器SSH连接是否正常") + @PostMapping("/{id}/test-connection") + public Response testConnection( + @Parameter(description = "服务器ID", required = true) @PathVariable Long id + ) { + boolean success = serverService.testConnection(id); + return Response.success(success); + } + @Override protected void exportData(HttpServletResponse response, List data) { log.info("导出服务器数据,数据量:{}", data.size()); diff --git a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerService.java b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerService.java index 287fad64..72b30089 100644 --- a/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerService.java +++ b/backend/src/main/java/com/qqchen/deploy/backend/deploy/service/IServerService.java @@ -19,5 +19,13 @@ public interface IServerService extends IBaseService new BusinessException(ResponseCode.DATA_NOT_FOUND)); + + // 2. 验证必要字段 + if (server.getHostIp() == null || server.getSshUser() == null) { + throw new BusinessException(ResponseCode.INVALID_PARAM, new Object[]{"服务器IP和SSH用户名不能为空"}); + } + + SSHClient ssh = new SSHClient(); + try { + // 3. 配置SSH客户端 + // TODO: 【安全改进】生产环境应使用更安全的主机密钥验证方式 + // 当前:使用 PromiscuousVerifier 跳过主机密钥验证(不验证服务器身份,存在中间人攻击风险) + // 建议改为: + // 1. ssh.loadKnownHosts() - 使用 ~/.ssh/known_hosts 文件验证 + // 2. ssh.addHostKeyVerifier(new ConsoleVerifyingHostKeyVerifier()) - 首次自动接受,后续验证 + // 3. 在数据库中存储服务器指纹,首次连接时记录,后续验证 + ssh.addHostKeyVerifier(new PromiscuousVerifier()); + ssh.setTimeout(10000); // 10秒超时 + ssh.setConnectTimeout(10000); + + // 4. 连接服务器 + int port = server.getSshPort() != null ? server.getSshPort() : 22; + log.info("尝试连接服务器: {}@{}:{}", server.getSshUser(), server.getHostIp(), port); + ssh.connect(server.getHostIp(), port); + + // 5. 根据认证方式进行认证 + if (server.getAuthType() == AuthTypeEnum.KEY) { + // 密钥认证 + if (server.getSshPrivateKey() == null || server.getSshPrivateKey().isEmpty()) { + throw new BusinessException(ResponseCode.INVALID_PARAM, new Object[]{"SSH私钥不能为空"}); + } + + KeyProvider keyProvider; + if (server.getSshPassphrase() != null && !server.getSshPassphrase().isEmpty()) { + // 私钥有密码保护 + keyProvider = ssh.loadKeys(server.getSshPrivateKey(), null, + net.schmizz.sshj.userauth.password.PasswordUtils.createOneOff(server.getSshPassphrase().toCharArray())); + } else { + // 私钥无密码保护 + keyProvider = ssh.loadKeys(server.getSshPrivateKey(), null, null); + } + + ssh.authPublickey(server.getSshUser(), keyProvider); + log.info("使用密钥认证成功"); + } else { + // 密码认证 + if (server.getSshPassword() == null || server.getSshPassword().isEmpty()) { + throw new BusinessException(ResponseCode.INVALID_PARAM, new Object[]{"SSH密码不能为空"}); + } + + ssh.authPassword(server.getSshUser(), server.getSshPassword()); + log.info("使用密码认证成功"); + } + + // 6. 测试执行简单命令 + try (var session = ssh.startSession()) { + session.allocateDefaultPTY(); + var cmd = session.exec("echo 'Connection test successful'"); + cmd.join(5, TimeUnit.SECONDS); + + if (cmd.getExitStatus() == 0) { + log.info("SSH连接测试成功: {}", server.getHostIp()); + return true; + } else { + log.warn("SSH连接测试失败,命令执行异常: {}", server.getHostIp()); + return false; + } + } + + } catch (IOException e) { + log.error("SSH连接测试失败: {} - {}", server.getHostIp(), e.getMessage()); + throw new BusinessException(ResponseCode.ERROR, new Object[]{"SSH连接失败: " + e.getMessage()}); + } finally { + // 7. 关闭连接 + try { + if (ssh.isConnected()) { + ssh.disconnect(); + } + } catch (IOException e) { + log.error("关闭SSH连接失败", e); + } + } + } }