0


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'exportdefault{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 =newTerminal({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 =newFitAddon();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";}elseif(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、rowsconst uri =`${this.socketURI}&path=${this.path}&cols=${this.cols}&rows=${this.rows}`;
      console.log(uri);this.socket =newWebSocket(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;returnfunction(){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. 后端核心代码
packagecom.example.webshell.service.impl;importcom.alibaba.fastjson.JSONObject;importcom.example.webshell.constant.Constant;importcom.example.webshell.entity.LoginNodeInfo;importcom.example.webshell.entity.ShellConnectInfo;importcom.example.webshell.entity.SocketData;importcom.example.webshell.entity.WebShellParam;importcom.example.webshell.service.WebShellService;importcom.example.webshell.utils.ThreadPoolUtils;importcom.example.webshell.utils.WebShellUtil;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.jcraft.jsch.*;importlombok.extern.slf4j.Slf4j;importorg.springframework.stereotype.Service;importjava.io.IOException;importjava.io.InputStream;importjava.io.OutputStream;importjava.util.Arrays;importjava.util.Map;importjava.util.Properties;importjava.util.concurrent.ConcurrentHashMap;importstaticcom.example.webshell.constant.Constant.*;@Slf4j@ServicepublicclassWebShellServiceImplimplementsWebShellService{/**
     * 存放ssh连接信息的map
     */privatestaticfinalMap<String,Object>SSH_MAP=newConcurrentHashMap<>();/**
     * 初始化连接
     */@OverridepublicvoidinitConnection(javax.websocket.Session webSocketSession,WebShellParam webShellParam){JSch jSch =newJSch();ShellConnectInfo shellConnectInfo =newShellConnectInfo();
        shellConnectInfo.setJsch(jSch);
        shellConnectInfo.setSession(webSocketSession);String uuid =WebShellUtil.getUuid(webSocketSession);// 根据集群和登录节点查询IP TODOLoginNodeInfo loginNodeInfo =newLoginNodeInfo("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,newSocketData(OPERATE_ERROR, e.getMessage()));close(webSocketSession);}});//将这个ssh连接信息放入缓存中SSH_MAP.put(uuid, shellConnectInfo);}/**
     * 处理客户端发送的数据
     */@OverridepublicvoidhandleMessage(javax.websocket.Session webSocketSession,String message){ObjectMapper objectMapper =newObjectMapper();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);}elseif(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());}}/**
     * 关闭连接
     */privatevoidclose(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连接终端
     */privatevoidconnectToSsh(ShellConnectInfo shellConnectInfo,WebShellParam webShellParam,LoginNodeInfo loginNodeInfo,javax.websocket.Session webSocketSession)throwsJSchException{Properties config =newProperties();// 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 =newbyte[Constant.BUFFER_SIZE];int i;//如果没有数据来,线程会一直阻塞在这个地方等待数据。while((i = inputStream.read(buffer))!=-1){sendMessage(webSocketSession,newSocketData(OPERATE_COMMAND,newString(Arrays.copyOfRange(buffer,0, i))));}}catch(IOException e){
            log.error("读取终端返回的信息流异常:", e);}finally{//断开连接后关闭会话
            session.disconnect();
            channel.disconnect();}}/**
     * 向前端展示上次登录信息
     */privatevoidshowLastLogin(Session session,javax.websocket.Session webSocketSession,String username)throwsJSchException{ChannelExec channelExec =(ChannelExec) session.openChannel("exec");
        channelExec.setCommand("lastlog -u "+ username);
        channelExec.connect();
        channelExec.setErrStream(System.err);try(InputStream inputStream = channelExec.getInputStream()){byte[] buffer =newbyte[Constant.BUFFER_SIZE];int i;StringBuilder sb =newStringBuilder();while((i = inputStream.read(buffer))!=-1){
                sb.append(newString(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,newSocketData(OPERATE_COMMAND, msg));}}catch(IOException e){
            log.error("读取终端返回的信息流异常:", e);}finally{
            channelExec.disconnect();}}/**
     * 数据写回前端
     */privatevoidsendMessage(javax.websocket.Session webSocketSession,SocketData data){try{
            webSocketSession.getBasicRemote().sendText(JSONObject.toJSONString(data));}catch(IOException e){
            log.error("数据写回前端异常:", e);}}/**
     * 将消息转发到终端
     */privatevoidsendToTerminal(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());}}}}

三、效果展示

在这里插入图片描述


本文转载自: https://blog.csdn.net/oNew_Lifeo/article/details/140154614
版权归原作者 在线打码 所有, 如有侵权,请联系我们删除。

“Vue+Xterm.js+WebSocket+JSch实现Web Shell终端”的评论:

还没有评论