当前位置: 首页 > news >正文

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

在日常运维工作中,我们经常需要通过SSH连接多台服务器进行操作。传统的SSH客户端虽然功能完善,但在某些场景下存在一些限制。

本文将介绍如何使用Spring Boot开发一个Web SSH客户端,让用户可以通过浏览器直接连接和操作远程服务器。

这种方案在企业内部运维管理、临时访问、移动办公等场景中具有一定的实用价值。

Web SSH客户端的应用场景

相比传统SSH客户端,Web SSH在以下场景中具有实际价值:

传统SSH客户端的局限性

客户端依赖:需要在每台设备上安装SSH客户端软件

统一管理困难:难以统一管理服务器连接配置和用户权限

操作审计不便:缺乏统一的操作日志记录和管理

移动设备支持有限:在手机、平板上操作体验较差

防火墙限制:某些网络环境下SSH端口可能被阻止

Web SSH的实际优势

无需安装客户端:通过浏览器即可使用,降低部署成本

统一权限管理:可以集中管理用户的服务器访问权限

操作记录可追溯:所有SSH操作都可以记录和审计

移动设备友好:在移动设备上也能提供相对较好的使用体验

绕过端口限制:通过HTTP/HTTPS端口提供服务

这些特点使得Web SSH在企业内部运维平台、云服务管理后台、教学环境等场景中有实际的应用价值。

技术方案设计

本文将基于以下技术栈实现Web SSH客户端:

后端技术选型

Spring Boot 3.x:提供Web框架和自动配置能力

JSch库:Java实现的SSH2客户端,用于建立SSH连接

WebSocket:实现浏览器与服务器间的双向实时通信

Spring JdbcTemplate:轻量级数据库操作,存储服务器配置和操作记录

前端技术选型

HTML + JavaScript:构建Web界面,无需复杂框架

Xterm.js:在浏览器中模拟终端界面

WebSocket API:与后端建立实时通信连接

系统架构

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

核心流程:用户在浏览器中输入SSH连接信息,Spring Boot后端使用JSch库建立SSH连接,通过WebSocket将终端数据实时传输到前端Xterm.js组件进行显示。

核心功能实现

1. 项目初始化

首先创建Spring Boot项目并添加必要的依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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>

2. SSH连接管理器

创建SSH连接管理器,负责建立和维护SSH连接:

