<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      SpringBoot + SSH 客戶端:在瀏覽器中執行遠程命令

      在日常運維工作中,我們經常需要通過SSH連接多臺服務器進行操作。傳統的SSH客戶端雖然功能完善,但在某些場景下存在一些限制。
      本文將介紹如何使用Spring Boot開發一個Web SSH客戶端,讓用戶可以通過瀏覽器直接連接和操作遠程服務器。
      這種方案在企業內部運維管理、臨時訪問、移動辦公等場景中具有一定的實用價值。
      圖片
      圖片
      圖片
      01
      Web SSH客戶端的應用場景
      圖片
      相比傳統SSH客戶端,Web SSH在以下場景中具有實際價值:
      傳統SSH客戶端的局限性
      ? 客戶端依賴: 需要在每臺設備上安裝SSH客戶端軟件
      ? 統一管理困難: 難以統一管理服務器連接配置和用戶權限
      ? 操作審計不便: 缺乏統一的操作日志記錄和管理
      ? 移動設備支持有限: 在手機、平板上操作體驗較差
      ? 防火墻限制: 某些網絡環境下SSH端口可能被阻止
      Web SSH的實際優勢
      ? 無需安裝客戶端: 通過瀏覽器即可使用,降低部署成本
      ? 統一權限管理: 可以集中管理用戶的服務器訪問權限
      ? 操作記錄可追溯: 所有SSH操作都可以記錄和審計
      ? 移動設備友好: 在移動設備上也能提供相對較好的使用體驗
      ? 繞過端口限制: 通過HTTP/HTTPS端口提供服務
      這些特點使得Web SSH在企業內部運維平臺、云服務管理后臺、教學環境等場景中有實際的應用價值。
      圖片
      02
      技術方案設計
      圖片
      本文將基于以下技術棧實現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組件進行顯示。
      圖片
      03
      核心功能實現
      圖片
      1. 項目初始化
      首先創建Spring Boot項目并添加必要的依賴:
      <?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>
      1. SSH連接管理器
      創建SSH連接管理器,負責建立和維護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();
          }
      }
      1. WebSocket配置
      配置WebSocket,實現瀏覽器與服務器的實時通信:
      @Configuration
      @EnableWebSocket
      publicclassWebSocketConfigimplementsWebSocketConfigurer {
          
          @Autowired
          private SSHWebSocketHandler sshWebSocketHandler;
          
          @Override
          publicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry registry) {
              registry.addHandler(sshWebSocketHandler, "/ssh")
                      .setAllowedOriginPatterns("*"); // 生產環境中應該限制域名
          }
      }
      1. WebSocket處理器
      創建WebSocket處理器,處理SSH命令的發送和接收:
      @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";
          }
      }
      1. 服務器信息管理
      使用JdbcTemplate進行服務器配置的數據操作:
      @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);
          }
      }
      1. 文件傳輸功能
      集成SFTP文件傳輸
      @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; }
          }
      }
      1. REST API控制器
      創建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()));
              }
          }
      }
      1. 前端實現
      使用純HTML + JavaScript集成xterm.js
      <!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')">&times;</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>
      1. 數據庫初始化
      創建必要的數據庫表結構:
      -- 服務器配置表
      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
      圖片
      04
      性能優化與最佳實踐
      圖片
      1. 緩存優化
      @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) {
              // 清理緩存
          }
      }
      1. 安全增強
      @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()
              );
          }
      }
      圖片
      05
      總結
      圖片
      本文介紹了如何使用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
      posted @ 2025-09-08 14:26  CharyGao  閱讀(17)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产成人亚洲精品成人区| 在线观看国产区亚洲一区| 免费国产好深啊好涨好硬视频| 日本一区二区三区专线| 一区二区三区成人| 熟妇人妻不卡中文字幕| 玉溪市| 中文字幕日韩精品东京热| 亚洲av中文乱码一区二| 日韩一区二区三区日韩精品| 亚洲av日韩av一区久久| 久久精品国产亚洲精品色婷婷| 九九热精品在线观看| 国产一区二区三区四区色| 久久国产免费观看精品| 孝昌县| 开心五月婷婷综合网站| 亚洲精品揄拍自拍首页一| 欧美一区二区三区激情| 最新精品国偷自产在线| 亚洲爆乳少妇无码激情| 久久亚洲av午夜福利精品一区| 精品人妻二区中文字幕| 苍井空一区二区三区在线观看| 亚洲AV毛片一区二区三区| 少妇xxxxx性开放| 亚洲精品无amm毛片| 亚洲精品美女一区二区| 国产喷水1区2区3区咪咪爱AV| 成人午夜在线观看日韩| 亚洲a∨无码一区二区三区| 国产成人无码| 成年视频人免费网站动漫在线| 长腿校花无力呻吟娇喘| 亚洲欧美偷国产日韩| 国产香蕉一区二区三区在线视频| 资中县| 三上悠亚精品一区二区久久| 99精品视频在线观看婷婷| 国产精品99中文字幕| 男人一天堂精品国产乱码|