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());}}}
}
三、效果展示

相关文章:
Vue+Xterm.js+WebSocket+JSch实现Web Shell终端
一、需求 在系统中使用Web Shell连接集群的登录节点 二、实现 前端使用Vue,WebSocket实现前后端通信,后端使用JSch ssh通讯包。 1. 前端核心代码 <template><div class"shell-container"><div id"shell"/>&l…...
用 adb 来模拟手机插上电源和拔掉电源的情形
实用的 ADB 命令 要模拟手机从 USB 充电器上拔掉的情形,你可以使用: adb shell dumpsys battery set usb 0或者,如果你使用的是 Android 6.0 或更高版本的设备,你可以使用: adb shell dumpsys battery unplug要重新…...
【SPIE独立出版】第四届智能交通系统与智慧城市国际学术会议(ITSSC 2024)
第四届智能交通系统与智慧城市国际学术会议(ITSSC 2024)将于2024年8月23-25日在中国西安举行。本次会议主要围绕智能交通、交通新能源、无人驾驶、智慧城市、智能家居、智能生活等研究领域展开讨论, 旨在为该研究领域的专家学者们提供一个分享…...
【Unity数据交互】如何Unity中读取Ecxel中的数据
👨💻个人主页:元宇宙-秩沅 👨💻 hallo 欢迎 点赞👍 收藏⭐ 留言📝 加关注✅! 👨💻 本文由 秩沅 原创 👨💻 专栏交流🧧&…...
基于深度学习LightWeight的人体姿态检测跌倒系统源码
一. LightWeight概述 light weight openpose是openpose的简化版本,使用了openpose的大体流程。 Light weight openpose和openpose的区别是: a 前者使用的是Mobilenet V1(到conv5_5),后者使用的是Vgg19(前10…...
SpringBoot 生产实践:没有父 starter 的打包问题
文章目录 前言一、搜索引擎二、Chat GPT三、官方文档四、小结推荐阅读 前言 今天刚准备写点文章,需要 SpringBoot 项目来演示效果。一时心血来潮,没有采用传统的方式(即通过引入 spring-boot-starter-parent 父工程的方式)。 &l…...
IDEA配Git
目录 前言 1.创建Git仓库,获得可提交渠道 2.选择本地提交的项目名 3.配置远程仓库的地址 4.新增远程仓库地址 5.开始进行commit操作 6.push由于邮箱问题被拒绝的解决方法: 后记 前言 以下操作都是基于你已经下载了Git的前提下进行的,…...
51单片机STC89C52RC——14.1 直流电机调速
目录 目的/效果 1:电机转速同步LED呼吸灯 2 通过独立按键 控制直流电机转速。 一,STC单片机模块 二,直流电机 2.1 简介 2.2 驱动电路 2.2.1 大功率器件直接驱动 2.2.2 H桥驱动 正转 反转 2.2.3 ULN2003D 引脚、电路 2.3 PWM&…...
AI对于高考和IT行业的深远影响
目录 AI对IT行业的冲击及深远影响1. 工作自动化2. 新的就业机会3. 行业融合4. 技术升级和创新5. 数据的重要性 IT行业的冬天要持续多久?大学的软件开发类专业是否还值得报考?其他问题IT行业是否都是加班严重?35岁后就业困难是否普遍现象&…...
C语言下的文件详解
主要内容 文件概述文件指针文件的打开与关闭文件的读写 文件 把输入和输出的数据以文件的形式保存在计算机的外存储器上,可以确保数据能随时使用,避免反复输入和读取数据 文件概述 文件是指一组相关数据的有序集合 文件是存储数据的基本单位&#…...
Oracle PL / SQL块结构
在PL / SQL中,最小的有意义的代码分组被称为块。 块代码为变量声明和异常处理提供执行和作用域边界。 PL / SQL允许您创建匿名块和命名块。 命名块可以是包,过程,函数,触发器或对象类型。 PL / SQL是SQL的过程语言扩展&#x…...
MySQL的安装和启动
安装 版本 1,社区版:免费,不提供任何技术支持 2,商业版:可以试用30天,官方提供技术支持下载 1,下载地址:https://dev.mysql.com/downloads/mysql/ 2,安装:傻…...
Prometheus概述
1.什么是prometheus Prometheus 是一个开源的服务监控系统和时序数据库,其提供了通用的数据模型和快捷数据采集、存储和查询接口。它的核心组件Prometheus server会定期从静态配置的监控目标或者基于服务发现自动配置的自标中进行拉取数据,当新拉取到的…...
【SQL】什么是最左前缀原则/最左匹配原则
最左前缀原则(或最左匹配原则)是关系型数据库在使用复合索引时遵循的一条重要规则。该原则指的是,当查询条件使用复合索引时,查询优化器会首先使用索引的最左边的列,依次向右匹配,直到不再满足查询条件为止…...
java项目配置logback日志
在resource目录下添加logback配置文件 <?xml version"1.0" encoding"UTF-8"?> <configuration scan"true" scanPeriod"60 seconds" debug"false"><property name"log_dir" value"/APL/log…...
Python入门 2024/7/6
目录 元组的定义和操作 字符串的定义和操作 字符串 字符串的替换 字符串的分割 字符串的规整操作(去除前后空格) 字符串的规整操作(去掉前后指定字符串) 操作 字符串的替换 字符串的分割 字符串的规整操作 统计字符串的…...
ChatGPT4深度解析:探索智能对话新境界
大模型chatgpt4分析功能初探 目录 1、探测目的 2、目标变量分析 3、特征缺失率处理 4、特征描述性分析 5、异常值分析 6、相关性分析 7、高阶特征挖掘 1、探测目的 1、分析chat4的数据分析能力,提高部门人效 2、给数据挖掘提供思路 3、原始数据…...
触底加载的两种思路(以vue3前端和nodejs后端为例)
一:首先,nodejs后端的代码都是一样的. 需要前端返回page参数,然后nodejs逻辑进行处理,截取页数和每页条数和总条数, 总条数用来作为判断是否有数据的条件,也可以不用,注意看下文 一:不用获取容器高度的. pinia中进行的axios请求处理 在vue文件中进行pinia中数据的导入,继续进…...
tobias实现支付宝支付
tobias是一个为支付宝支付SDK做的Flutter插件。 如何使用 你需要在pubspec.yaml中配置url_scheme。url_scheme是一个独特的字符串,用来重新启动你的app,但是请注意字符串“_”是不合法的。 在iOS端,你还需要配置并传入一个universal link。…...
【音视频 | RTSP】RTSP协议详解 及 抓包例子解析(详细而不赘述)
😁博客主页😁:🚀https://blog.csdn.net/wkd_007🚀 🤑博客内容🤑:🍭嵌入式开发、Linux、C语言、C、数据结构、音视频🍭 🤣本文内容🤣&a…...
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする
日语学习-日语知识点小记-构建基础-JLPT-N4阶段(33):にする 1、前言(1)情况说明(2)工程师的信仰2、知识点(1) にする1,接续:名词+にする2,接续:疑问词+にする3,(A)は(B)にする。(2)復習:(1)复习句子(2)ために & ように(3)そう(4)にする3、…...
Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
ardupilot 开发环境eclipse 中import 缺少C++
目录 文章目录 目录摘要1.修复过程摘要 本节主要解决ardupilot 开发环境eclipse 中import 缺少C++,无法导入ardupilot代码,会引起查看不方便的问题。如下图所示 1.修复过程 0.安装ubuntu 软件中自带的eclipse 1.打开eclipse—Help—install new software 2.在 Work with中…...
今日学习:Spring线程池|并发修改异常|链路丢失|登录续期|VIP过期策略|数值类缓存
文章目录 优雅版线程池ThreadPoolTaskExecutor和ThreadPoolTaskExecutor的装饰器并发修改异常并发修改异常简介实现机制设计原因及意义 使用线程池造成的链路丢失问题线程池导致的链路丢失问题发生原因 常见解决方法更好的解决方法设计精妙之处 登录续期登录续期常见实现方式特…...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...
音视频——I2S 协议详解
I2S 协议详解 I2S (Inter-IC Sound) 协议是一种串行总线协议,专门用于在数字音频设备之间传输数字音频数据。它由飞利浦(Philips)公司开发,以其简单、高效和广泛的兼容性而闻名。 1. 信号线 I2S 协议通常使用三根或四根信号线&a…...
Golang——9、反射和文件操作
反射和文件操作 1、反射1.1、reflect.TypeOf()获取任意值的类型对象1.2、reflect.ValueOf()1.3、结构体反射 2、文件操作2.1、os.Open()打开文件2.2、方式一:使用Read()读取文件2.3、方式二:bufio读取文件2.4、方式三:os.ReadFile读取2.5、写…...
PostgreSQL——环境搭建
一、Linux # 安装 PostgreSQL 15 仓库 sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-$(rpm -E %{rhel})-x86_64/pgdg-redhat-repo-latest.noarch.rpm# 安装之前先确认是否已经存在PostgreSQL rpm -qa | grep postgres# 如果存在࿰…...
WPF八大法则:告别模态窗口卡顿
⚙️ 核心问题:阻塞式模态窗口的缺陷 原始代码中ShowDialog()会阻塞UI线程,导致后续逻辑无法执行: var result modalWindow.ShowDialog(); // 线程阻塞 ProcessResult(result); // 必须等待窗口关闭根本问题:…...
