<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          再見,xShell !再見...(附源碼)建議收藏

          共 28392字,需瀏覽 57分鐘

           ·

          2021-03-04 06:53

          點擊上方[全棧開發(fā)者社區(qū)]右上角[...][設(shè)為星標(biāo)?]


          前言

          最近由于項目需求,項目中需要實現(xiàn)一個WebSSH連接終端的功能,由于自己第一次做這類型功能,所以首先上了GitHub找了找有沒有現(xiàn)成的輪子可以拿來直接用,當(dāng)時看到了很多這方面的項目,例如:GateOne、webssh、shellinabox等,這些項目都可以很好地實現(xiàn)webssh的功能,但是最終并沒有采用,原因是在于這些底層大都是python寫的,需要依賴很多文件,自己用的時候可以使用這種方案,快捷省事,但是做到項目中供用戶使用時,總不能要求用戶做到服務(wù)器中必須包含這些底層依賴,這顯然不太合理,所以我決定自己動手寫一個WebSSH的功能,并且作為一個獨立的項目開源出來。

          github項目開源地址:https://github.com/NoCortY/WebSSH

          技術(shù)選型

          由于webssh需要實時數(shù)據(jù)交互,所以會選用長連接的WebSocket,為了開發(fā)的方便,框架選用SpringBoot,另外還自己了解了Java用戶連接ssh的jsch和實現(xiàn)前端shell頁面的xterm.js.

          所以,最終的技術(shù)選型就是 SpringBoot+Websocket+jsch+xterm.js。

          導(dǎo)入依賴

          <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相關(guān) -->
              <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是一個冷門技術(shù),所以很多同學(xué)并沒有這方面的知識支撐,我也是為了實現(xiàn)這個功能所以臨時學(xué)的,所以在這給大家介紹一下。

          xterm.js是一個基于WebSocket的容器,它可以幫助我們在前端實現(xiàn)命令行的樣式。就像是我們平常再用SecureCRT或者XShell連接服務(wù)器時一樣。

          下面是官網(wǎng)上的入門案例:

          <!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>

          最終測試,頁面就是下面這個樣子:

          可以看到頁面已經(jīng)出現(xiàn)了類似與shell的樣式,那就根據(jù)這個繼續(xù)深入,實現(xiàn)一個webssh。

          后端實現(xiàn)

          由于xterm只要只是實現(xiàn)了前端的樣式,并不能真正地實現(xiàn)與服務(wù)器交互,與服務(wù)器交互主要還是靠我們Java后端來進(jìn)行控制的,所以我們從后端開始,使用jsch+websocket實現(xiàn)這部分內(nèi)容。

          WebSocket配置

          由于消息實時推送到前端需要用到WebSocket,不了解WebSocket的同學(xué)可以先去自行了解一下,這里就不過多介紹了,我們直接開始進(jìn)行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通道
                  //指定處理器和路徑,并設(shè)置跨域
                  webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh")
                          .addInterceptors(new WebSocketInterceptor())
                          .setAllowedOrigins("*");
              }
          }

          處理器(Handler)和攔截器(Interceptor)的實現(xiàn)

          剛才我們完成了WebSocket的配置,并指定了一個處理器和攔截器。所以接下來就是處理器和攔截器的實現(xiàn)。

          攔截器:


          public class WebSocketInterceptor implements HandshakeInterceptor {
              /**
               * @Description: Handler處理前調(diào)用
               * @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,這里由于是獨立的項目,沒有用戶模塊,所以可以用隨機(jī)的UUID
                      //但是如果要集成到自己的項目中,需要將其改為自己識別用戶的標(biāo)識
                      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的回調(diào)
               * @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));
                  //調(diào)用初始化連接
                  webSSHService.initConnection(webSocketSession);
                }

              /**
               * @Description: 收到消息的回調(diào)
               * @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("用戶:{},發(fā)送命令:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
                      //調(diào)用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: 出現(xiàn)錯誤的回調(diào)
               * @Param: [webSocketSession, throwable]
               * @return: void
               * @Author: Object
               * @Date: 2020/3/8
                  */

                @Override
                public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
                  logger.error("數(shù)據(jù)傳輸錯誤");
                }

              /**
               * @Description: 連接關(guān)閉的回調(diào)
               * @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)));
                  //調(diào)用service關(guān)閉連接
                  webSSHService.close(webSocketSession);
                }

              @Override
              public boolean supportsPartialMessages() {
                  return false;
              }
          }

          需要注意的是,我在攔截器中加入的用戶標(biāo)識是使用了隨機(jī)的UUID,這是因為作為一個獨立的websocket項目,沒有用戶模塊,如果需要將這個項目集成到自己的項目中,需要修改這部分代碼,將其改為自己項目中識別一個用戶所用的用戶標(biāo)識。

          WebSSH的業(yè)務(wù)邏輯實現(xiàn)(核心)

          剛才我們實現(xiàn)了websocket的配置,都是一些死代碼,實現(xiàn)了接口再根據(jù)自身需求即可實現(xiàn),現(xiàn)在我們將進(jìn)行后端主要業(yè)務(wù)邏輯的實現(xiàn),在實現(xiàn)這個邏輯之前,我們先來想想,WebSSH,我們主要想要呈現(xiàn)一個什么效果。

          我這里做了一個總結(jié):

          1.首先我們得先連接上終端(初始化連接)

          2.其次我們的服務(wù)端需要處理來自前端的消息(接收并處理前端消息)

          3.我們需要將終端返回的消息回寫到前端(數(shù)據(jù)回寫前端)

          4.關(guān)閉連接

          根據(jù)這四個需求,我們先定義一個接口,這樣可以讓需求明了起來。


          /**
           * @Description: WebSSH的業(yè)務(wù)邏輯
           * @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: 處理客戶段發(fā)的數(shù)據(jù)
               * @Param:
               * @return:
               * @Author: NoCortY
               * @Date: 2020/3/7
               */

              public void recvHandle(String buffer, WebSocketSession session);

              /**
               * @Description: 數(shù)據(jù)寫回前端 for websocket
               * @Param:
               * @return:
               * @Author: NoCortY
               * @Date: 2020/3/7
               */

              public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;

              /**
               * @Description: 關(guān)閉連接
               * @Param:
               * @return:
               * @Author: NoCortY
               * @Date: 2020/3/7
               */

              public void close(WebSocketSession session);
          }

          現(xiàn)在我們可以根據(jù)這個接口去實現(xiàn)我們定義的功能了。

          初始化連接

          由于我們的底層是依賴jsch實現(xiàn)的,所以這里是需要使用jsch去建立連接的。而所謂初始化連接,實際上就是將我們所需要的連接信息,保存在一個Map中,這里并不進(jìn)行任何的真實連接操作。為什么這里不直接進(jìn)行連接?因為這里前端只是連接上了WebSocket,但是我們還需要前端給我們發(fā)來linux終端的用戶名和密碼,沒有這些信息,我們是無法進(jìn)行連接的。


          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);
          }

          處理客戶端發(fā)送的數(shù)據(jù)

          在這一步驟中,我們會分為兩個分支。

          第一個分支:如果客戶端發(fā)來的是終端的用戶名和密碼等信息,那么我們進(jìn)行終端的連接。

          第二個分支:如果客戶端發(fā)來的是操作終端的命令,那么我們就直接轉(zhuǎn)發(fā)到終端并且獲取終端的執(zhí)行結(jié)果。

          具體代碼實現(xiàn):


          public void recvHandle(String buffer, WebSocketSession session) {
                  ObjectMapper objectMapper = new ObjectMapper();
                  WebSSHData webSSHData = null;
                  try {
                      //轉(zhuǎn)換前端發(fā)送的JSON
                      webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
                  } catch (IOException e) {
                      logger.error("Json轉(zhuǎn)換異常");
                      logger.error("異常信息:{}", e.getMessage());
                      return;
                  }
              //獲取剛才設(shè)置的隨機(jī)的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())) {
                      //如果是發(fā)送命令的請求
                      String command = webSSHData.getCommand();
                      SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
                      if (sshConnectInfo != null) {
                          try {
                              //發(fā)送命令到終端
                              transToSSH(sshConnectInfo.getChannel(), command);
                          } catch (IOException e) {
                              logger.error("webssh連接異常");
                              logger.error("異常信息:{}", e.getMessage());
                              close(session);
                          }
                      }
                  } else {
                      logger.error("不支持的操作");
                      close(session);
                  }
          }

          數(shù)據(jù)通過websocket發(fā)送到前端


          public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
                  session.sendMessage(new TextMessage(buffer));
          }

          關(guān)閉連接

          public void close(WebSocketSession session) {
              //獲取隨機(jī)生成的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);
                  }
          }

          至此,我們的整個后端實現(xiàn)就結(jié)束了,由于篇幅有限,這里將一些操作封裝成了方法,就不做過多展示了,重點講邏輯實現(xiàn)的思路吧。接下來我們將進(jìn)行前端的實現(xiàn)。

          前端實現(xiàn)

          前端工作主要分為這么幾個步驟:

          頁面的實現(xiàn)

          連接WebSocket并完成數(shù)據(jù)的接收并回寫 數(shù)據(jù)的發(fā)送 所以我們一步一步來實現(xiàn)它。

          頁面實現(xiàn) 頁面的實現(xiàn)很簡單,我們只不過需要在一整個屏幕上都顯示終端那種大黑屏幕,所以我們并不用寫什么樣式,只需要創(chuàng)建一個div,之后將terminal實例通過xterm放到這個div中,就可以實現(xiàn)了。

          <!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并完成數(shù)據(jù)的發(fā)送、接收、回寫


          openTerminal( {
              //這里的內(nèi)容可以寫死,但是要整合到項目中時,需要通過參數(shù)的方式傳入,可以動態(tài)連接某個終端。
                  operate:'connect',
                  host: 'ip地址',
                  port: '端口號',
                  username: '用戶名',
                  password: '密碼'
              });
              function openTerminal(options){
                  var client = new WSSHClient();
                  var term = new Terminal({
                      cols: 97,
                      rows: 37,
                      cursorBlink: true// 光標(biāo)閃爍
                      cursorStyle: "block"// 光標(biāo)樣式  null | 'block' | 'underline' | 'bar'
                      scrollback: 800//回滾
                      tabStopWidth: 8//制表寬度
                      screenKeys: true
                  });

                  term.on('data', function (data) {
                      //鍵盤輸入時的回調(diào)函數(shù)
                      client.sendClientData(data);
                  });
                  term.open(document.getElementById('terminal'));
                  //在頁面上顯示連接中...
                  term.write('Connecting...');
                  //執(zhí)行連接操作
                  client.connect({
                      onError: function (error) {
                          //連接失敗回調(diào)
                          term.write('Error: ' + error + '\r\n');
                      },
                      onConnect: function () {
                          //連接成功回調(diào)
                          client.sendInitData(options);
                      },
                      onClose: function () {
                          //連接關(guān)閉回調(diào)
                          term.write("\rconnection closed");
                      },
                      onData: function (data) {
                          //收到數(shù)據(jù)時回調(diào)
                          term.write(data);
                      }
                  });
              }

          效果展示

          連接

          連接成功

          命令操作

          ls命令:

          vim編輯器:

          top命令:

          結(jié)語

          這樣我們就完成了一個webssh項目的實現(xiàn),沒有依賴其它任何的組件,后端完全使用Java實現(xiàn),由于用了SpringBoot,非常容易部署。

          但是,我們還可以對這個項目進(jìn)行擴(kuò)展,比如新增上傳或下載文件,就像Xftp一樣,可以很方便地拖拽式上傳下載文件。

          作者 | ObjectSpace

          來源 | https://sourl.cn/apXiEy

          覺得本文對你有幫助?請分享給更多人

          關(guān)注「全棧開發(fā)者社區(qū)」加星標(biāo),提升全棧技能

          本公眾號會不定期給大家發(fā)福利,包括送書、學(xué)習(xí)資源等,敬請期待吧!

          如果感覺推送內(nèi)容不錯,不妨右下角點個在看轉(zhuǎn)發(fā)朋友圈或收藏,感謝支持。


          好文章,留言、點贊、在看和分享一條龍吧??

          瀏覽 60
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  草逼逼逼逼 | 亚洲区在线播放 | 韩国三级91 | 久久一区国产 | 黄色色情网站在线观看 |