赞
踩
在系统中使用Web Shell连接集群的登录节点
前端使用Vue,WebSocket实现前后端通信,后端使用JSch ssh通讯包。
<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>
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()); } } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。