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);
+ }
+ }
+ }
}