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+c
          dataWrapper += "\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、rows
      const 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 server
                  this.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<>();

    /**
     * 初始化连接
     */
    @Override
    public 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 TODO
        LoginNodeInfo 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);
    }

    /**
     * 处理客户端发送的数据
     */
    @Override
    public 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());
        //连接超时时间30s
        session.connect(30 * 1000);

        //查询上次登录时间
        showLastLogin(session, webSocketSession, loginNodeInfo.getUsername());

        //开启交互式shell通道
        ChannelShell channel = (ChannelShell) session.openChannel("shell");
        //设置channel
        shellConnectInfo.setChannel(channel);

        //通道连接超时时间3s
        channel.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.mfbz.cn/a/769435.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Unity动画系统(2)

6.1 动画系统基础2-3_哔哩哔哩_bilibili p316 模型添加Animator组件 动画控制器 AnimatorController AnimatorController 可以通过代码控制动画速度 建立动画间的联系 bool值的设定 trigger p318 trigger点击的时候触发&#xff0c;如喊叫&#xff0c;开枪及换子弹等&#x…

css flex 子元素溢出时,父元素被撑开解决方案

当父元素使用flex: 1;自适应填满时&#xff0c;子元素内容溢出&#xff0c;父元素内容撑大&#xff0c;导致页面显示问题&#xff0c;或设置了overflow 为scroll 的元素没出现滚动条等问题 解决方案&#xff1a; 1.如果是横向排列&#xff0c;flex: 1;的元素加上width: 0; 此…

【PB案例学习笔记】-28制作一个右键菜单

写在前面 这是PB案例学习笔记系列文章的第28篇&#xff0c;该系列文章适合具有一定PB基础的读者。 通过一个个由浅入深的编程实战案例学习&#xff0c;提高编程技巧&#xff0c;以保证小伙伴们能应付公司的各种开发需求。 文章中设计到的源码&#xff0c;小凡都上传到了gite…

流量控制组件选型之 Sentinel vs Hystrix

Sentinel: Sentinel 是阿里中间件团队研发的面向分布式服务架构的轻量级高可用流量控制组件&#xff0c;于2018年7月正式开源。Sentinel 主要以流量为切入点&#xff0c;从流量控制、熔断降级、系统负载保护等多个维度来帮助用户提升服务的稳定性。大家可能会问&#xff1a;Sen…

总线局域网及解决冲突的方案

上文内容&#xff1a;局域网 1.什么是总线局域网 总线网结构&#xff1a; 所有的结点通过专门的网卡附接到一条总线上&#xff1b; 所有结点的信息都发送到同一条总线上&#xff08;冲突&#xff09;&#xff1b; 所有结点都从同一媒体上收取信息&#xff08;广播&am…

视频汇聚/安防监控/GB28181国标EasyCVR视频综合管理平台出现串流的原因排查及解决

安防视频监控系统/视频汇聚EasyCVR视频综合管理平台&#xff0c;采用了开放式的网络结构&#xff0c;能在复杂的网络环境中&#xff08;专网、局域网、广域网、VPN、公网等&#xff09;将前端海量的设备进行统一集中接入与视频汇聚管理&#xff0c;视频汇聚EasyCVR平台支持设备…

Stable Diffusion web UI 插件

2024.7.3更新&#xff0c;持续更新中 如果需要在linux上自己安装sd&#xff0c;参考&#xff1a;stable diffusion linux安装 插件复制到 /stable-diffusion-webui/extensions 目录下&#xff0c;然后重新启动sd即可 一、插件安装方法 每种插件的安装方法可能略有不同&#xf…

Redis分布式锁的应用场景有哪些

⼀ 、应⽤场景 在多线程并发的场景下 &#xff0c;Java Synchronized/Reentrantlock 锁能够实现同⼀个JVM进程内多线程 并发的安全性 &#xff0c;但⽆法保证多个JVM进程实例构成的集群环境在多线程下的安全性。在⼀些业务场景 下需要引⼊分布式锁。 1、缓存击穿 当某个热点缓…

精确计算应用的冷启动耗时

在iOS项目中&#xff0c;冷启动时间是指从用户点击应用图标开始&#xff0c;到应用完全加载并呈现出第一个界面&#xff08;可能需要网络请求必要的数据&#xff09;所花费的时间。这里以 main 函数为界&#xff0c;分为两个时间段&#xff1a; 从用户点击应用图标 ~ invoke m…

深度学习简介-AI(三)

