springboot项目中如何集成Webssh连接服务器?
什么是Webssh?
WebSSH 是基于浏览器的 SSH 客户端,依托 WebSocket+HTTPS 加密传输,无需本地安装工具即可远程管理服务器。其优势在于跨平台兼容各类终端设备,支持集中权限管控与操作审计,能绕过部分防火墙限制,部署灵活且可与运维平台快速集成,大幅降低管理成本与使用门槛。
下面,我们就来手把手实现Webssh:
本教程将详细介绍如何在Vue项目中将现有的VNC连接替换为WebSSH连接,以提供更好的终端体验。
1 🌟工作原理🌟
- 前端通过WebSocket连接到后端的 /ws/ssh 端点
- 前端发送包含SSH连接参数的消息(主机、端口、用户名、密码)
- 后端接收消息后,使用JSch库建立到目标SSH服务器的连接
- 后端在WebSocket连接和SSH连接之间双向转发数据
- 前端的键盘输入通过WebSocket发送到后端,再由后端转发到SSH服务器
- SSH服务器的输出通过后端转发回前端,在终端中显示
2 🌟vue前端集成Webssh组件🌟
2.1 🌟安装xterm依赖🌟
首先,我们需要安装用于WebSSH功能的必要依赖包:
npm install xterm xterm-addon-fit xterm-addon-attach 这些包提供了:
- xterm: 终端模拟器核心库
- xterm-addon-fit: 自动适配容器尺寸的插件
- xterm-addon-attach: 将终端连接到WebSocket的插件
2.2 🌟创建WebSSH组件🌟
创建一个名为WebSSH.vue的新组件,用于处理SSH连接和终端显示:
<template> <div> <div> <div> <span>{{ host }}</span>:<span>{{ port }}</span> </div> <div> <!-- 连接/断开按钮 --> <button @click="connect" :disabled="connected"> <i></i> </button> <button @click="disconnect" :disabled="!connected"> <i></i> </button> </div> </div> <div> <div ref="terminal"></div> </div> </div> </template> <script> import { onMounted, onUnmounted, ref, nextTick } from 'vue'; import { Terminal } from 'xterm'; import { FitAddon } from 'xterm-addon-fit'; import 'xterm/css/xterm.css'; export default { name: 'WebSSH', props: { host: { type: String, required: true }, port: { type: Number, default: 22 }, username: { type: String, required: true }, password: { type: String, required: true } }, emits: ['connect', 'disconnect', 'error'], setup(props, { emit }) { const terminal = ref(null); const xterm = ref(null); const fitAddon = ref(null); const wsConnection = ref(null); const connected = ref(false); const connect = () => { if (connected.value) { console.log('Already connected'); return; } // WebSocket连接到后端SSH代理服务 // 修改为使用后端WebSocket端点 const wsUrl = `ws://localhost:8080/ws/ssh`; // 使用你的后端WebSocket端点 try { wsConnection.value = new WebSocket(wsUrl); wsConnection.value.onopen = () => { console.log('WebSocket连接已建立'); // 发送连接信息到后端 const connectMsg = { type: 'connect', host: props.host, port: props.port, username: userName, password: password }; wsConnection.value.send(JSON.stringify(connectMsg)); }; wsConnection.value.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === 'connected') { // 连接成功 connected.value = true; emit('connect'); // 初始化终端 if (!xterm.value) { xterm.value = new Terminal({ fontSize: 14, fontFamily: 'Monaco, Menlo, Ubuntu Mono, monospace', rows: 30, cols: 80, cursorBlink: true, convertEol: true }); fitAddon.value = new FitAddon(); xterm.value.loadAddon(fitAddon.value); xterm.value.open(terminal.value); fitAddon.value.fit(); // 将终端输入转发到WebSocket xterm.value.onData(data => { if (wsConnection.value && wsConnection.value.readyState === WebSocket.OPEN) { const stdinMsg = { type: 'stdin', data: data }; wsConnection.value.send(JSON.stringify(stdinMsg)); } }); // 当终端大小改变时,通知WebSocket xterm.value.onResize((size) => { if (wsConnection.value && wsConnection.value.readyState === WebSocket.OPEN) { const resizeMsg = { type: 'resize', cols: size.cols, rows: size.rows }; wsConnection.value.send(JSON.stringify(resizeMsg)); } }); } } else if (data.type === 'stdout') { // 输出数据到终端 xterm.value && xterm.value.write(data.data); } else if (data.type === 'error') { // 处理错误 console.error('SSH错误:', data.data); emit('error', data.data); } else if (data.type === 'disconnected') { // 处理断开连接 connected.value = false; emit('disconnect'); } } catch (e) { // 如果不是JSON格式,直接输出(可能是纯文本响应) xterm.value && xterm.value.write(event.data); } }; wsConnection.value.onclose = () => { console.log('WebSocket连接已关闭'); connected.value = false; emit('disconnect'); }; wsConnection.value.onerror = (error) => { console.error('WebSocket连接错误:', error); connected.value = false; emit('error', error); }; // 监听窗口大小变化 window.addEventListener('resize', () => { setTimeout(() => { if (fitAddon.value) { fitAddon.value.fit(); } }); }); } catch (error) { console.error('连接失败:', error); emit('error', error); } }; const disconnect = () => { if (wsConnection.value) { // 发送断开连接消息 const disconnectMsg = { type: 'disconnect' }; wsConnection.value.send(JSON.stringify(disconnectMsg)); wsConnection.value.close(); wsConnection.value = null; } if (xterm.value) { xterm.value.dispose(); xterm.value = null; } connected.value = false; emit('disconnect'); }; onMounted(() => { // 初始化终端尺寸 nextTick(() => { if (fitAddon.value) { fitAddon.value.fit(); } }); }); onUnmounted(() => { disconnect(); }); return { terminal, connect, disconnect, connected }; } }; </script> <style scoped> .webssh-container { display: flex; flex-direction: column; height: 100%; background-color: #1e1e1e; border: 1px solid #333; border-radius: 4px; overflow: hidden; } .terminal-header { display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background-color: #2d2d30; color: #ccc; border-bottom: 1px solid #333; } .connection-info { font-family: monospace; font-size: 12px; } .controls { display: flex; gap: 8px; } .control-btn { background: #3c3c3c; border: 1px solid #555; color: #ccc; border-radius: 4px; padding: 4px 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; width: 30px; height: 30px; } .control-btn:hover { background: #4c4c4c; } .control-btn:disabled { background: #222; color: #666; cursor: not-allowed; } .terminal-wrapper { flex: 1; padding: 10px; overflow: hidden; } .terminal { height: 100%; width: 100%; } .xterm { height: 100%; } .xterm .xterm-viewport { height: 100% !important; } </style> 2.3 🌟导入WebSSH组件到主页面🌟
在需要使用WebSSH功能的页面(例如workspace/index.vue)中导入WebSSH组件:
<WebSSH v-if="showWebSSH" :key="sshComponentKey" :host="sshHost" :port="sshPort" :username="userInfo.username" :password="sshPassword" @connect="onSSHConnect" @disconnect="onSSHDisconnect" @error="onSSHError"> </WebSSH> <script> import WebSSH from '@/components/WebSSH.vue'; </script> 2.4 🌟添加SSH事件处理函数🌟
在页面中添加SSH连接相关的事件处理函数:
// SSH事件处理函数constonSSHConnect=()=>{ console.log('SSH连接成功'); ElMessage.success('SSH连接成功');};constonSSHDisconnect=()=>{ console.log('SSH连接断开'); ElMessage.info('SSH连接已断开'); showWebSSH.value =false;};constonSSHError=(error)=>{ console.error('SSH连接错误:', error); ElMessage.error('SSH连接失败: '+ error.message); showWebSSH.value =false;};3 🌟后端采用Websocket+jsch远程连接终端🌟
本文将详细介绍如何在Spring Boot项目中实现WebSSH功能,使用户可以通过浏览器访问远程SSH服务器。
3.1 🌟添加必要依赖🌟
<!-- SSH Client for WebSSH --><dependency><groupId>com.jcraft</groupId><artifactId>jsch</artifactId><version>0.1.55</version></dependency><!-- Spring WebSocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>3.2 🌟创建 SSH 消息类🌟
创建 SshMessage.java 类来处理前后端通信的数据格式:
importcom.fasterxml.jackson.databind.ObjectMapper;@Data@NoArgsConstructor@AllArgsConstructorpublicclassSshMessage{privateString type;privateString data;privateString host;privateString username;privateString password;privateInteger port;privateInteger cols;privateInteger rows;publicStringtoJson(){try{ObjectMapper mapper =newObjectMapper();return mapper.writeValueAsString(this);}catch(Exception e){ e.printStackTrace();return"{}";}}}3.3 🌟创建WebSocket SSH处理器🌟
创建 WebSocketSshHandler.java 类来处理WebSocket连接和SSH交互:
packagecn.edu.gzpt.blockchain.handler;importcn.edu.gzpt.blockchain.handler.SshMessage;importcom.jcraft.jsch.ChannelShell;importcom.jcraft.jsch.JSch;importcom.jcraft.jsch.JSchException;importcom.jcraft.jsch.Session;importorg.springframework.stereotype.Component;importorg.springframework.web.socket.*;importjava.io.IOException;importjava.util.concurrent.ConcurrentHashMap;/** * WebSocket SSH处理器,用于处理WebSSH连接请求和数据转发 * 实现WebSocketHandler接口,处理前端WebSocket连接与后端SSH连接之间的数据传输 */@ComponentpublicclassWebSocketSshHandlerimplementsWebSocketHandler{/** * 存储WebSocket会话ID与SSH会话的映射关系 */privatefinalConcurrentHashMap<String,Session> sshSessions =newConcurrentHashMap<>();/** * 存储WebSocket会话ID与SSH Shell通道的映射关系 */privatefinalConcurrentHashMap<String,ChannelShell> shellChannels =newConcurrentHashMap<>();/** * 存储WebSocket会话ID与SSH输出读取线程的映射关系 */privatefinalConcurrentHashMap<String,Thread> readerThreads =newConcurrentHashMap<>();/** * WebSocket连接建立后的回调方法 * 记录连接建立日志 * * @param session WebSocket会话对象 * @throws Exception 异常 */@OverridepublicvoidafterConnectionEstablished(WebSocketSession session)throwsException{System.out.println("WebSocket连接已建立: "+ session.getId());}/** * 处理WebSocket消息的核心方法 * 根据消息类型分发到相应的处理方法 * * @param webSocketSession WebSocket会话对象 * @param message WebSocket消息对象 * @throws Exception 异常 */@OverridepublicvoidhandleMessage(WebSocketSession webSocketSession,WebSocketMessage<?> message)throwsException{String payload =(String) message.getPayload();try{// 解析来自前端的消息SshMessage sshMessage =parseMessage(payload);switch(sshMessage.getType()){case"connect":handleConnect(webSocketSession, sshMessage);break;case"stdin":handleStdin(webSocketSession, sshMessage);break;case"resize":handleResize(webSocketSession, sshMessage);break;case"disconnect":handleDisconnect(webSocketSession);break;default:sendError(webSocketSession,"未知的消息类型: "+ sshMessage.getType());}}catch(Exception e){sendError(webSocketSession,"处理消息时发生错误: "+ e.getMessage()); e.printStackTrace();}}/** * WebSocket传输错误处理方法 * 记录错误日志并断开连接 * * @param session WebSocket会话对象 * @param exception 传输异常对象 * @throws Exception 异常 */@OverridepublicvoidhandleTransportError(WebSocketSession session,Throwable exception)throwsException{System.out.println("WebSocket传输错误: "+ exception.getMessage());handleDisconnect(session);}/** * WebSocket连接关闭后的回调方法 * 记录关闭日志并清理资源 * * @param session WebSocket会话对象 * @param status 连接关闭状态 * @throws Exception 异常 */@OverridepublicvoidafterConnectionClosed(WebSocketSession session,CloseStatus status)throwsException{System.out.println("WebSocket连接已关闭: "+ session.getId()+", 状态: "+ status);handleDisconnect(session);}/** * 判断是否支持部分消息 * * @return 是否支持部分消息,此处返回false表示不支持 */@OverridepublicbooleansupportsPartialMessages(){returnfalse;}/** * 处理SSH连接请求 * 根据前端传递的SSH连接参数建立SSH连接 * * @param session WebSocket会话对象 * @param message 包含SSH连接参数的消息对象 * @throws JSchException SSH连接异常 */privatevoidhandleConnect(WebSocketSession session,SshMessage message)throwsJSchException{String sessionId = session.getId();String host = message.getHost();int port = message.getPort();String username = message.getUsername();String password = message.getPassword();if(host ==null|| username ==null|| password ==null){sendError(session,"缺少必要的SSH连接参数");return;}// 创建JSch实例JSch jsch =newJSch();Session sshSession = jsch.getSession(username, host, port); sshSession.setPassword(password);// 设置不验证主机密钥 sshSession.setConfig("StrictHostKeyChecking","no"); sshSession.connect(30000);// 30秒超时// 打开shell通道ChannelShell channel =(ChannelShell) sshSession.openChannel("shell"); channel.setPty(true); channel.connect();// 存储会话 sshSessions.put(sessionId, sshSession); shellChannels.put(sessionId, channel);// 启动读取线程Thread readerThread =newThread(()->{try{byte[] buffer =newbyte[1024];int i;while((i = channel.getInputStream().read(buffer))!=-1){String output =newString(buffer,0, i);sendToWebSocket(session,"stdout", output);}}catch(IOException e){System.err.println("读取SSH输出时发生错误: "+ e.getMessage());try{ session.close();}catch(IOException ioException){ ioException.printStackTrace();}}}); readerThread.setDaemon(true); readerThread.start(); readerThreads.put(sessionId, readerThread);// 发送连接成功消息sendToWebSocket(session,"connected","SSH连接已建立");}/** * 处理来自前端的键盘输入数据 * 将前端发送的键盘输入数据转发到SSH服务器 * * @param session WebSocket会话对象 * @param message 包含输入数据的消息对象 * @throws IOException IO异常 */privatevoidhandleStdin(WebSocketSession session,SshMessage message)throwsIOException{String data = message.getData();String sessionId = session.getId();ChannelShell channel = shellChannels.get(sessionId);if(channel !=null&& channel.isConnected()){ channel.getOutputStream().write(data.getBytes("UTF-8")); channel.getOutputStream().flush();}else{sendError(session,"SSH连接未建立或已断开");}}/** * 处理终端窗口大小调整请求 * 根据前端发送的窗口大小调整SSH终端尺寸 * * @param session WebSocket会话对象 * @param message 包含窗口尺寸信息的消息对象 */privatevoidhandleResize(WebSocketSession session,SshMessage message){String sessionId = session.getId();ChannelShell channel = shellChannels.get(sessionId);if(channel !=null&& channel.isConnected()){int cols = message.getCols();int rows = message.getRows();if(cols >0&& rows >0){ channel.setPtySize(cols, rows,640,480);}}}/** * 处理断开连接请求 * 清理所有与当前WebSocket会话关联的资源 * * @param session WebSocket会话对象 */privatevoidhandleDisconnect(WebSocketSession session){String sessionId = session.getId();// 关闭读取线程Thread readerThread = readerThreads.remove(sessionId);if(readerThread !=null){ readerThread.interrupt();}// 关闭shell通道ChannelShell channel = shellChannels.remove(sessionId);if(channel !=null&& channel.isConnected()){ channel.disconnect();}// 关闭SSH会话Session sshSession = sshSessions.remove(sessionId);if(sshSession !=null&& sshSession.isConnected()){ sshSession.disconnect();}}/** * 向WebSocket客户端发送消息 * * @param session WebSocket会话对象 * @param type 消息类型 * @param data 消息数据 */privatevoidsendToWebSocket(WebSocketSession session,String type,String data){try{if(session.isOpen()){SshMessage response =newSshMessage(type, data); session.sendMessage(newTextMessage(response.toJson()));}}catch(IOException e){System.err.println("发送WebSocket消息失败: "+ e.getMessage());}}/** * 向WebSocket客户端发送错误消息 * * @param session WebSocket会话对象 * @param errorMessage 错误消息内容 */privatevoidsendError(WebSocketSession session,String errorMessage){try{if(session.isOpen()){SshMessage errorMsg =newSshMessage("error", errorMessage); session.sendMessage(newTextMessage(errorMsg.toJson()));}}catch(IOException e){System.err.println("发送错误消息失败: "+ e.getMessage());}}/** * 解析前端发送的JSON格式消息 * 提取消息类型、数据及SSH连接参数 * * @param json 待解析的JSON字符串 * @return 解析后的SshMessage对象 */privateSshMessageparseMessage(String json){// 简单解析JSON字符串// 在实际项目中建议使用Jackson等库进行解析try{String type =extractJsonValue(json,"type");String data =extractJsonValue(json,"data");String host =extractJsonValue(json,"host");String username =extractJsonValue(json,"username");String password =extractJsonValue(json,"password");Integer port =null;String portStr =extractJsonValue(json,"port");if(portStr !=null&&!portStr.isEmpty()){try{ port =Integer.parseInt(portStr);}catch(NumberFormatException e){// 忽略错误,保持null}}Integer cols =null;String colsStr =extractJsonValue(json,"cols");if(colsStr !=null&&!colsStr.isEmpty()){try{ cols =Integer.parseInt(colsStr);}catch(NumberFormatException e){// 忽略错误,保持null}}Integer rows =null;String rowsStr =extractJsonValue(json,"rows");if(rowsStr !=null&&!rowsStr.isEmpty()){try{ rows =Integer.parseInt(rowsStr);}catch(NumberFormatException e){// 忽略错误,保持null}}returnnewSshMessage(type, data, host, username, password, port, cols, rows);}catch(Exception e){thrownewRuntimeException("解析消息失败: "+ e.getMessage());}}/** * 从JSON字符串中提取指定键的值 * 使用正则表达式解析JSON字符串 * * @param json JSON字符串 * @param key 要提取的键名 * @return 对应键的值,如果不存在则返回null */privateStringextractJsonValue(String json,String key){String pattern ="\""+ key +"\":\"([^\"]*)\"";java.util.regex.Pattern p =java.util.regex.Pattern.compile(pattern);java.util.regex.Matcher m = p.matcher(json);if(m.find()){return m.group(1);}// 尝试匹配数字值 pattern ="\""+ key +"\":([0-9]+)"; p =java.util.regex.Pattern.compile(pattern); m = p.matcher(json);if(m.find()){return m.group(1);}returnnull;}}上述代码主要是WebSocket SSH处理器,实现远程SSH终端功能。通过WebSocket接收前端消息,支持SSH连接、命令执行、终端重定向和断开连接操作。使用JSch库建立SSH连接,维护会话状态,并将SSH输出实时推送回前端显示。
3.4 🌟配置Websocket端点🌟
创建 WebSocketConfig.java 配置类来注册WebSocket处理器:
importcn.edu.gzpt.blockchain.handler.WebSocketSshHandler;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.annotation.Configuration;importorg.springframework.web.socket.config.annotation.EnableWebSocket;importorg.springframework.web.socket.config.annotation.WebSocketConfigurer;importorg.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;@Configuration@EnableWebSocketpublicclassWebSocketConfigimplementsWebSocketConfigurer{@AutowiredprivateWebSocketSshHandler webSocketSshHandler;@OverridepublicvoidregisterWebSocketHandlers(WebSocketHandlerRegistry registry){ registry.addHandler(webSocketSshHandler,"/ws/ssh").setAllowedOrigins("*");// 生产环境中应设置具体域名}}4 🌟效果图🌟

☀️☀️这里是skywalker的博客小记,如果你喜欢我的文章,可以点赞支持一下博主,感谢您对自由的支持~,如果有其他想要探索的内容,还请留言在评论区~