增加服务器管理认证方式,增加测试连接接口

This commit is contained in:
dengqichen 2025-10-30 14:48:10 +08:00
parent 9868a32bc7
commit 0594d1856d
4 changed files with 118 additions and 0 deletions

View File

@ -177,6 +177,13 @@
<groupId>org.springframework.retry</groupId> <groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId> <artifactId>spring-retry</artifactId>
</dependency> </dependency>
<!-- SSH Client (SSHJ) -->
<dependency>
<groupId>com.hierynomus</groupId>
<artifactId>sshj</artifactId>
<version>0.38.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId> <artifactId>spring-boot-starter-aop</artifactId>

View File

@ -41,6 +41,15 @@ public class ServerApiController
return Response.success(result); return Response.success(result);
} }
@Operation(summary = "测试SSH连接", description = "测试服务器SSH连接是否正常")
@PostMapping("/{id}/test-connection")
public Response<Boolean> testConnection(
@Parameter(description = "服务器ID", required = true) @PathVariable Long id
) {
boolean success = serverService.testConnection(id);
return Response.success(success);
}
@Override @Override
protected void exportData(HttpServletResponse response, List<ServerDTO> data) { protected void exportData(HttpServletResponse response, List<ServerDTO> data) {
log.info("导出服务器数据,数据量:{}", data.size()); log.info("导出服务器数据,数据量:{}", data.size());

View File

@ -19,5 +19,13 @@ public interface IServerService extends IBaseService<Server, ServerDTO, ServerQu
* @return 更新后的服务器信息 * @return 更新后的服务器信息
*/ */
ServerDTO initializeServerInfo(Long serverId, ServerInitializeDTO dto); ServerDTO initializeServerInfo(Long serverId, ServerInitializeDTO dto);
/**
* 测试服务器SSH连接
*
* @param serverId 服务器ID
* @return 是否连接成功
*/
boolean testConnection(Long serverId);
} }

View File

@ -3,6 +3,7 @@ package com.qqchen.deploy.backend.deploy.service.impl;
import com.qqchen.deploy.backend.deploy.dto.ServerDTO; import com.qqchen.deploy.backend.deploy.dto.ServerDTO;
import com.qqchen.deploy.backend.deploy.dto.ServerInitializeDTO; import com.qqchen.deploy.backend.deploy.dto.ServerInitializeDTO;
import com.qqchen.deploy.backend.deploy.entity.Server; import com.qqchen.deploy.backend.deploy.entity.Server;
import com.qqchen.deploy.backend.deploy.enums.AuthTypeEnum;
import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum; import com.qqchen.deploy.backend.deploy.enums.ServerStatusEnum;
import com.qqchen.deploy.backend.deploy.query.ServerQuery; import com.qqchen.deploy.backend.deploy.query.ServerQuery;
import com.qqchen.deploy.backend.deploy.repository.IServerRepository; import com.qqchen.deploy.backend.deploy.repository.IServerRepository;
@ -12,10 +13,15 @@ import com.qqchen.deploy.backend.framework.enums.ResponseCode;
import com.qqchen.deploy.backend.framework.exception.BusinessException; import com.qqchen.deploy.backend.framework.exception.BusinessException;
import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl; import com.qqchen.deploy.backend.framework.service.impl.BaseServiceImpl;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.schmizz.sshj.SSHClient;
import net.schmizz.sshj.transport.verification.PromiscuousVerifier;
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.concurrent.TimeUnit;
/** /**
@ -57,5 +63,93 @@ public class ServerServiceImpl
return converter.toDto(updated); return converter.toDto(updated);
} }
@Override
public boolean testConnection(Long serverId) {
// 1. 查询服务器信息
Server server = serverRepository.findById(serverId)
.orElseThrow(() -> 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);
}
}
}
} }