Реализовать проект WebSSH, используя чистую Java

Java

предисловие

В последнее время из-за требований проекта в проекте необходимо реализовать функцию терминала подключения WebSSH.Поскольку я впервые делаю такую ​​функцию, я сначала отправился на GitHub, чтобы узнать, есть ли готовые колеса, которые могут можно использовать напрямую.Я много такого видел.Аспекты проектов,таких как:GateOne,webssh,shellinabox и т.д.,эти проекты вполне могут реализовывать функции webssh,но они не были приняты в итоге,т.к. большинство из этих нижние слои написаны на python и должны полагаться на множество файлов.Это решение можно использовать время от времени, что быстро и просто, но когда проект используется пользователями, невозможно требовать от пользователей включения этих базовых зависимостей в server, что явно неразумно, поэтому я решил написать функцию WebSSH самостоятельно и открыть исходный код как независимый проект.

Адрес с открытым исходным кодом проекта github:GitHub.com/no Cort Y/веб…

Технический отбор

Так как webssh требует взаимодействия с данными в режиме реального времени, он будет использовать давно подключенный WebSocket.Для удобства разработки фреймворк использует SpringBoot.Кроме того, я также узнал о jsch для пользователей Java для подключения к ssh и xterm.js для реализации внешние страницы оболочки.

так,Окончательный технический выбор — SpringBoot+Websocket+jsch+xterm.js..

импортировать зависимости

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
    <!-- Web相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- jsch支持 -->
    <dependency>
        <groupId>com.jcraft</groupId>
        <artifactId>jsch</artifactId>
        <version>0.1.54</version>
    </dependency>
    <!-- WebSocket 支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- 文件上传解析器 -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>1.4</version>
    </dependency>
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
</dependencies>

Простой случай xterm

Поскольку xterm — непопулярная технология, многие студенты не имеют поддержки знаний в этой области, я также временно изучил ее, чтобы реализовать эту функцию, поэтому я познакомлю вас с ней здесь.

xterm.js — это контейнер на основе WebSocket, который помогает нам реализовать стиль командной строки во внешнем интерфейсе. Точно так же, как когда мы обычно используем SecureCRT или XShell для подключения к серверу.

Ниже приводится вводный кейс на официальном сайте:

<!doctype html>
 <html>
  <head>
    <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
    <script src="node_modules/xterm/lib/xterm.js"></script>
  </head>
  <body>
    <div id="terminal"></div>
    <script>
      var term = new Terminal();
      term.open(document.getElementById('terminal'));
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')
    </script>
  </body>
 </html>

В финальном тесте страница выглядит так:

xterm入门

Вы можете увидеть, что страница имеет стиль, похожий на стиль оболочки, затем углубитесь и реализуйте webssh в соответствии с этим.

бэкэнд реализация