深度学习简介 深度学习简介深度学习例子深度学习训练优化1.随机初始化2.优化损失函数3.优化器选择4.选择/调整模型结构 深度学习常见概念隐含层/中间层随机初始化损失函数导数与梯度优化器Mini Batch/epoch 深度学习训练逻辑图 深度学习简介 深度学习例子 猜数字 A: 我现在心…

24年河南特岗教师招聘流程+报名流程

河南特岗教师报名流程如下 1.登录河南省特岗招聘网 登录河南省特岗招聘网注册账号和密码&#xff0c;账号可以是手机号或者身份证号&#xff0c;密码自己设置 2.注册登录账号 注册完账号重新登录账号&#xff0c;输入身份证号、手机号、密码、验证码 3.浏览考试须知 填写个人信…

Unity Shader技巧:实现带投影机效果,有效避免边缘拉伸问题

这个是原始的projector 投影组件,边缘会有拉伸 经过修改shader 后边缘就没有拉伸了 (实现代码在文章最后) 这个着色器通过检查每个像素的UV坐标是否在定义的边界内,来确定是否应用黑色边框。如果UV坐标处于边缘区域,那么像素颜色会被强制设为黑色,从而在投影图像周围形成一…

JWT入门

JWT与TOKEN JWT&#xff08;JSON Web Token&#xff09;是一种基于 JSON 格式的轻量级安全令牌&#xff0c;通常用于在网络应用间安全地传递信息。而“token”一词则是一个更广泛的术语&#xff0c;用来指代任何形式的令牌&#xff0c;用于在计算机系统中进行身份验证或授权。J…

云原生技术架构详解

云原生技术最全详解(图文全面总结) 容器技术 容器技术&#xff1a;是将应用程序、及其所有依赖项&#xff0c;打包到一个独立的、可移植的容器中。 如下图所示: 容器技术的实现&#xff0c;最典型的就是以Docker为代表的。 如下图所示&#xff1a; 主要解决&#xff1a; 1、…

【ROS中Cjson文件的作用】

在ROS (Robot Operating System) 中&#xff0c;.json 文件通常用于存储配置信息、数据序列化或者在某些情况下用于网络通信和数据交换。JSON&#xff08;JavaScript Object Notation&#xff09;是一种轻量级的数据交换格式&#xff0c;易于人阅读和编写&#xff0c;同时也易于…

UE4_材质_使用彩色半透明阴影

学习笔记&#xff0c;不喜勿喷&#xff01;侵权立删&#xff0c;祝愿大美临沂生活越来越好&#xff01; 本教程将介绍如何配置虚幻引擎来投射彩色半透明阴影。 此功能在许多应用中都很有用&#xff0c;常见例子就是透过彩色玻璃窗的彩色光。 一、半透明阴影颜色 阴影在穿过半…

【Python机器学习】模型评估与改进——带交叉验证的网格搜索

虽然将数据划分为训练集、验证集、测试集的方法是可行的&#xff0c;也相对常用&#xff0c;但这种方法对数据的划分相当敏感&#xff0c;为了得到对泛化性能的更好估计&#xff0c;我们可以使用交叉验证来评估每种参数组合的性能&#xff0c;而不是仅将数据单次划分为训练集与…

模拟退火算法2—优缺点

模拟退火算法优点 1、以一定的概率接受恶化解 模拟退火算法(SA)在搜索策略上与传统的随机搜索方法不同,它不仅引入了适当的随机因素,而且还引入了物理系统退火过程的自然机理。这种自然机理的引入使模拟退火算法在迭代过程中不仅接受使目标函数变“好”的试探点,而且还能以一…

Hadoop权威指南-读书笔记-02-关于MapReduce

Hadoop权威指南-读书笔记 记录一下读这本书的时候觉得有意思或者重要的点~ 还是老样子~挑重点记录哈&#x1f601;有兴趣的小伙伴可以去看看原著&#x1f60a; 第二章 关于MapReduce MapReduce是一种可用于数据处理的编程模型。 MapReduce程序本质上是并行运行的&#xff0c…

行业模板|DataEase旅游行业大屏模板推荐

DataEase开源数据可视化分析工具于2022年6月发布模板市场&#xff08;https://templates-de.fit2cloud.com&#xff09;&#xff0c;并于2024年1月新增适用于DataEase v2版本的模板分类。模板市场旨在为DataEase用户提供专业、美观、拿来即用的大屏模板&#xff0c;方便用户根据…