Vue+Xterm.js+WebSocket+JSch實現Web Shell終端

一、需求

在系統中使用Web Shell連接集群的登錄節點

二、實現

前端使用Vue,WebSocket實現前后端通信,后端使用JSch ssh通訊包。

1. 前端核心代碼
<template><div class="shell-container"><div id="shell"/></div>
</template><script>import 'xterm/css/xterm.css'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'export default {name: 'WebShell',props: {socketURI: {type: String,default: ''},},watch: {socketURI: {deep: true, //對象內部屬性的監聽,關鍵。immediate: true,handler() {this.initSocket();},},},data() {return {term: undefined,rows: 24,cols: 80,path: "",isShellConn: false // shell是否連接成功}},mounted() {const { onTerminalResize } = this;this.initSocket();// 通過防抖函數const resizedFunc = this.debounce(function() {onTerminalResize();}, 250); // 250毫秒內只執行一次  window.addEventListener('resize', resizedFunc);},beforeUnmount() {this.socket.close();this.term&&this.term.dispose();window.removeEventListener('resize');},methods: {initTerm() {let term = new Terminal({rendererType: "canvas", //渲染類型rows: this.rows, //行數cols: this.cols, // 不指定行數,自動回車后光標從下一行開始convertEol: true, //啟用時,光標將設置為下一行的開頭disableStdin: false, //是否應禁用輸入windowsMode: true, // 根據窗口換行cursorBlink: true, //光標閃爍theme: {foreground: "#ECECEC", //字體background: "#000000", //背景色cursor: "help", //設置光標lineHeight: 20,},});this.term = term;const fitAddon = new FitAddon();this.term.loadAddon(fitAddon);this.fitAddon = fitAddon;let element = document.getElementById("shell");term.open(element);// 自適應大小(使終端的尺寸和幾何尺寸適合于終端容器的尺寸),初始化的時候寬高都是對的fitAddon.fit();term.focus();//監視命令行輸入this.term.onData((data) => {let dataWrapper = data;if (dataWrapper === "\r") {dataWrapper = "\n";} else if (dataWrapper === "\u0003") {// 輸入ctrl+cdataWrapper += "\n";}// 將輸入的命令通知給后臺,后臺返回數據。this.socket.send(JSON.stringify({ type: "command", data: dataWrapper }));});},onTerminalResize() {this.fitAddon.fit();this.socket.send(JSON.stringify({type: "resize",data: {rows: this.term.rows,cols: this.term.cols,}}));},initSocket() {if (this.socketURI == "") {return;}// 添加path、cols、rowsconst uri = `${this.socketURI}&path=${this.path}&cols=${this.cols}&rows=${this.rows}`;console.log(uri);this.socket = new WebSocket(uri);this.socketOnClose();this.socketOnOpen();this.socketOnmessage();this.socketOnError();},socketOnOpen() {this.socket.onopen = () => {console.log("websocket鏈接成功");this.initTerm();};},socketOnmessage() {this.socket.onmessage = (evt) => {try {if (typeof evt.data === "string") {const msg = JSON.parse(evt.data);switch(msg.type) {case "command":// 將返回的數據寫入xterm,回顯在webshell上this.term.write(msg.data);// 當shell首次連接成功時才發送resize事件if (!this.isShellConn) {// when server ready for connection,send resize to serverthis.onTerminalResize();this.isShellConn = true;}break;case "exit":this.term.write("Process exited with code 0");break;}}} catch (e) {console.error(e);console.log("parse json error.", evt.data);}};},socketOnClose() {this.socket.onclose = () => {this.socket.close();console.log("關閉 socket");window.removeEventListener("resize", this.onTerminalResize);};},socketOnError() {this.socket.onerror = () => {console.log("socket 鏈接失敗");};},debounce(func, wait) {  let timeout;  return function() {  const context = this;  const args = arguments;  clearTimeout(timeout);  timeout = setTimeout(function() {  func.apply(context, args);  }, wait);  };  }  }
}
</script><!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#shell {width: 100%;height: 100%;
}
.shell-container {height: 100%;
}
</style>
2. 后端核心代碼
package com.example.webshell.service.impl;import com.alibaba.fastjson.JSONObject;
import com.example.webshell.constant.Constant;
import com.example.webshell.entity.LoginNodeInfo;
import com.example.webshell.entity.ShellConnectInfo;
import com.example.webshell.entity.SocketData;
import com.example.webshell.entity.WebShellParam;
import com.example.webshell.service.WebShellService;
import com.example.webshell.utils.ThreadPoolUtils;
import com.example.webshell.utils.WebShellUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;import static com.example.webshell.constant.Constant.*;@Slf4j
@Service
public class WebShellServiceImpl implements WebShellService {/*** 存放ssh連接信息的map*/private static final Map<String, Object> SSH_MAP = new ConcurrentHashMap<>();/*** 初始化連接*/@Overridepublic void initConnection(javax.websocket.Session webSocketSession, WebShellParam webShellParam) {JSch jSch = new JSch();ShellConnectInfo shellConnectInfo = new ShellConnectInfo();shellConnectInfo.setJsch(jSch);shellConnectInfo.setSession(webSocketSession);String uuid = WebShellUtil.getUuid(webSocketSession);// 根據集群和登錄節點查詢IP TODOLoginNodeInfo loginNodeInfo = new LoginNodeInfo("demo_admin", "demo_admin", "192.168.88.102", 22);//啟動線程異步處理ThreadPoolUtils.execute(() -> {try {connectToSsh(shellConnectInfo, webShellParam, loginNodeInfo, webSocketSession);} catch (JSchException e) {log.error("web shell連接異常: {}", e.getMessage());sendMessage(webSocketSession, new SocketData(OPERATE_ERROR, e.getMessage()));close(webSocketSession);}});//將這個ssh連接信息放入緩存中SSH_MAP.put(uuid, shellConnectInfo);}/*** 處理客戶端發送的數據*/@Overridepublic void handleMessage(javax.websocket.Session webSocketSession, String message) {ObjectMapper objectMapper = new ObjectMapper();SocketData shellData;try {shellData = objectMapper.readValue(message, SocketData.class);String userId = WebShellUtil.getUuid(webSocketSession);//找到剛才存儲的ssh連接對象ShellConnectInfo shellConnectInfo = (ShellConnectInfo) SSH_MAP.get(userId);if (shellConnectInfo != null) {if (OPERATE_RESIZE.equals(shellData.getType())) {ChannelShell channel = shellConnectInfo.getChannel();Object data = shellData.getData();Map map = objectMapper.readValue(JSONObject.toJSONString(data), Map.class);System.out.println(map);channel.setPtySize(Integer.parseInt(map.get("cols").toString()), Integer.parseInt(map.get("rows").toString()), 0, 0);} else if (OPERATE_COMMAND.equals(shellData.getType())) {String command = shellData.getData().toString();sendToTerminal(shellConnectInfo.getChannel(), command);// 退出狀態碼int exitStatus = shellConnectInfo.getChannel().getExitStatus();System.out.println(exitStatus);} else {log.error("不支持的操作");close(webSocketSession);}}} catch (Exception e) {e.printStackTrace();log.error("消息處理異常: {}", e.getMessage());}}/*** 關閉連接*/private void close(javax.websocket.Session webSocketSession) {String userId = WebShellUtil.getUuid(webSocketSession);ShellConnectInfo shellConnectInfo = (ShellConnectInfo) SSH_MAP.get(userId);if (shellConnectInfo != null) {//斷開連接if (shellConnectInfo.getChannel() != null) {shellConnectInfo.getChannel().disconnect();}//map中移除SSH_MAP.remove(userId);}}/*** 使用jsch連接終端*/private void connectToSsh(ShellConnectInfo shellConnectInfo, WebShellParam webShellParam, LoginNodeInfo loginNodeInfo, javax.websocket.Session webSocketSession) throws JSchException {Properties config = new Properties();// SSH 連接遠程主機時,會檢查主機的公鑰。如果是第一次該主機,會顯示該主機的公鑰摘要,提示用戶是否信任該主機config.put("StrictHostKeyChecking", "no");//獲取jsch的會話Session session = shellConnectInfo.getJsch().getSession(loginNodeInfo.getUsername(), loginNodeInfo.getHost(), loginNodeInfo.getPort());session.setConfig(config);//設置密碼session.setPassword(loginNodeInfo.getPassword());//連接超時時間30ssession.connect(30 * 1000);//查詢上次登錄時間showLastLogin(session, webSocketSession, loginNodeInfo.getUsername());//開啟交互式shell通道ChannelShell channel = (ChannelShell) session.openChannel("shell");//設置channelshellConnectInfo.setChannel(channel);//通道連接超時時間3schannel.connect(3 * 1000);channel.setPty(true);//讀取終端返回的信息流try (InputStream inputStream = channel.getInputStream()) {//循環讀取byte[] buffer = new byte[Constant.BUFFER_SIZE];int i;//如果沒有數據來,線程會一直阻塞在這個地方等待數據。while ((i = inputStream.read(buffer)) != -1) {sendMessage(webSocketSession, new SocketData(OPERATE_COMMAND, new String(Arrays.copyOfRange(buffer, 0, i))));}} catch (IOException e) {log.error("讀取終端返回的信息流異常:", e);} finally {//斷開連接后關閉會話session.disconnect();channel.disconnect();}}/*** 向前端展示上次登錄信息*/private void showLastLogin(Session session, javax.websocket.Session webSocketSession, String username) throws JSchException {ChannelExec channelExec = (ChannelExec) session.openChannel("exec");channelExec.setCommand("lastlog -u " + username);channelExec.connect();channelExec.setErrStream(System.err);try (InputStream inputStream = channelExec.getInputStream()) {byte[] buffer = new byte[Constant.BUFFER_SIZE];int i;StringBuilder sb = new StringBuilder();while ((i = inputStream.read(buffer)) != -1) {sb.append(new String(Arrays.copyOfRange(buffer, 0, i)));}// 解析結果String[] split = sb.toString().split("\n");if (split.length > 1) {String[] items = split[1].split("\\s+", 4);String msg = String.format("Last login: %s from %s\n", items[3], items[2]);sendMessage(webSocketSession, new SocketData(OPERATE_COMMAND, msg));}} catch (IOException e) {log.error("讀取終端返回的信息流異常:", e);} finally {channelExec.disconnect();}}/*** 數據寫回前端*/private void sendMessage(javax.websocket.Session webSocketSession, SocketData data) {try {webSocketSession.getBasicRemote().sendText(JSONObject.toJSONString(data));} catch (IOException e) {log.error("數據寫回前端異常:", e);}}/*** 將消息轉發到終端*/private void sendToTerminal(Channel channel, String command) {if (channel != null) {try {OutputStream outputStream = channel.getOutputStream();outputStream.write(command.getBytes());outputStream.flush();} catch (IOException e) {log.error("web shell將消息轉發到終端異常:{}", e.getMessage());}}}
}

三、效果展示

在這里插入圖片描述

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/40258.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/40258.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/40258.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

C++ 實現字符串逆序

C 實現字符串逆序 思路&#xff1a; 輸入一個字符串。使用雙指針法&#xff0c;交換字符串的首尾字符&#xff0c;逐步向中間移動。輸出逆序后的字符串。 #include <iostream> #include <string>using namespace std;void reverseString(string &str) {int …

【FPGA】STA靜態時序分析

文章目錄 一.定義二.分類1. 靜態時序分析2. 靜態時序分析 三. 概念四. 時間余量1.場景2.建立時間余量3.保持時間余量 一.定義 時序分析:檢查電路是否滿足時序要求&#xff1b; 二.分類 1. 靜態時序分析 STA,遍歷所有的時序路徑&#xff0c;根據時序庫&#xff08;.lib文件&…

【Mojolicious RESTful接口全解】構建現代化Web服務的秘訣

標題&#xff1a;【Mojolicious RESTful接口全解】構建現代化Web服務的秘訣 Mojolicious是一個基于Perl的高性能、實時的Web框架&#xff0c;它以其簡潔的語法和強大的功能而聞名。Mojolicious不僅支持傳統的Web應用開發&#xff0c;還特別適合構建RESTful API。本文將詳細介紹…

新手教學系列——使用uWSGI對Flask應用提速

在構建和部署Flask應用時,性能和穩定性是兩個關鍵的因素。為了提升Flask應用的性能,我們可以借助uWSGI這個強大的工具。本文將詳細介紹為什么要使用uWSGI、uWSGI的底層原理,并提供一個實例配置,幫助你更好地理解和應用這個工具。 為什么要使用uWSGI uWSGI 是一個應用服務…

探索企業知識邊界,鴻翼ECM AI助手開啟智慧問答新時代

在信息化迅速發展的當下&#xff0c;企業積累的數字文檔數量巨大&#xff0c;這些文檔中蘊含的深層信息對業務發展至關重要。然而&#xff0c;傳統的搜索技術常常因只能進行關鍵字查詢而無法滿足對文檔深層次理解的需求。 據Gartner調查&#xff0c;高達47%的員工在尋找有效工…

Webpack: 三種Chunk產物的打包邏輯

概述 在前文 Webpack: Dependency Graph 管理模塊間依賴 中&#xff0c;我們已經詳細講解了「構建」階段如何從 Entry 開始逐步遞歸讀入、解析模塊內容&#xff0c;并最終構建出模塊依賴關系圖 —— ModuleGraph 對象。本文我們繼續往下&#xff0c;講解在接下來的「封裝」階段…

【大數據】—美國交通事故分析(2016 年 2 月至 2020 年 12 月)

引言 在當今快速發展的數字時代&#xff0c;大數據已成為我們理解世界、做出決策的重要工具。特別是在交通安全領域&#xff0c;大數據分析能夠揭示事故模式、識別風險因素&#xff0c;并幫助制定預防措施&#xff0c;從而挽救生命。本文將深入探討2016年2月至2020年12月期間&…

【redis】 LRU 和 LFU 算法

1、簡介 Redis 中的 LRU&#xff08;Least Recently Used&#xff09;和 LFU&#xff08;Least Frequently Used&#xff09;算法是用于決定在內存空間不足時&#xff0c;哪些鍵&#xff08;key&#xff09;應該被刪除以釋放空間的策略。這兩種算法都試圖通過跟蹤鍵的使用情況…

解決Memcached內存碎片:優化緩存性能的策略

解決Memcached內存碎片&#xff1a;優化緩存性能的策略 Memcached是一個廣泛使用的高性能分布式內存緩存系統&#xff0c;它通過在內存中緩存數據來加速數據檢索操作。然而&#xff0c;隨著時間的推移和緩存操作的進行&#xff0c;Memcached可能會遇到內存碎片問題&#xff0c…

24年河南特崗教師招聘流程+報名流程

河南特崗教師報名流程如下 1.登錄河南省特崗招聘網 登錄河南省特崗招聘網注冊賬號和密碼&#xff0c;賬號可以是手機號或者身份證號&#xff0c;密碼自己設置 2.注冊登錄賬號 注冊完賬號重新登錄賬號&#xff0c;輸入身份證號、手機號、密碼、驗證碼 3.瀏覽考試須知 填寫個人信…

Python 編程快速上手——讓繁瑣工作自動化(第2版)讀書筆記01 Python基礎快速過關

Python 編程快速上手——讓繁瑣工作自動化&#xff08;第2版&#xff09;讀書筆記01 Python基礎快速過關 1 python基礎概念 Python提供了高效的高級數據結構&#xff0c;還能簡單有效地面向對象編程。 python運算符順序 **——%——//——/——*——-——python中常見的數據…

Real-Time 3D Graphics with WebGL2

WebGL渲染管線 下圖是WebGL渲染管線的示意圖: Vertex Buffer Objects (VBOs) VBOS中包含了用于描述幾何體的信息。如&#xff0c;幾何體的頂點坐標&#xff0c;法線坐標&#xff0c;顏色&#xff0c;紋理坐標等。 Index Buffer Objects (IBOs) IBOs中包含了描述頂點關系的信…

C#的多線程UI窗體控件顯示方案 - 開源研究系列文章

上次編寫了《LUAgent服務器端工具》這個應用&#xff0c;然后里面需要新啟動一個線程去對文件進行上傳到FTP服務器&#xff0c;但是新線程里無法對應用主線程UI的內容進行更改&#xff0c;所以就需要在線程里設置主UI線程里控件信息的方法&#xff0c;于是就有了此博文。此文記…

Rocky Linux 9 快速安裝docker 教程

前述 CentOS 7系統將于2024年06月30日停止維護服務。CentOS官方不再提供CentOS 及后續版本&#xff0c;不再支持新的軟件和補丁更新。CentOS用戶現有業務隨時面臨宕機和安全風險&#xff0c;并無法確保及時恢復。由于 CentOS Stream 相對不穩定&#xff0c;剛好在尋找平替系統…

idm 支持斷點續傳嗎 idm 斷點續傳如何使用 idm斷點續傳怎么解決 idm下載中斷后無法繼續下載

斷點續傳功能&#xff0c;讓我再也不會懼怕下載大型文件。在斷點續傳的幫助下&#xff0c;用戶可以隨時暫停下載任務&#xff0c;并在空閑時繼續之前的下載進程。下載文件不懼網絡波動&#xff0c;斷點續傳讓下載過程更穩定。有關 idm 支持斷點續傳嗎&#xff0c;idm 斷點續傳如…

JavaScript:if-else類型

目錄 任務描述 相關知識 if語句 if-else語句 匹配問題 編程要求 任務描述 本關任務&#xff1a;根據成績判斷考試結果。 相關知識 在編程中&#xff0c;我們常常根據變量是否滿足某個條件來執行不同的語句。 JavaScript中利用以if關鍵字開頭的條件語句達到以上目的&am…

商城項目回顧

哈哈&#xff0c;準備期末考試去了&#xff0c;項目停了一段時間。現在又忘的差不多了。所以專門寫一篇博客總結前期項目的知識點。 Client軟件包 代碼加總結&#xff1a; 這段代碼實現了一個簡單的客戶端程序&#xff0c;用于與服務器建立連接、發送登錄信息并接收服務器的響…

筆記:tencentos2.4升級gcc4到gcc8.5

由于開發需要將tencentos2.4的GCC版本升級到和cat /proc/version中GCC8.4較接近的版本。 過程如下&#xff1a; 首先 ls -al /etc/yum.repos.d/ 觀察tlinux.repo 可以看到類似&#xff1a; [tlinux] nametlinux-$releasever - tlinux baseurlhttp://mirrors.tencent.com/t…

在主線程和非主線程調用 DispatchQueue.main.sync { }

在 Swift 中&#xff0c;DispatchQueue.main.sync { } 的行為取決于當前執行代碼的線程。以下是詳細的說明&#xff1a; 主線程調用 DispatchQueue.main.sync { } 當在主線程上調用 DispatchQueue.main.sync { } 時&#xff0c;會發生死鎖&#xff08;Deadlock&#xff09;。…

|從零搭建網絡| VisionTransformer網絡詳解及搭建

&#x1f31c;|從零搭建網絡| VisionTransformer系列網絡詳解及搭建&#x1f31b; 文章目錄 &#x1f31c;|從零搭建網絡| VisionTransformer系列網絡詳解及搭建&#x1f31b;&#x1f31c; 前言 &#x1f31b;&#x1f31c; VIT模型詳解 &#x1f31b;&#x1f31c; VIT模型架…