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.rhkb.cn/news/371371.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

web缓存代理服务器

一、web缓存代理 web代理的工作机制 代理服务器是一个位于客户端和原始&#xff08;资源&#xff09;服务器之间的服务器&#xff0c;为了从原始服务器取得内容&#xff0c;客户端向代理服务器发送一个请求&#xff0c;并指定目标原始服务器&#xff0c;然后代理服务器向原始…

【NTN 卫星通信】Starlink基于终端用户的测量以及测试概述

1 概述 收集了一些starlink的资料&#xff0c;是基于终端侧部署在野外的一些测试以及测量结果。 2 低地球轨道卫星网络概述 低地球轨道卫星网络(lsn)被认为是即将到来的6G中真正实现全球覆盖的关键基础设施。本文介绍了我们对Starlink端到端网络特征的初步测量结果和观测结果&…

win11自动删除文件的问题,安全中心提示

win11自动删除文件的问题&#xff0c;解决方法&#xff1a; 1.点击任务栏上的开始图标&#xff0c;在显示的应用中&#xff0c;点击打开设置。 或者点击电脑右下角的开始也可以 2.点击设置。也可以按Wini打开设置窗口。 3.左侧点击隐私和安全性&#xff0c;右侧点击Windows安全…

尚品汇-(十四)

&#xff08;1&#xff09;提交git 商品后台管理到此已经完成&#xff0c;我们可以把项目提交到公共的环境&#xff0c;原来使用svn&#xff0c;现在使用git 首先在本地创建ssh key&#xff1b; 命令&#xff1a;ssh-keygen -t rsa -C "your_emailyouremail.com" I…

【SVN的使用-源代码管理工具-命令行的使用 Objective-C语言】

一、接下来,我们来说一个终端的命令行的使用, 1.我们说,你的电脑里边呢,有终端, 在Mac里边,你想新建一个txt,应该怎么写,对,打开文本编辑, 打开这个东西,写点儿东西,然后保存一下,保存的时候,你还要去选择格式, 现在,如果我们用命令行,可以更方便一些, 2.首…

数据结构(其一)--基础知识篇

1. 数据结构三要素 1.1 数据结构的运算 即&#xff0c;增删改查 1.2 数据结构的存储结构 2. 数据类型&#xff0c;抽象数据类型 数据类型&#xff1a; &#xff08;1&#xff09;. 原子类型&#xff1a;bool、int... &#xff08;2&#xff09;. 结构类型&#xff1a;类、…

【vue组件库搭建06】组件库构建及npm发包

一、格式化目录结构 根据以下图片搭建组件库目录 index.js作为入口文件&#xff0c;将所有组件引入&#xff0c;并注册组件名称 import { EButton } from "./Button"; export * from "./Button"; import { ECard } from "./Card"; export * fr…

ubuntu常用命令

常用命令 netstatpsps -auxps -eLf netstat netstat -tunlp这个命令用来显示网络连接、路由表和网络接口统计信息。 Proto (协议): 显示连接使用的协议&#xff0c;通常是 “tcp” 或 “udp”。 Recv-Q (接收队列): 表示接收缓冲区中未被进程读取的数据包数量。一般情况下&am…

qt6 通过http查询天气的实现

步骤如下&#xff1a; cmakelist 当中&#xff0c;增加如下配置 引入包 访问远端api 解析返回的数据 cmakelist 当中&#xff0c;增加如下配置&#xff0c;作用是引入Network库。 引入包 3、访问远端api void Form1::on_pushButton_clicked() {//根据URL(http://t.weather.…

【Linux】在线求助命令--help,man page , info page

我们知道Linux有很多的命令&#xff0c;那LInux要不要背命令&#xff1f; 答案是背最常用的那些就行了 那有的时候我们想查询一些命令的详细用法该怎么办呢&#xff1f; 这里我给出3种方法 1.--help --help的使用方法很简单啊 要查询的命令 --help 我们看个例子 这里我只…

利用 Hexo 搭建个人博客

〇、前言 本文将会讨论&#xff0c;如何将 CSDN 上的博客&#xff0c;拉取到本地&#xff0c;然后PicGo、Hexo、Github 等工具建立个人博客&#xff0c;环境为 Ubuntu 20.04。 一、利用 Hexo 预备工作 首先安装 Node.js、npm、git工具。 > node -v v12.22.9 > npm -…

计算组的妙用!!页面权限控制

需求描述&#xff1a; 某些特殊的场景下&#xff0c;针对某页看板&#xff0c;需要进行数据权限卡控&#xff0c;但是又不能对全部的数据进行RLS处理&#xff0c;这种情况下可以利用计算组来解决这个需求。 实际场景 事实表包含产品维度和销售维度 两个维度属于同一公司下面的…

方向导数和梯度

方向导数和梯度 1 导数的回忆2 偏导数及其向量形式偏导数的几何意义偏导数的向量形式 3 方向导数向量形式几何意义方向导数和偏导的关系 4 梯度5 梯度下降算法 1 导数的回忆 导数的几何意义如图所示&#xff1a; 当 P 0 P_{0} P0​点不断接近 P P P时&#xff0c;导数如下定义…

Springboot学习之用EasyExcel4导入导出数据(基于MyBatisPlus)

一、POM依赖 <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><m…

Feign-未完成

Feign Java中如何实现接口调用&#xff1f;即如何发起http请求 前三种方式比较麻烦&#xff0c;在发起请求前&#xff0c;需要将Java对象进行序列化转为json格式的数据&#xff0c;才能发送&#xff0c;然后进行响应时&#xff0c;还需要把json数据进行反序列化成java对象。 …

短信验证码实现

一、设置AccessKey 创建用户并配置使用权限&#xff0c;使我们拥有调用 aliyunAPI 的权限&#xff0c;之后会生成 AccessKeyID 和 AccessKey密码&#xff0c;后面我们会使用到。需要注意的是 AccessKeyID 和 AccessKey密码生成后我们需要将他保存起来&#xff0c;否则后期无法查…

multisim中关于74ls192n和DSWPK开关仿真图分析(减法计数器)

&#x1f3c6;本文收录于「Bug调优」专栏&#xff0c;主要记录项目实战过程中的Bug之前因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&收藏&&…

如何监控和分析 PostgreSQL 中的查询执行计划?

文章目录 一、为什么监控和分析查询执行计划很重要二、PostgreSQL 中用于获取查询执行计划的方法三、理解查询执行计划的关键元素四、通过示例分析查询执行计划五、优化查询执行计划的常见策略六、使用工具辅助分析七、结合实际案例的详细分析八、总结 在 PostgreSQL 数据库中&…

nginx的匹配及重定向

一、nginx的匹配&#xff1a; nginx中location的优先级和匹配方式&#xff1a; 1.精确匹配&#xff1a;location / 对字符串进行完全匹配&#xff0c;必须完全符合 2.正则匹配&#xff1a;location ^~ ^~ 前缀匹配&#xff0c;以什么为开头 ~区分大小写的匹配 ~* 不区分…

第十四届蓝桥杯省赛C++B组G题【子串简写】题解(AC)

题目大意 给定字符串 s s s&#xff0c;字符 a , b a, b a,b&#xff0c;问字符串 s s s 中有多少个 a a a 开头 b b b 结尾的子串。 解题思路 20pts 使用二重循环枚举左端点和右端点&#xff0c;判断是否为 a a a 开头 b b b 结尾的字符串&#xff0c;是则答案加一…