@Component
@Slf4j
public class SSHConnectionManager {private final Map<String, Session> connections = new ConcurrentHashMap<>();private final Map<String, ChannelShell> channels = new ConcurrentHashMap<>();/*** 建立SSH连接*/public String createConnection(String host, int port, String username, String password) {try {JSch jsch = new JSch();Session session = jsch.getSession(username, host, port);// 配置连接参数Properties config = new Properties();config.put("StrictHostKeyChecking", "no");config.put("PreferredAuthentications", "password");session.setConfig(config);session.setPassword(password);// 建立连接session.connect(30000); // 30秒超时// 创建Shell通道ChannelShell channel = (ChannelShell) session.openChannel("shell");channel.setPty(true);channel.setPtyType("xterm", 80, 24, 640, 480);// 生成连接IDString connectionId = 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());throw new RuntimeException("SSH连接失败: " + e.getMessage());}}/*** 获取SSH通道*/public ChannelShell getChannel(String connectionId) {return channels.get(connectionId);}/*** 获取SSH会话*/public Session getSession(String connectionId) {return connections.get(connectionId);}/*** 关闭SSH连接*/public void closeConnection(String connectionId) {ChannelShell channel = channels.remove(connectionId);if (channel != null && channel.isConnected()) {channel.disconnect();}Session session = connections.remove(connectionId);if (session != null && session.isConnected()) {session.disconnect();}log.info("SSH连接已关闭: {}", connectionId);}/*** 检查连接状态*/public boolean isConnected(String connectionId) {Session session = connections.get(connectionId);return session != null && session.isConnected();}
}

3. WebSocket配置

配置WebSocket,实现浏览器与服务器的实时通信:

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {@Autowiredprivate SSHWebSocketHandler sshWebSocketHandler;@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(sshWebSocketHandler, "/ssh").setAllowedOriginPatterns("*"); // 生产环境中应该限制域名}
}

4. WebSocket处理器

创建WebSocket处理器,处理SSH命令的发送和接收:

@Component
@Slf4j
public class SSHWebSocketHandler extends TextWebSocketHandler {@Autowiredprivate SSHConnectionManager connectionManager;private final Map<WebSocketSession, String> sessionConnections = new ConcurrentHashMap<>();private final Map<WebSocketSession, String> sessionUsers = new ConcurrentHashMap<>();// 为每个WebSocket会话添加同步锁private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>();@Overridepublic void afterConnectionEstablished(WebSocketSession session) {log.info("WebSocket连接建立: {}", session.getId());// 为每个会话创建同步锁sessionLocks.put(session, new Object());}@Overrideprotected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {try {String payload = message.getPayload();ObjectMapper mapper = new ObjectMapper();JsonNode jsonNode = mapper.readTree(payload);String type = 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连接请求*/private void handleConnect(WebSocketSession session, JsonNode jsonNode) {try {String host = jsonNode.get("host").asText();int port = jsonNode.get("port").asInt(22);String username = jsonNode.get("username").asText();String password = jsonNode.get("password").asText();boolean enableCollaboration = jsonNode.has("enableCollaboration") && jsonNode.get("enableCollaboration").asBoolean();// 存储用户信息sessionUsers.put(session, username);// 建立SSH连接String connectionId = connectionManager.createConnection(host, port, username, password);sessionConnections.put(session, connectionId);// 启动SSH通道ChannelShell channel = connectionManager.getChannel(connectionId);startSSHChannel(session, channel);// 发送连接成功消息Map<String, Object> response = new HashMap<>();response.put("type", "connected");response.put("message", "SSH连接建立成功");sendMessage(session, response);} catch (Exception e) {log.error("建立SSH连接失败", e);sendError(session, "连接失败: " + e.getMessage());}}/*** 处理命令执行请求*/private void handleCommand(WebSocketSession session, JsonNode jsonNode) {String connectionId = sessionConnections.get(session);if (connectionId == null) {sendError(session, "SSH连接未建立");return;}String command = jsonNode.get("command").asText();ChannelShell channel = connectionManager.getChannel(connectionId);String username = sessionUsers.get(session);if (channel != null && channel.isConnected()) {try {// 发送命令到SSH通道OutputStream out = channel.getOutputStream();out.write(command.getBytes());out.flush();} catch (IOException e) {log.error("发送SSH命令失败", e);sendError(session, "命令执行失败");}}}/*** 启动SSH通道并处理输出*/private void startSSHChannel(WebSocketSession session, ChannelShell channel) {try {// 连接通道channel.connect();// 处理SSH输出InputStream in = channel.getInputStream();// 在单独的线程中读取SSH输出new Thread(() -> {byte[] buffer = new byte[4096];try {while (channel.isConnected() && session.isOpen()) {if (in.available() > 0) {int len = in.read(buffer);if (len > 0) {String output = new String(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());}}/*** 处理终端大小调整*/private void handleResize(WebSocketSession session, JsonNode jsonNode) {String connectionId = sessionConnections.get(session);if (connectionId != null) {ChannelShell channel = connectionManager.getChannel(connectionId);if (channel != null) {try {int cols = jsonNode.get("cols").asInt();int rows = jsonNode.get("rows").asInt();channel.setPtySize(cols, rows, cols * 8, rows * 16);} catch (Exception e) {log.warn("调整终端大小失败", e);}}}}/*** 处理断开连接*/private void handleDisconnect(WebSocketSession session) {String connectionId = sessionConnections.remove(session);String username = sessionUsers.remove(session);if (connectionId != null) {connectionManager.closeConnection(connectionId);}// 清理锁资源sessionLocks.remove(session);}@Overridepublic void afterConnectionClosed(WebSocketSession session, CloseStatus status) {handleDisconnect(session);log.info("WebSocket连接关闭: {}", session.getId());}/*** 发送消息到WebSocket客户端(线程安全)*/private void sendMessage(WebSocketSession session, Object message) {Object lock = sessionLocks.get(session);if (lock == null) return;synchronized (lock) {try {if (session.isOpen()) {ObjectMapper mapper = new ObjectMapper();String json = mapper.writeValueAsString(message);session.sendMessage(new TextMessage(json));}} catch (Exception e) {log.error("发送WebSocket消息失败", e);}}}/*** 发送错误消息*/private void sendError(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";}
}

5. 服务器信息管理

使用JdbcTemplate进行服务器配置的数据操作:

@Component
public class ServerConfig {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
public class ServerRepository {@Autowiredprivate JdbcTemplate jdbcTemplate;private final String INSERT_SERVER = """INSERT INTO servers (name, host, port, username, password, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)""";private final String SELECT_ALL_SERVERS = """SELECT id, name, host, port, username, password, created_at, updated_at FROM servers ORDER BY created_at DESC""";private final String SELECT_SERVER_BY_ID = """SELECT id, name, host, port, username, password, created_at, updated_at FROM servers WHERE id = ?""";private final String UPDATE_SERVER = """UPDATE servers SET name=?, host=?, port=?, username=?, password=?, updated_at=? WHERE id=?""";private final String DELETE_SERVER = "DELETE FROM servers WHERE id = ?";public Long saveServer(ServerConfig server) {KeyHolder keyHolder = new GeneratedKeyHolder();jdbcTemplate.update(connection -> {PreparedStatement ps = 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 {ServerConfig server = jdbcTemplate.queryForObject(SELECT_SERVER_BY_ID, this::mapRowToServer, id);return Optional.ofNullable(server);} catch (EmptyResultDataAccessException e) {return Optional.empty();}}public void updateServer(ServerConfig server) {jdbcTemplate.update(UPDATE_SERVER,server.getName(),server.getHost(), server.getPort(),server.getUsername(),server.getPassword(),Timestamp.valueOf(LocalDateTime.now()),server.getId());}public void deleteServer(Long id) {jdbcTemplate.update(DELETE_SERVER, id);}private ServerConfig mapRowToServer(ResultSet rs, int rowNum) throws SQLException {ServerConfig server = new ServerConfig();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
public class ServerService {@Autowiredprivate 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);}public void deleteServer(Long id) {serverRepository.deleteServer(id);}
}

6. 文件传输功能

集成SFTP文件传输

@Service
@Slf4j
public class FileTransferService {/*** 上传文件到远程服务器*/public void uploadFile(ServerConfig server, MultipartFile file, String remotePath) throws Exception {Session session = null;ChannelSftp sftpChannel = null;try {session = createSession(server);sftpChannel = (ChannelSftp) session.openChannel("sftp");sftpChannel.connect();// 确保远程目录存在createRemoteDirectory(sftpChannel, remotePath);// 上传文件String remoteFilePath = remotePath + "/" + file.getOriginalFilename();try (InputStream inputStream = file.getInputStream()) {sftpChannel.put(inputStream, remoteFilePath);}log.info("文件上传成功: {} -> {}", file.getOriginalFilename(), remoteFilePath);} finally {closeConnections(sftpChannel, session);}}/*** 从远程服务器下载文件*/public byte[] downloadFile(ServerConfig server, String remoteFilePath) throws Exception {Session session = null;ChannelSftp sftpChannel = null;try {session = createSession(server);sftpChannel = (ChannelSftp) session.openChannel("sftp");sftpChannel.connect();try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();InputStream inputStream = sftpChannel.get(remoteFilePath)) {byte[] buffer = new byte[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 {Session session = null;ChannelSftp sftpChannel = null;List<FileInfo> files = new ArrayList<>();try {session = createSession(server);sftpChannel = (ChannelSftp) session.openChannel("sftp");sftpChannel.connect();Vector<ChannelSftp.LsEntry> entries = sftpChannel.ls(remotePath);for (ChannelSftp.LsEntry entry : entries) {String filename = entry.getFilename();if (!filename.equals(".") && !filename.equals("..")) {SftpATTRS attrs = entry.getAttrs();files.add(new FileInfo(filename,attrs.isDir(),attrs.getSize(),attrs.getMTime() * 1000L, // Convert to millisecondsgetPermissionString(attrs.getPermissions())));}}log.info("目录列表获取成功: {}, 文件数: {}", remotePath, files.size());return files;} finally {closeConnections(sftpChannel, session);}}/*** 创建远程目录*/public void createRemoteDirectory(ServerConfig server, String remotePath) throws Exception {Session session = null;ChannelSftp sftpChannel = null;try {session = createSession(server);sftpChannel = (ChannelSftp) session.openChannel("sftp");sftpChannel.connect();createRemoteDirectory(sftpChannel, remotePath);log.info("远程目录创建成功: {}", remotePath);} finally {closeConnections(sftpChannel, session);}}/*** 删除远程文件或目录*/public void deleteRemoteFile(ServerConfig server, String remotePath, boolean isDirectory) throws Exception {Session session = null;ChannelSftp sftpChannel = 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);}}/*** 重命名远程文件*/public void renameRemoteFile(ServerConfig server, String oldPath, String newPath) throws Exception {Session session = null;ChannelSftp sftpChannel = null;try {session = createSession(server);sftpChannel = (ChannelSftp) session.openChannel("sftp");sftpChannel.connect();sftpChannel.rename(oldPath, newPath);log.info("文件重命名成功: {} -> {}", oldPath, newPath);} finally {closeConnections(sftpChannel, session);}}/*** 批量上传文件*/public void uploadFiles(ServerConfig server, MultipartFile[] files, String remotePath) throws Exception {Session session = null;ChannelSftp sftpChannel = null;try {session = createSession(server);sftpChannel = (ChannelSftp) session.openChannel("sftp");sftpChannel.connect();// 确保远程目录存在createRemoteDirectory(sftpChannel, remotePath);for (MultipartFile file : files) {if (!file.isEmpty()) {String remoteFilePath = remotePath + "/" + file.getOriginalFilename();try (InputStream inputStream = 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 {JSch jsch = new JSch();Session session = jsch.getSession(server.getUsername(), server.getHost(), server.getPort());session.setPassword(server.getPassword());Properties config = new Properties();config.put("StrictHostKeyChecking", "no");config.put("PreferredAuthentications", "password");session.setConfig(config);session.connect(10000); // 10秒超时return session;}private void createRemoteDirectory(ChannelSftp sftpChannel, String remotePath) {try {String[] pathParts = remotePath.split("/");String currentPath = "";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());}}private void closeConnections(ChannelSftp sftpChannel, Session session) {if (sftpChannel != null && sftpChannel.isConnected()) {sftpChannel.disconnect();}if (session != null && session.isConnected()) {session.disconnect();}}private String getPermissionString(int permissions) {StringBuilder sb = new StringBuilder();// Owner permissionssb.append((permissions & 0400) != 0 ? 'r' : '-');sb.append((permissions & 0200) != 0 ? 'w' : '-');sb.append((permissions & 0100) != 0 ? 'x' : '-');// Group permissionssb.append((permissions & 0040) != 0 ? 'r' : '-');sb.append((permissions & 0020) != 0 ? 'w' : '-');sb.append((permissions & 0010) != 0 ? 'x' : '-');// Others permissionssb.append((permissions & 0004) != 0 ? 'r' : '-');sb.append((permissions & 0002) != 0 ? 'w' : '-');sb.append((permissions & 0001) != 0 ? 'x' : '-');return sb.toString();}// 文件信息内部类public static class FileInfo {private String name;private boolean isDirectory;private long size;private long lastModified;private String permissions;public FileInfo(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;}// Getterspublic String getName() { return name; }public boolean isDirectory() { return isDirectory; }public long getSize() { return size; }public long getLastModified() { return lastModified; }public String getPermissions() { return permissions; }}
}

7. REST API控制器

创建REST API来管理服务器配置:

@RestController
@RequestMapping("/api/servers")
public class ServerController {@Autowiredprivate ServerService serverService;/*** 获取服务器列表*/@GetMappingpublic ResponseEntity<List<ServerConfig>> getServers() {List<ServerConfig> servers = serverService.getAllServers();return ResponseEntity.ok(servers);}/*** 添加服务器*/@PostMappingpublic ResponseEntity<Map<String, Object>> addServer(@RequestBody ServerConfig server) {try {Long serverId = 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 {// 简单的连接测试JSch jsch = new JSch();Session session = 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()));}}
}

8. 前端实现

使用纯HTML + JavaScript集成xterm.js

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Web SSH 企业版客户端</title><!-- 引入xterm.js --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css" /><script src="https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js"></script><script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js"></script><!-- 引入Font Awesome图标 --><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fortawesome/fontawesome-free@6.4.0/css/all.min.css"><style>/* 考虑篇幅,此处忽略样式代码 */</style>
</head>
<body><div class="main-container"><!-- 侧边栏 --><div class="sidebar" id="sidebar"><div class="sidebar-header"><div class="sidebar-title"><i class="fas fa-terminal"></i><span id="sidebarTitle">Web SSH</span></div><button class="sidebar-toggle" onclick="toggleSidebar()"><i class="fas fa-bars"></i></button></div><nav class="sidebar-nav"><div class="nav-item active" onclick="switchPage('ssh')"><i class="fas fa-terminal nav-icon"></i><span class="nav-text">SSH连接</span></div><div class="nav-item" onclick="switchPage('files')"><i class="fas fa-folder nav-icon"></i><span class="nav-text">文件管理</span></div></nav></div><!-- 主内容区 --><div class="main-content"><!-- SSH连接页面 --><div class="page-content active" id="page-ssh"><div class="content-header"><h1 class="content-title">SSH连接管理</h1><div class="action-buttons"><button class="btn btn-secondary" onclick="loadSavedServers()"><i class="fas fa-download"></i> 加载保存的服务器</button></div></div><!-- 连接面板 --><div class="connection-panel"><div class="connection-form"><div class="form-group"><label for="savedServers">快速连接</label><select id="savedServers" onchange="loadServerConfig()"><option value="">选择已保存的服务器...</option></select></div><div class="form-group"><label for="host">服务器地址</label><input type="text" id="host" placeholder="192.168.1.100 或 example.com" value="localhost"></div><div class="form-group"><label for="port">端口</label><input type="number" id="port" placeholder="22" value="22"></div><div class="form-group"><label for="username">用户名</label><input type="text" id="username" placeholder="root"></div><div class="form-group"><label for="password">密码</label><input type="password" id="password" placeholder="密码"></div><div class="form-group"><label for="serverName">服务器名称(可选)</label><input type="text" id="serverName" placeholder="给这个连接起个名字"></div></div><div class="checkbox-group"><input type="checkbox" id="saveServer"><label for="saveServer">保存此服务器配置</label></div><div style="margin-top: 20px; display: flex; gap: 10px;"><button class="btn btn-primary" onclick="connectSSH()"><i class="fas fa-plug"></i> 连接</button><button class="btn btn-success" onclick="testConnection()" id="testBtn"><i class="fas fa-check"></i> 测试连接</button><button class="btn btn-danger" onclick="disconnectSSH()" disabled id="disconnectBtn"><i class="fas fa-times"></i> 断开连接</button></div><!-- 状态提示 --><div id="alertContainer"></div></div><!-- 终端容器 --><div class="terminal-container hidden" id="terminalContainer"><!-- Tab栏 --><div class="terminal-tabs" id="terminalTabs"><!-- tabs will be added dynamically --></div><!-- Terminal内容区 --><div class="terminal-content" id="terminalContent"><!-- terminals will be added dynamically --></div><div class="status-bar"><span id="statusBar">就绪</span><span id="terminalStats">行: 24, 列: 80</span></div></div></div><!-- 文件管理页面 --><div class="page-content" id="page-files"><div class="content-header"><h1 class="content-title">文件管理器</h1><div class="action-buttons"><button class="btn btn-primary" onclick="showUploadModal()"><i class="fas fa-upload"></i> 上传文件</button><button class="btn btn-success" onclick="createFolder()"><i class="fas fa-folder-plus"></i> 新建文件夹</button></div></div><div class="file-manager" id="fileManager"><div class="file-manager-header"><div class="file-path"><button class="btn btn-secondary" onclick="navigateUp()"><i class="fas fa-arrow-up"></i></button><input type="text" id="currentPath" value="/" readonly><button class="btn btn-secondary" onclick="refreshFiles()"><i class="fas fa-sync"></i></button></div><div class="file-actions"><select id="fileServerSelect" onchange="switchFileServer()" style="padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; background: white;"><option value="">选择服务器...</option></select></div></div><div class="file-grid" id="fileGrid"><div class="alert alert-info">请先选择一个服务器来浏览文件</div></div></div></div></div></div><!-- 弹窗 --><!-- 文件上传弹窗 --><div class="modal" id="uploadModal"><div class="modal-content"><div class="modal-header"><h3 class="modal-title">上传文件</h3><button class="modal-close" onclick="closeModal('uploadModal')">&times;</button></div><div><div class="form-group"><label for="uploadFiles">选择文件</label><input type="file" id="uploadFiles" multiple></div><div class="form-group"><label for="uploadPath">上传路径</label><input type="text" id="uploadPath" value="/" required></div><div style="text-align: right; margin-top: 20px;"><button type="button" class="btn btn-secondary" onclick="closeModal('uploadModal')">取消</button><button type="button" class="btn btn-primary" onclick="handleUpload(); return false;">上传</button></div></div></div></div><!-- JavaScript代码 --><script src="js/webssh-multisession.js"></script>
</body>
</html>

9. 数据库初始化

创建必要的数据库表结构:

-- 服务器配置表
CREATE TABLE IF NOT EXISTS servers (id BIGINT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(100) NOT NULL COMMENT '服务器名称',host VARCHAR(255) NOT NULL COMMENT '服务器地址',port INT DEFAULT 22 COMMENT 'SSH端口',username VARCHAR(100) NOT NULL COMMENT '用户名',password VARCHAR(500) NOT NULL COMMENT '密码(建议加密存储)',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);-- 删除现有测试数据(避免重复插入)
DELETE FROM 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=UTCdriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: roothikari:maximum-pool-size: 20minimum-idle: 5connection-timeout: 30000server:port: 8080servlet:context-path: /compression:enabled: truemime-types: text/html,text/xml,text/plain,text/css,text/javascript,application/javascript,application/jsontomcat:max-connections: 200threads:max: 100min-spare: 10logging:level:root: INFOcom.example.webssh: DEBUGfile:name: logs/webssh.logpattern:file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"# 自定义配置
webssh:ssh:connection-timeout: 30000session-timeout: 1800000max-connections-per-user: 10file:upload-max-size: 100MBtemp-dir: /tmp/webssh-uploadscollaboration:enabled: truemax-participants: 10session-timeout: 3600000

性能优化与最佳实践

1. 缓存优化

@Service
@EnableCaching
public class CachedServerService {@Cacheable(value = "servers", key = "#username")public List<Server> getUserServers(String username) {return serverRepository.findByCreatedBy(username);}@CacheEvict(value = "servers", key = "#username")public void clearUserServersCache(String username) {// 清理缓存}
}

2. 安全增强

@Component
public class SecurityEnhancements {/*** 密码加密存储*/public String encryptPassword(String password) {try {Cipher cipher = 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) {throw new RuntimeException("密码加密失败", e);}}/*** 操作审计*/@EventListenerpublic void handleSSHCommand(SSHCommandEvent event) {auditService.logSSHOperation(event.getUsername(),event.getServerHost(), event.getCommand(),event.getTimestamp());}
}

总结

本文介绍了如何使用Spring Boot开发一个基础的Web SSH客户端。

通过JSch库处理SSH连接,WebSocket实现实时通信,JdbcTemplate进行数据存储,我们构建了一个功能完整的Web SSH解决方案。

这个项目适合作为学习WebSocket通信、SSH协议应用的实践案例。

在实际生产环境中使用时,还需要考虑以下几个方面:

安全注意事项