Поскольку xterm реализует только интерфейсный стиль, он не может реально взаимодействовать с сервером.Взаимодействие с сервером в основном контролируется нашим внутренним интерфейсом Java, поэтому мы начинаем с внутреннего интерфейса и используем jsch+websocket для реализации этой части. содержания.

  • Конфигурация веб-сокета

    Поскольку отправка сообщения в режиме реального времени на внешний интерфейс требует использования WebSocket, учащиеся, не знакомые с WebSocket, могут узнать об этом самостоятельно.Я не буду вводить здесь слишком много.Мы приступим непосредственно к настройке WebSocket.

    /**
    * @Description: websocket配置
    * @Author: NoCortY
    * @Date: 2020/3/8
    */
    @Configuration
    @EnableWebSocket
    public class WebSSHWebSocketConfig implements WebSocketConfigurer{
        @Autowired
        WebSSHWebSocketHandler webSSHWebSocketHandler;
        @Override
        public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
            //socket通道
            //指定处理器和路径,并设置跨域
            webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh")
                    .addInterceptors(new WebSocketInterceptor())
                    .setAllowedOrigins("*");
        }
    }
    
  • Реализация обработчика и перехватчика

    Только что мы завершили настройку WebSocket и указали обработчик и перехватчик. Итак, следующий шаг — реализация процессоров и перехватчиков.

    перехватчик:

    public class WebSocketInterceptor implements HandshakeInterceptor {
        /**
         * @Description: Handler处理前调用
         * @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]
         * @return: boolean
         * @Author: NoCortY
         * @Date: 2020/3/1
         */
        @Override
        public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
            if (serverHttpRequest instanceof ServletServerHttpRequest) {
                ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
                //生成一个UUID,这里由于是独立的项目,没有用户模块,所以可以用随机的UUID
                //但是如果要集成到自己的项目中,需要将其改为自己识别用户的标识
                String uuid = UUID.randomUUID().toString().replace("-","");
                //将uuid放到websocketsession中
                map.put(ConstantPool.USER_UUID_KEY, uuid);
                return true;
            } else {
                return false;
            }
        }
    
        @Override
        public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
    
        }
    }
    

    процессор:

    /**
    * @Description: WebSSH的WebSocket处理器
    * @Author: NoCortY
    * @Date: 2020/3/8
    */
    @Component
    public class WebSSHWebSocketHandler implements WebSocketHandler{
        @Autowired
        private WebSSHService webSSHService;
        private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);
    
        /**
         * @Description: 用户连接上WebSocket的回调
         * @Param: [webSocketSession]
         * @return: void
         * @Author: Object
         * @Date: 2020/3/8
         */
        @Override
        public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
            logger.info("用户:{},连接WebSSH", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY));
            //调用初始化连接
            webSSHService.initConnection(webSocketSession);
        }
    
        /**
         * @Description: 收到消息的回调
         * @Param: [webSocketSession, webSocketMessage]
         * @return: void
         * @Author: NoCortY
         * @Date: 2020/3/8
         */
        @Override
        public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
            if (webSocketMessage instanceof TextMessage) {
                logger.info("用户:{},发送命令:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
                //调用service接收消息
                webSSHService.recvHandle(((TextMessage) webSocketMessage).getPayload(), webSocketSession);
            } else if (webSocketMessage instanceof BinaryMessage) {
    
            } else if (webSocketMessage instanceof PongMessage) {
    
            } else {
                System.out.println("Unexpected WebSocket message type: " + webSocketMessage);
            }
        }
    
        /**
         * @Description: 出现错误的回调
         * @Param: [webSocketSession, throwable]
         * @return: void
         * @Author: Object
         * @Date: 2020/3/8
         */
        @Override
        public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
            logger.error("数据传输错误");
        }
    
        /**
         * @Description: 连接关闭的回调
         * @Param: [webSocketSession, closeStatus]
         * @return: void
         * @Author: NoCortY
         * @Date: 2020/3/8
         */
        @Override
        public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
            logger.info("用户:{}断开webssh连接", String.valueOf(webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY)));
            //调用service关闭连接
            webSSHService.close(webSocketSession);
        }
    
        @Override
        public boolean supportsPartialMessages() {
            return false;
        }
    }
    

    Следует отметить, что идентификатор пользователя, который я добавил в перехватчик, используетслучайный UUID, это потому, что в качестве независимого проекта веб-сокета нет пользовательского модуля.Если вам нужно интегрировать этот проект в свой собственный проект, вам нужно изменить эту часть кода и изменить его на идентификатор пользователя, используемый для идентификации пользователя в ваш собственный проект.

  • Реализация бизнес-логики WebSSH (ядро)

    Конфигурацию websocket мы реализовали только сейчас, все это мертвые коды.После реализации интерфейса мы можем реализовать его в соответствии с нашими потребностями.Теперь мы реализуем основную бизнес-логику бэкенда.Перед реализацией этой логики давайте подумаем о это, WebSSH, мы в основном хотим представить эффект.

    Я сделал резюме здесь:

    1. Сначала мы должны подключиться к терминалу (инициализировать подключение)

    2. Во-вторых, нашему серверу нужно обрабатывать сообщения от внешнего интерфейса (получать и обрабатывать сообщения от внешнего интерфейса)

    3. Нам нужно записать обратно сообщение, возвращенное терминалом, во внешний интерфейс (данные записываются обратно во внешний интерфейс)

    4. Закройте соединение

    В соответствии с этими четырьмя требованиями мы сначала определяем интерфейс, который может прояснить требования.

    /**
     * @Description: WebSSH的业务逻辑
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public interface WebSSHService {
        /**
         * @Description: 初始化ssh连接
         * @Param:
         * @return:
         * @Author: NoCortY
         * @Date: 2020/3/7
         */
        public void initConnection(WebSocketSession session);
    
        /**
         * @Description: 处理客户段发的数据
         * @Param:
         * @return:
         * @Author: NoCortY
         * @Date: 2020/3/7
         */
        public void recvHandle(String buffer, WebSocketSession session);
    
        /**
         * @Description: 数据写回前端 for websocket
         * @Param:
         * @return:
         * @Author: NoCortY
         * @Date: 2020/3/7
         */
        public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;
    
        /**
         * @Description: 关闭连接
         * @Param:
         * @return:
         * @Author: NoCortY
         * @Date: 2020/3/7
         */
        public void close(WebSocketSession session);
    }
    

    Теперь мы можем реализовать функции, которые мы определили в соответствии с этим интерфейсом.

    1. инициализировать соединение

      Поскольку наш нижний уровень реализован с использованием jsch, нам нужно использовать jsch для установления соединения здесь. Так называемое соединение инициализации на самом деле предназначено для сохранения необходимой нам информации о соединении в Map, и здесь не выполняется никакой реальной операции соединения. Почему бы не подключиться напрямую сюда? Поскольку интерфейс подключен только к WebSocket, но нам также нужны имя пользователя и пароль linux-терминала, отправленные нам интерфейсом, без этой информации мы не можем подключиться.

      public void initConnection(WebSocketSession session) {
              JSch jSch = new JSch();
              SSHConnectInfo sshConnectInfo = new SSHConnectInfo();
              sshConnectInfo.setjSch(jSch);
              sshConnectInfo.setWebSocketSession(session);
              String uuid = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
              //将这个ssh连接信息放入map中
              sshMap.put(uuid, sshConnectInfo);
      }
      
    2. Обработка данных, отправленных клиентом

      На этом этапе мы разделимся на две ветви.

      Первая ветвь: Если клиент отправляет информацию типа имени пользователя и пароля терминала, то подключаем терминал.

      Вторая ветвь: если клиент отправляет команду на работу терминала, то мы напрямую пересылаем ее на терминал и получаем результат выполнения терминала.

      Конкретная реализация кода:

      public void recvHandle(String buffer, WebSocketSession session) {
              ObjectMapper objectMapper = new ObjectMapper();
              WebSSHData webSSHData = null;
              try {
                  //转换前端发送的JSON
                  webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
              } catch (IOException e) {
                  logger.error("Json转换异常");
                  logger.error("异常信息:{}", e.getMessage());
                  return;
              }
          //获取刚才设置的随机的uuid
              String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
              if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(webSSHData.getOperate())) {
                  //如果是连接请求
                  //找到刚才存储的ssh连接对象
                  SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
                  //启动线程异步处理
                  WebSSHData finalWebSSHData = webSSHData;
                  executorService.execute(new Runnable() {
                      @Override
                      public void run() {
                          try {
                              //连接到终端
                              connectToSSH(sshConnectInfo, finalWebSSHData, session);
                          } catch (JSchException | IOException e) {
                              logger.error("webssh连接异常");
                              logger.error("异常信息:{}", e.getMessage());
                              close(session);
                          }
                      }
                  });
              } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
                  //如果是发送命令的请求
                  String command = webSSHData.getCommand();
                  SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
                  if (sshConnectInfo != null) {
                      try {
                          //发送命令到终端
                          transToSSH(sshConnectInfo.getChannel(), command);
                      } catch (IOException e) {
                          logger.error("webssh连接异常");
                          logger.error("异常信息:{}", e.getMessage());
                          close(session);
                      }
                  }
              } else {
                  logger.error("不支持的操作");
                  close(session);
              }
      }
      
    3. Данные отправляются на внешний интерфейс через веб-сокет

      public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
              session.sendMessage(new TextMessage(buffer));
      }
      
    4. закрыть соединение

      public void close(WebSocketSession session) {
          //获取随机生成的uuid
              String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
              SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
              if (sshConnectInfo != null) {
                  //断开连接
                  if (sshConnectInfo.getChannel() != null) sshConnectInfo.getChannel().disconnect();
                  //map中移除该ssh连接信息
                  sshMap.remove(userId);
              }
      }
      

    На этом вся наша back-end реализация закончена.Из-за ограниченного места некоторые операции здесь инкапсулированы в методы, поэтому я не буду их слишком много показывать.Давайте сосредоточимся на идее реализации логики. Далее мы реализуем интерфейсную реализацию.

