SpringBoot + SSH 客户端:在浏览器中执行远程命令






浏览器终端 ←→ WebSocket ←→ Spring Boot应用 ←→ SSH连接 ←→ 目标服务器
↓ ↓
用户界面 数据存储
命令输入 操作记录
结果显示 配置管理


-
项目初始化
<?xml version="1.0" encoding="UTF-8"?>
<projectxmlns="http://maven.apache.org/POM/4.0.0">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.example</groupId>
<artifactId>web-ssh-client</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- WebSocket支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- SSH客户端 -->
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jsch</artifactId>
<version>0.1.55</version>
</dependency>
<!-- JDBC支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!-- H2数据库(开发测试用) -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MySQL驱动(生产环境用) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- JSON处理 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
-
SSH连接管理器
@Component
@Slf4j
publicclassSSHConnectionManager {
privatefinal Map<String, Session> connections = newConcurrentHashMap<>();
privatefinal Map<String, ChannelShell> channels = newConcurrentHashMap<>();
/**
* 建立SSH连接
*/
public String createConnection(String host, int port, String username, String password) {
try {
JSchjsch=newJSch();
Sessionsession= jsch.getSession(username, host, port);
// 配置连接参数
Propertiesconfig=newProperties();
config.put("StrictHostKeyChecking", "no");
config.put("PreferredAuthentications", "password");
session.setConfig(config);
session.setPassword(password);
// 建立连接
session.connect(30000); // 30秒超时
// 创建Shell通道
ChannelShellchannel= (ChannelShell) session.openChannel("shell");
channel.setPty(true);
channel.setPtyType("xterm", 80, 24, 640, 480);
// 生成连接ID
StringconnectionId= UUID.randomUUID().toString();
// 保存连接和通道
connections.put(connectionId, session);
channels.put(connectionId, channel);
log.info("SSH连接建立成功: {}@{}:{}", username, host, port);
return connectionId;
} catch (JSchException e) {
log.error("SSH连接失败: {}", e.getMessage());
thrownewRuntimeException("SSH连接失败: " + e.getMessage());
}
}
/**
* 获取SSH通道
*/
public ChannelShell getChannel(String connectionId) {
return channels.get(connectionId);
}
/**
* 获取SSH会话
*/
public Session getSession(String connectionId) {
return connections.get(connectionId);
}
/**
* 关闭SSH连接
*/
publicvoidcloseConnection(String connectionId) {
ChannelShellchannel= channels.remove(connectionId);
if (channel != null && channel.isConnected()) {
channel.disconnect();
}
Sessionsession= connections.remove(connectionId);
if (session != null && session.isConnected()) {
session.disconnect();
}
log.info("SSH连接已关闭: {}", connectionId);
}
/**
* 检查连接状态
*/
publicbooleanisConnected(String connectionId) {
Sessionsession= connections.get(connectionId);
return session != null && session.isConnected();
}
}
-
WebSocket配置
@Configuration
@EnableWebSocket
publicclassWebSocketConfigimplementsWebSocketConfigurer {
@Autowired
private SSHWebSocketHandler sshWebSocketHandler;
@Override
publicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(sshWebSocketHandler, "/ssh")
.setAllowedOriginPatterns("*"); // 生产环境中应该限制域名
}
}
-
WebSocket处理器
@Component
@Slf4j
publicclassSSHWebSocketHandlerextendsTextWebSocketHandler {
@Autowired
private SSHConnectionManager connectionManager;
privatefinal Map<WebSocketSession, String> sessionConnections = newConcurrentHashMap<>();
privatefinal Map<WebSocketSession, String> sessionUsers = newConcurrentHashMap<>();
// 为每个WebSocket会话添加同步锁
privatefinal Map<WebSocketSession, Object> sessionLocks = newConcurrentHashMap<>();
@Override
publicvoidafterConnectionEstablished(WebSocketSession session) {
log.info("WebSocket连接建立: {}", session.getId());
// 为每个会话创建同步锁
sessionLocks.put(session, newObject());
}
@Override
protectedvoidhandleTextMessage(WebSocketSession session, TextMessage message)throws Exception {
try {
Stringpayload= message.getPayload();
ObjectMappermapper=newObjectMapper();
JsonNodejsonNode= mapper.readTree(payload);
Stringtype= jsonNode.get("type").asText();
switch (type) {
case"connect":
handleConnect(session, jsonNode);
break;
case"command":
handleCommand(session, jsonNode);
break;
case"resize":
handleResize(session, jsonNode);
break;
case"disconnect":
handleDisconnect(session);
break;
default:
log.warn("未知的消息类型: {}", type);
}
} catch (Exception e) {
log.error("处理WebSocket消息失败", e);
sendError(session, "处理消息失败: " + e.getMessage());
}
}
/**
* 处理SSH连接请求
*/
privatevoidhandleConnect(WebSocketSession session, JsonNode jsonNode) {
try {
Stringhost= jsonNode.get("host").asText();
intport= jsonNode.get("port").asInt(22);
Stringusername= jsonNode.get("username").asText();
Stringpassword= jsonNode.get("password").asText();
booleanenableCollaboration= jsonNode.has("enableCollaboration") &&
jsonNode.get("enableCollaboration").asBoolean();
// 存储用户信息
sessionUsers.put(session, username);
// 建立SSH连接
StringconnectionId= connectionManager.createConnection(host, port, username, password);
sessionConnections.put(session, connectionId);
// 启动SSH通道
ChannelShellchannel= connectionManager.getChannel(connectionId);
startSSHChannel(session, channel);
// 发送连接成功消息
Map<String, Object> response = newHashMap<>();
response.put("type", "connected");
response.put("message", "SSH连接建立成功");
sendMessage(session, response);
} catch (Exception e) {
log.error("建立SSH连接失败", e);
sendError(session, "连接失败: " + e.getMessage());
}
}
/**
* 处理命令执行请求
*/
privatevoidhandleCommand(WebSocketSession session, JsonNode jsonNode) {
StringconnectionId= sessionConnections.get(session);
if (connectionId == null) {
sendError(session, "SSH连接未建立");
return;
}
Stringcommand= jsonNode.get("command").asText();
ChannelShellchannel= connectionManager.getChannel(connectionId);
Stringusername= sessionUsers.get(session);
if (channel != null && channel.isConnected()) {
try {
// 发送命令到SSH通道
OutputStreamout= channel.getOutputStream();
out.write(command.getBytes());
out.flush();
} catch (IOException e) {
log.error("发送SSH命令失败", e);
sendError(session, "命令执行失败");
}
}
}
/**
* 启动SSH通道并处理输出
*/
privatevoidstartSSHChannel(WebSocketSession session, ChannelShell channel) {
try {
// 连接通道
channel.connect();
// 处理SSH输出
InputStreamin= channel.getInputStream();
// 在单独的线程中读取SSH输出
newThread(() -> {
byte[] buffer = newbyte[4096];
try {
while (channel.isConnected() && session.isOpen()) {
if (in.available() > 0) {
intlen= in.read(buffer);
if (len > 0) {
Stringoutput=newString(buffer, 0, len, "UTF-8");
// 发送给当前会话
sendMessage(session, Map.of(
"type", "output",
"data", output
));
}
} else {
// 没有数据时短暂休眠,避免CPU占用过高
Thread.sleep(10);
}
}
} catch (IOException | InterruptedException e) {
log.warn("SSH输出读取中断: {}", e.getMessage());
}
}, "SSH-Output-Reader-" + session.getId()).start();
} catch (JSchException | IOException e) {
log.error("启动SSH通道失败", e);
sendError(session, "通道启动失败: " + e.getMessage());
}
}
/**
* 处理终端大小调整
*/
privatevoidhandleResize(WebSocketSession session, JsonNode jsonNode) {
StringconnectionId= sessionConnections.get(session);
if (connectionId != null) {
ChannelShellchannel= connectionManager.getChannel(connectionId);
if (channel != null) {
try {
intcols= jsonNode.get("cols").asInt();
introws= jsonNode.get("rows").asInt();
channel.setPtySize(cols, rows, cols * 8, rows * 16);
} catch (Exception e) {
log.warn("调整终端大小失败", e);
}
}
}
}
/**
* 处理断开连接
*/
privatevoidhandleDisconnect(WebSocketSession session) {
StringconnectionId= sessionConnections.remove(session);
Stringusername= sessionUsers.remove(session);
if (connectionId != null) {
connectionManager.closeConnection(connectionId);
}
// 清理锁资源
sessionLocks.remove(session);
}
@Override
publicvoidafterConnectionClosed(WebSocketSession session, CloseStatus status) {
handleDisconnect(session);
log.info("WebSocket连接关闭: {}", session.getId());
}
/**
* 发送消息到WebSocket客户端(线程安全)
*/
privatevoidsendMessage(WebSocketSession session, Object message) {
Objectlock= sessionLocks.get(session);
if (lock == null) return;
synchronized (lock) {
try {
if (session.isOpen()) {
ObjectMappermapper=newObjectMapper();
Stringjson= mapper.writeValueAsString(message);
session.sendMessage(newTextMessage(json));
}
} catch (Exception e) {
log.error("发送WebSocket消息失败", e);
}
}
}
/**
* 发送错误消息
*/
privatevoidsendError(WebSocketSession session, String error) {
sendMessage(session, Map.of(
"type", "error",
"message", error
));
}
/**
* 从会话中获取用户信息
*/
private String getUserFromSession(WebSocketSession session) {
// 简化实现,实际应用中可以从session中获取认证用户信息
return"anonymous";
}
/**
* 从会话中获取主机信息
*/
private String getHostFromSession(WebSocketSession session) {
// 简化实现,实际应用中可以保存连接信息
return"unknown";
}
}
-
服务器信息管理
@Component
publicclassServerConfig {
private Long id;
private String name;
private String host;
private Integer port;
private String username;
private String password;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
// 构造函数、getter和setter省略
}
@Repository
publicclassServerRepository {
@Autowired
private JdbcTemplate jdbcTemplate;
privatefinalStringINSERT_SERVER="""
INSERT INTO servers (name, host, port, username, password, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
""";
privatefinalStringSELECT_ALL_SERVERS="""
SELECT id, name, host, port, username, password, created_at, updated_at
FROM servers ORDER BY created_at DESC
""";
privatefinalStringSELECT_SERVER_BY_ID="""
SELECT id, name, host, port, username, password, created_at, updated_at
FROM servers WHERE id = ?
""";
privatefinalStringUPDATE_SERVER="""
UPDATE servers SET name=?, host=?, port=?, username=?, password=?, updated_at=?
WHERE id=?
""";
privatefinalStringDELETE_SERVER="DELETE FROM servers WHERE id = ?";
public Long saveServer(ServerConfig server) {
KeyHolderkeyHolder=newGeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatementps= connection.prepareStatement(INSERT_SERVER, Statement.RETURN_GENERATED_KEYS);
ps.setString(1, server.getName());
ps.setString(2, server.getHost());
ps.setInt(3, server.getPort());
ps.setString(4, server.getUsername());
ps.setString(5, server.getPassword());
ps.setTimestamp(6, Timestamp.valueOf(LocalDateTime.now()));
ps.setTimestamp(7, Timestamp.valueOf(LocalDateTime.now()));
return ps;
}, keyHolder);
return keyHolder.getKey().longValue();
}
public List<ServerConfig> findAllServers() {
return jdbcTemplate.query(SELECT_ALL_SERVERS, this::mapRowToServer);
}
public Optional<ServerConfig> findServerById(Long id) {
try {
ServerConfigserver= jdbcTemplate.queryForObject(SELECT_SERVER_BY_ID,
this::mapRowToServer, id);
return Optional.ofNullable(server);
} catch (EmptyResultDataAccessException e) {
return Optional.empty();
}
}
publicvoidupdateServer(ServerConfig server) {
jdbcTemplate.update(UPDATE_SERVER,
server.getName(),
server.getHost(),
server.getPort(),
server.getUsername(),
server.getPassword(),
Timestamp.valueOf(LocalDateTime.now()),
server.getId());
}
publicvoiddeleteServer(Long id) {
jdbcTemplate.update(DELETE_SERVER, id);
}
private ServerConfig mapRowToServer(ResultSet rs, int rowNum)throws SQLException {
ServerConfigserver=newServerConfig();
server.setId(rs.getLong("id"));
server.setName(rs.getString("name"));
server.setHost(rs.getString("host"));
server.setPort(rs.getInt("port"));
server.setUsername(rs.getString("username"));
server.setPassword(rs.getString("password"));
server.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
server.setUpdatedAt(rs.getTimestamp("updated_at").toLocalDateTime());
return server;
}
}
@Service
publicclassServerService {
@Autowired
private ServerRepository serverRepository;
public Long saveServer(ServerConfig server) {
// 密码加密存储(生产环境建议)
// server.setPassword(encryptPassword(server.getPassword()));
return serverRepository.saveServer(server);
}
public List<ServerConfig> getAllServers() {
List<ServerConfig> servers = serverRepository.findAllServers();
// 不返回密码信息到前端
servers.forEach(server -> server.setPassword(null));
return servers;
}
public Optional<ServerConfig> getServerById(Long id) {
return serverRepository.findServerById(id);
}
publicvoiddeleteServer(Long id) {
serverRepository.deleteServer(id);
}
}
-
文件传输功能
@Service
@Slf4j
publicclassFileTransferService {
/**
* 上传文件到远程服务器
*/
publicvoiduploadFile(ServerConfig server, MultipartFile file, String remotePath)throws Exception {
Sessionsession=null;
ChannelSftpsftpChannel=null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
// 确保远程目录存在
createRemoteDirectory(sftpChannel, remotePath);
// 上传文件
StringremoteFilePath= remotePath + "/" + file.getOriginalFilename();
try (InputStreaminputStream= file.getInputStream()) {
sftpChannel.put(inputStream, remoteFilePath);
}
log.info("文件上传成功: {} -> {}", file.getOriginalFilename(), remoteFilePath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 从远程服务器下载文件
*/
publicbyte[] downloadFile(ServerConfig server, String remoteFilePath) throws Exception {
Sessionsession=null;
ChannelSftpsftpChannel=null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
try (ByteArrayOutputStreamoutputStream=newByteArrayOutputStream();
InputStreaminputStream= sftpChannel.get(remoteFilePath)) {
byte[] buffer = newbyte[8192];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
log.info("文件下载成功: {}", remoteFilePath);
return outputStream.toByteArray();
}
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 列出远程目录内容
*/
@SuppressWarnings("unchecked")
public List<FileInfo> listDirectory(ServerConfig server, String remotePath)throws Exception {
Sessionsession=null;
ChannelSftpsftpChannel=null;
List<FileInfo> files = newArrayList<>();
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
Vector<ChannelSftp.LsEntry> entries = sftpChannel.ls(remotePath);
for (ChannelSftp.LsEntry entry : entries) {
Stringfilename= entry.getFilename();
if (!filename.equals(".") && !filename.equals("..")) {
SftpATTRSattrs= entry.getAttrs();
files.add(newFileInfo(
filename,
attrs.isDir(),
attrs.getSize(),
attrs.getMTime() * 1000L, // Convert to milliseconds
getPermissionString(attrs.getPermissions())
));
}
}
log.info("目录列表获取成功: {}, 文件数: {}", remotePath, files.size());
return files;
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 创建远程目录
*/
publicvoidcreateRemoteDirectory(ServerConfig server, String remotePath)throws Exception {
Sessionsession=null;
ChannelSftpsftpChannel=null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
createRemoteDirectory(sftpChannel, remotePath);
log.info("远程目录创建成功: {}", remotePath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 删除远程文件或目录
*/
publicvoiddeleteRemoteFile(ServerConfig server, String remotePath, boolean isDirectory)throws Exception {
Sessionsession=null;
ChannelSftpsftpChannel=null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
if (isDirectory) {
sftpChannel.rmdir(remotePath);
} else {
sftpChannel.rm(remotePath);
}
log.info("远程文件删除成功: {}", remotePath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 重命名远程文件
*/
publicvoidrenameRemoteFile(ServerConfig server, String oldPath, String newPath)throws Exception {
Sessionsession=null;
ChannelSftpsftpChannel=null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
sftpChannel.rename(oldPath, newPath);
log.info("文件重命名成功: {} -> {}", oldPath, newPath);
} finally {
closeConnections(sftpChannel, session);
}
}
/**
* 批量上传文件
*/
publicvoiduploadFiles(ServerConfig server, MultipartFile[] files, String remotePath)throws Exception {
Sessionsession=null;
ChannelSftpsftpChannel=null;
try {
session = createSession(server);
sftpChannel = (ChannelSftp) session.openChannel("sftp");
sftpChannel.connect();
// 确保远程目录存在
createRemoteDirectory(sftpChannel, remotePath);
for (MultipartFile file : files) {
if (!file.isEmpty()) {
StringremoteFilePath= remotePath + "/" + file.getOriginalFilename();
try (InputStreaminputStream= file.getInputStream()) {
sftpChannel.put(inputStream, remoteFilePath);
log.info("文件上传成功: {}", file.getOriginalFilename());
}
}
}
log.info("批量上传完成,共上传 {} 个文件", files.length);
} finally {
closeConnections(sftpChannel, session);
}
}
// 私有辅助方法
private Session createSession(ServerConfig server)throws JSchException {
JSchjsch=newJSch();
Sessionsession= jsch.getSession(server.getUsername(), server.getHost(), server.getPort());
session.setPassword(server.getPassword());
Propertiesconfig=newProperties();
config.put("StrictHostKeyChecking", "no");
config.put("PreferredAuthentications", "password");
session.setConfig(config);
session.connect(10000); // 10秒超时
return session;
}
privatevoidcreateRemoteDirectory(ChannelSftp sftpChannel, String remotePath) {
try {
String[] pathParts = remotePath.split("/");
StringcurrentPath="";
for (String part : pathParts) {
if (!part.isEmpty()) {
currentPath += "/" + part;
try {
sftpChannel.mkdir(currentPath);
} catch (SftpException e) {
log.error(e.getMessage(),e);
}
}
}
} catch (Exception e) {
log.warn("创建远程目录失败: {}", e.getMessage());
}
}
privatevoidcloseConnections(ChannelSftp sftpChannel, Session session) {
if (sftpChannel != null && sftpChannel.isConnected()) {
sftpChannel.disconnect();
}
if (session != null && session.isConnected()) {
session.disconnect();
}
}
private String getPermissionString(int permissions) {
StringBuildersb=newStringBuilder();
// Owner permissions
sb.append((permissions & 0400) != 0 ? 'r' : '-');
sb.append((permissions & 0200) != 0 ? 'w' : '-');
sb.append((permissions & 0100) != 0 ? 'x' : '-');
// Group permissions
sb.append((permissions & 0040) != 0 ? 'r' : '-');
sb.append((permissions & 0020) != 0 ? 'w' : '-');
sb.append((permissions & 0010) != 0 ? 'x' : '-');
// Others permissions
sb.append((permissions & 0004) != 0 ? 'r' : '-');
sb.append((permissions & 0002) != 0 ? 'w' : '-');
sb.append((permissions & 0001) != 0 ? 'x' : '-');
return sb.toString();
}
// 文件信息内部类
publicstaticclassFileInfo {
private String name;
privateboolean isDirectory;
privatelong size;
privatelong lastModified;
private String permissions;
publicFileInfo(String name, boolean isDirectory, long size, long lastModified, String permissions) {
this.name = name;
this.isDirectory = isDirectory;
this.size = size;
this.lastModified = lastModified;
this.permissions = permissions;
}
// Getters
public String getName() { return name; }
publicbooleanisDirectory() { return isDirectory; }
publiclonggetSize() { return size; }
publiclonggetLastModified() { return lastModified; }
public String getPermissions() { return permissions; }
}
}
-
REST API控制器
@RestController
@RequestMapping("/api/servers")
publicclassServerController {
@Autowired
private ServerService serverService;
/**
* 获取服务器列表
*/
@GetMapping
public ResponseEntity<List<ServerConfig>> getServers() {
List<ServerConfig> servers = serverService.getAllServers();
return ResponseEntity.ok(servers);
}
/**
* 添加服务器
*/
@PostMapping
public ResponseEntity<Map<String, Object>> addServer(@RequestBody ServerConfig server) {
try {
LongserverId= serverService.saveServer(server);
return ResponseEntity.ok(Map.of("success", true, "id", serverId));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
}
}
/**
* 删除服务器
*/
@DeleteMapping("/{id}")
public ResponseEntity<Map<String, Object>> deleteServer(@PathVariable Long id) {
try {
serverService.deleteServer(id);
return ResponseEntity.ok(Map.of("success", true));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("success", false, "message", e.getMessage()));
}
}
/**
* 测试服务器连接
*/
@PostMapping("/test")
public ResponseEntity<Map<String, Object>> testConnection(@RequestBody ServerConfig server) {
try {
// 简单的连接测试
JSchjsch=newJSch();
Sessionsession= jsch.getSession(server.getUsername(), server.getHost(), server.getPort());
session.setPassword(server.getPassword());
session.setConfig("StrictHostKeyChecking", "no");
session.connect(5000); // 5秒超时
session.disconnect();
return ResponseEntity.ok(Map.of("success", true, "message", "连接测试成功"));
} catch (Exception e) {
return ResponseEntity.ok(Map.of("success", false, "message", "连接测试失败: " + e.getMessage()));
}
}
}
-
前端实现
<!DOCTYPE html>
<htmllang="zh-CN">
<head>
<metacharset="UTF-8">
<metaname="viewport"content="width=device-width, initial-scale=1.0">
<title>Web SSH 企业版客户端</title>
<!-- 引入xterm.js -->
<linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" />
<scriptsrc="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script>
<scriptsrc="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script>
<!-- 引入Font Awesome图标 -->
<linkrel="stylesheet"href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css">
<style>
/* 考虑篇幅,此处忽略样式代码 */
</style>
</head>
<body>
<divclass="main-container">
<!-- 侧边栏 -->
<divclass="sidebar"id="sidebar">
<divclass="sidebar-header">
<divclass="sidebar-title">
<iclass="fas fa-terminal"></i>
<spanid="sidebarTitle">Web SSH</span>
</div>
<buttonclass="sidebar-toggle"onclick="toggleSidebar()">
<iclass="fas fa-bars"></i>
</button>
</div>
<navclass="sidebar-nav">
<divclass="nav-item active"onclick="switchPage('ssh')">
<iclass="fas fa-terminal nav-icon"></i>
<spanclass="nav-text">SSH连接</span>
</div>
<divclass="nav-item"onclick="switchPage('files')">
<iclass="fas fa-folder nav-icon"></i>
<spanclass="nav-text">文件管理</span>
</div>
</nav>
</div>
<!-- 主内容区 -->
<divclass="main-content">
<!-- SSH连接页面 -->
<divclass="page-content active"id="page-ssh">
<divclass="content-header">
<h1class="content-title">SSH连接管理</h1>
<divclass="action-buttons">
<buttonclass="btn btn-secondary"onclick="loadSavedServers()">
<iclass="fas fa-download"></i> 加载保存的服务器
</button>
</div>
</div>
<!-- 连接面板 -->
<divclass="connection-panel">
<divclass="connection-form">
<divclass="form-group">
<labelfor="savedServers">快速连接</label>
<selectid="savedServers"onchange="loadServerConfig()">
<optionvalue="">选择已保存的服务器...</option>
</select>
</div>
<divclass="form-group">
<labelfor="host">服务器地址</label>
<inputtype="text"id="host"placeholder="192.168.1.100 或 example.com"value="localhost">
</div>
<divclass="form-group">
<labelfor="port">端口</label>
<inputtype="number"id="port"placeholder="22"value="22">
</div>
<divclass="form-group">
<labelfor="username">用户名</label>
<inputtype="text"id="username"placeholder="root">
</div>
<divclass="form-group">
<labelfor="password">密码</label>
<inputtype="password"id="password"placeholder="密码">
</div>
<divclass="form-group">
<labelfor="serverName">服务器名称(可选)</label>
<inputtype="text"id="serverName"placeholder="给这个连接起个名字">
</div>
</div>
<divclass="checkbox-group">
<inputtype="checkbox"id="saveServer">
<labelfor="saveServer">保存此服务器配置</label>
</div>
<divstyle="margin-top: 20px; display: flex; gap: 10px;">
<buttonclass="btn btn-primary"onclick="connectSSH()">
<iclass="fas fa-plug"></i> 连接
</button>
<buttonclass="btn btn-success"onclick="testConnection()"id="testBtn">
<iclass="fas fa-check"></i> 测试连接
</button>
<buttonclass="btn btn-danger"onclick="disconnectSSH()"disabledid="disconnectBtn">
<iclass="fas fa-times"></i> 断开连接
</button>
</div>
<!-- 状态提示 -->
<divid="alertContainer"></div>
</div>
<!-- 终端容器 -->
<divclass="terminal-container hidden"id="terminalContainer">
<!-- Tab栏 -->
<divclass="terminal-tabs"id="terminalTabs">
<!-- tabs will be added dynamically -->
</div>
<!-- Terminal内容区 -->
<divclass="terminal-content"id="terminalContent">
<!-- terminals will be added dynamically -->
</div>
<divclass="status-bar">
<spanid="statusBar">就绪</span>
<spanid="terminalStats">行: 24, 列: 80</span>
</div>
</div>
</div>
<!-- 文件管理页面 -->
<divclass="page-content"id="page-files">
<divclass="content-header">
<h1class="content-title">文件管理器</h1>
<divclass="action-buttons">
<buttonclass="btn btn-primary"onclick="showUploadModal()">
<iclass="fas fa-upload"></i> 上传文件
</button>
<buttonclass="btn btn-success"onclick="createFolder()">
<iclass="fas fa-folder-plus"></i> 新建文件夹
</button>
</div>
</div>
<divclass="file-manager"id="fileManager">
<divclass="file-manager-header">
<divclass="file-path">
<buttonclass="btn btn-secondary"onclick="navigateUp()">
<iclass="fas fa-arrow-up"></i>
</button>
<inputtype="text"id="currentPath"value="/"readonly>
<buttonclass="btn btn-secondary"onclick="refreshFiles()">
<iclass="fas fa-sync"></i>
</button>
</div>
<divclass="file-actions">
<selectid="fileServerSelect"onchange="switchFileServer()"style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; background: white;">
<optionvalue="">选择服务器...</option>
</select>
</div>
</div>
<divclass="file-grid"id="fileGrid">
<divclass="alert alert-info">
请先选择一个服务器来浏览文件
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 弹窗 -->
<!-- 文件上传弹窗 -->
<divclass="modal"id="uploadModal">
<divclass="modal-content">
<divclass="modal-header">
<h3class="modal-title">上传文件</h3>
<buttonclass="modal-close"onclick="closeModal('uploadModal')">×</button>
</div>
<div>
<divclass="form-group">
<labelfor="uploadFiles">选择文件</label>
<inputtype="file"id="uploadFiles"multiple>
</div>
<divclass="form-group">
<labelfor="uploadPath">上传路径</label>
<inputtype="text"id="uploadPath"value="/"required>
</div>
<divstyle="text-align: right; margin-top: 20px;">
<buttontype="button"class="btn btn-secondary"onclick="closeModal('uploadModal')">取消</button>
<buttontype="button"class="btn btn-primary"onclick="handleUpload(); return false;">上传</button>
</div>
</div>
</div>
</div>
<!-- JavaScript代码 -->
<scriptsrc="js/webssh-multisession.js"></script>
</body>
</html>
-
数据库初始化
-- 服务器配置表
CREATE TABLE IF NOTEXISTS servers (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL COMMENT '服务器名称',
host VARCHAR(255) NOT NULL COMMENT '服务器地址',
port INTDEFAULT22 COMMENT 'SSH端口',
username VARCHAR(100) NOT NULL COMMENT '用户名',
password VARCHAR(500) NOT NULL COMMENT '密码(建议加密存储)',
created_at TIMESTAMPDEFAULTCURRENT_TIMESTAMP,
updated_at TIMESTAMPDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP
);
-- 删除现有测试数据(避免重复插入)
DELETEFROM servers;
-- 插入测试服务器数据
INSERT INTO servers (name, host, port, username, password) VALUES
('本地测试服务器', 'localhost', 22, 'root', 'password'),
('开发服务器', '192.168.1.100', 22, 'dev', 'devpass'),
('测试服务器', '192.168.1.101', 22, 'test', 'testpass'),
('生产服务器', '192.168.1.200', 22, 'prod', 'prodpass');
# 生产环境配置
spring:
datasource:
url:jdbc:mysql://localhost:3306/app_config?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
driver-class-name:com.mysql.cj.jdbc.Driver
username:root
password:root
hikari:
maximum-pool-size:20
minimum-idle:5
connection-timeout:30000
server:
port:8080
servlet:
context-path:/
compression:
enabled:true
mime-types:text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/json
tomcat:
max-connections:200
threads:
max:100
min-spare:10
logging:
level:
root:INFO
com.example.webssh:DEBUG
file:
name:logs/webssh.log
pattern:
file:"%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
# 自定义配置
webssh:
ssh:
connection-timeout:30000
session-timeout:1800000
max-connections-per-user:10
file:
upload-max-size:100MB
temp-dir:/tmp/webssh-uploads
collaboration:
enabled:true
max-participants:10
session-timeout:3600000


-
缓存优化
@Service
@EnableCaching
publicclassCachedServerService {
@Cacheable(value = "servers", key = "#username")
public List<Server> getUserServers(String username) {
return serverRepository.findByCreatedBy(username);
}
@CacheEvict(value = "servers", key = "#username")
publicvoidclearUserServersCache(String username) {
// 清理缓存
}
}
-
安全增强
@Component
publicclassSecurityEnhancements {
/**
* 密码加密存储
*/
public String encryptPassword(String password) {
try {
Ciphercipher= Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, getSecretKey());
byte[] encryptedPassword = cipher.doFinal(password.getBytes());
return Base64.getEncoder().encodeToString(encryptedPassword);
} catch (Exception e) {
thrownewRuntimeException("密码加密失败", e);
}
}
/**
* 操作审计
*/
@EventListener
publicvoidhandleSSHCommand(SSHCommandEvent event) {
auditService.logSSHOperation(
event.getUsername(),
event.getServerHost(),
event.getCommand(),
event.getTimestamp()
);
}
}


摘抄自网络,便于检索查找。

浙公网安备 33010602011771号