  • 密码应该加密存储,不要明文保存
  • 添加用户认证机制,避免无权限访问
  • 考虑使用SSH密钥认证替代密码认证
  • 限制可连接的服务器范围和用户权限

性能优化

  • SSH连接池管理,避免频繁建立连接
  • WebSocket连接数量控制
  • 大量输出时的数据传输优化

仓库地址:https://github.com/yuboon/java-examples/tree/master/springboot-web-ssh

http://www.dtcms.com/a/324513.html

相关文章:

  • 深入理解 Java 中的线程池:原理、参数与最佳实践
  • 【密码学】8. 密码协议
  • 金融机构在元宇宙中的业务开展与创新路径
  • 【教学类-29-06】20250809灰色门牌号-黏贴版(6层*5间层2间)题目和答案(剪贴卡片)
  • 使用Python调用OpenAI的function calling源码
  • Pytorch深度学习框架实战教程-番外篇02-Pytorch池化层概念定义、工作原理和作用
  • ROS2 QT 多线程功能包设计
  • PHP项目运行
  • (LeetCode 每日一题) 869. 重新排序得到 2 的幂 (哈希表+枚举)
  • Framework开发之Zygote进程2(基于开源的AOSP15)--init.rc在start zygote之后的事情(详细完整版逐行代码走读)
  • springboot骚操作
  • 【论文阅读】Deep Adversarial Multi-view Clustering Network
  • 视觉障碍物后处理
  • Java开发异步编程中常用的接口和类
  • 人工智能之数学基础:如何理解n个事件的独立?
  • [C/C++线程安全]_[中级]_[避免使用的C线程不安全函数]
  • Android APK 使用OpenGl 绘制三角形源码
  • Word XML 批注范围克隆处理器
  • 绕过文件上传漏洞并利用文件包含漏洞获取系统信息的技术分析
  • 使用MongoDB存储和计算距离
  • Spring Boot 2 升级 Spring Boot 3 的全方位深度指南
  • Leetcode 3644. Maximum K to Sort a Permutation
  • 9_基于深度学习的车型检测识别系统(yolo11、yolov8、yolov5+UI界面+Python项目源码+模型+标注好的数据集)
  • Error: error:0308010C:digital envelope routines::unsupported at new Hash
  • 【Python 小脚本·大用途 · 第 3 篇】
  • 编译xformers
  • 【深度学习新浪潮】遥感图像风格化迁移研究工作介绍
  • 学习记录(十九)-Overleaf如何插入图片(单,多)
  • 学习模板元编程(3)enable_if
  • CART算法:Gini指数