Интерфейсная реализация

Предварительная работа в основном делится на следующие этапы:

  1. Реализация страницы
  2. Подключите WebSocket и завершите получение данных и обратную запись
  3. отправка данных

Итак, давайте реализуем это шаг за шагом.

  • реализация страницы

    Реализация страницы очень проста, нам просто нужно отобразить большой черный экран терминала на весь экран, поэтому нам не нужно писать какой-либо стиль, просто создайте div, а затем поместите экземпляр терминала в этот div через xterm, Этого можно добиться.

    <!doctype html>
    <html>
    <head>
        <title>WebSSH</title>
        <link rel="stylesheet" href="../css/xterm.css" />
    </head>
    <body>
    <div id="terminal" style="width: 100%;height: 100%"></div>
    
    <script src="../lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
    <script src="../js/xterm.js" charset="utf-8"></script>
    <script src="../js/webssh.js" charset="utf-8"></script>
    <script src="../js/base64.js" charset="utf-8"></script>
    </body>
    </html>
    
  • Подключите WebSocket и завершите отправку, получение и обратную запись данных

    openTerminal( {
        //这里的内容可以写死,但是要整合到项目中时,需要通过参数的方式传入,可以动态连接某个终端。
            operate:'connect',
            host: 'ip地址',
            port: '端口号',
            username: '用户名',
            password: '密码'
        });
        function openTerminal(options){
            var client = new WSSHClient();
            var term = new Terminal({
                cols: 97,
                rows: 37,
                cursorBlink: true, // 光标闪烁
                cursorStyle: "block", // 光标样式  null | 'block' | 'underline' | 'bar'
                scrollback: 800, //回滚
                tabStopWidth: 8, //制表宽度
                screenKeys: true
            });
    
            term.on('data', function (data) {
                //键盘输入时的回调函数
                client.sendClientData(data);
            });
            term.open(document.getElementById('terminal'));
            //在页面上显示连接中...
            term.write('Connecting...');
            //执行连接操作
            client.connect({
                onError: function (error) {
                    //连接失败回调
                    term.write('Error: ' + error + '\r\n');
                },
                onConnect: function () {
                    //连接成功回调
                    client.sendInitData(options);
                },
                onClose: function () {
                    //连接关闭回调
                    term.write("\rconnection closed");
                },
                onData: function (data) {
                    //收到数据时回调
                    term.write(data);
                }
            });
        }
    

Показать результаты

  • соединять

    连接

  • соединение успешно

    连接成功

  • Командная операция

    команда лс:

    ls命令

    вим-редактор:

    vim编辑器

    верхняя команда:

    top命令

Эпилог

Таким образом, мы завершили реализацию проекта webssh, не полагаясь на какие-либо другие компоненты. Бэкенд полностью реализован на Java. Благодаря использованию SpringBoot его очень легко развернуть.

Однако мы также можем расширить этот проект, например, добавив файлы для загрузки или скачивания, как и Xftp, вы можете легко перетаскивать файлы для загрузки и скачивания.

После этого проекта я продолжу обновление, а вышеперечисленные функции будут реализовываться потихоньку.Github:GitHub.com/no Cort Y/веб…

Если вам это нравится, вы можете поставить ему звезду~

Добро пожаловать в мой личный блог:Object's Blog