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" />
<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">
<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號