欢迎光临
我们一直在努力

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 class="webssh-container">
    <div class="terminal-header">
    <div class="connection-info">
    <span class="host">{{ host }}</span>:<span class="port">{{ port }}</span>
    </div>
    <div class="controls">
    <!– 连接/断开按钮 –>
    <button class="control-btn" @click="connect" :disabled="connected">
    <i class="fas fa-plug"></i>
    </button>
    <button class="control-btn" @click="disconnect" :disabled="!connected">
    <i class="fas fa-power-off"></i>
    </button>
    </div>
    </div>
    <div class="terminal-wrapper">
    <div ref="terminal" class="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"
    class="right-content"
    id="containerFrame">
    </WebSSH>

    <script>
    import WebSSH from '@/components/WebSSH.vue';
    </script>

    2.4 🌟添加SSH事件处理函数🌟

    在页面中添加SSH连接相关的事件处理函数:

    // SSH事件处理函数
    const onSSHConnect = () => {
    console.log('SSH连接成功');
    ElMessage.success('SSH连接成功');
    };

    const onSSHDisconnect = () => {
    console.log('SSH连接断开');
    ElMessage.info('SSH连接已断开');
    showWebSSH.value = false;
    };

    const onSSHError = (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 类来处理前后端通信的数据格式:

    import com.fasterxml.jackson.databind.ObjectMapper;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class SshMessage {
    private String type;
    private String data;
    private String host;
    private String username;
    private String password;
    private Integer port;
    private Integer cols;
    private Integer rows;

    public String toJson() {
    try {
    ObjectMapper mapper = new ObjectMapper();
    return mapper.writeValueAsString(this);
    } catch (Exception e) {
    e.printStackTrace();
    return "{}";
    }
    }
    }

    3.3 🌟创建WebSocket SSH处理器🌟

    创建 WebSocketSshHandler.java 类来处理WebSocket连接和SSH交互:

    package cn.edu.gzpt.blockchain.handler;
    import cn.edu.gzpt.blockchain.handler.SshMessage;
    import com.jcraft.jsch.ChannelShell;
    import com.jcraft.jsch.JSch;
    import com.jcraft.jsch.JSchException;
    import com.jcraft.jsch.Session;
    import org.springframework.stereotype.Component;
    import org.springframework.web.socket.*;

    import java.io.IOException;
    import java.util.concurrent.ConcurrentHashMap;

    /**
    * WebSocket SSH处理器,用于处理WebSSH连接请求和数据转发
    * 实现WebSocketHandler接口,处理前端WebSocket连接与后端SSH连接之间的数据传输
    */

    @Component
    public class WebSocketSshHandler implements WebSocketHandler {

    /**
    * 存储WebSocket会话ID与SSH会话的映射关系
    */

    private final ConcurrentHashMap<String, Session> sshSessions = new ConcurrentHashMap<>();

    /**
    * 存储WebSocket会话ID与SSH Shell通道的映射关系
    */

    private final ConcurrentHashMap<String, ChannelShell> shellChannels = new ConcurrentHashMap<>();

    /**
    * 存储WebSocket会话ID与SSH输出读取线程的映射关系
    */

    private final ConcurrentHashMap<String, Thread> readerThreads = new ConcurrentHashMap<>();

    /**
    * WebSocket连接建立后的回调方法
    * 记录连接建立日志
    *
    * @param session WebSocket会话对象
    * @throws Exception 异常
    */

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
    System.out.println("WebSocket连接已建立: " + session.getId());
    }

    /**
    * 处理WebSocket消息的核心方法
    * 根据消息类型分发到相应的处理方法
    *
    * @param webSocketSession WebSocket会话对象
    * @param message WebSocket消息对象
    * @throws Exception 异常
    */

    @Override
    public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> message) throws Exception {
    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 异常
    */

    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
    System.out.println("WebSocket传输错误: " + exception.getMessage());
    handleDisconnect(session);
    }

    /**
    * WebSocket连接关闭后的回调方法
    * 记录关闭日志并清理资源
    *
    * @param session WebSocket会话对象
    * @param status 连接关闭状态
    * @throws Exception 异常
    */

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
    System.out.println("WebSocket连接已关闭: " + session.getId() + ", 状态: " + status);
    handleDisconnect(session);
    }

    /**
    * 判断是否支持部分消息
    *
    * @return 是否支持部分消息,此处返回false表示不支持
    */

    @Override
    public boolean supportsPartialMessages() {
    return false;
    }

    /**
    * 处理SSH连接请求
    * 根据前端传递的SSH连接参数建立SSH连接
    *
    * @param session WebSocket会话对象
    * @param message 包含SSH连接参数的消息对象
    * @throws JSchException SSH连接异常
    */

    private void handleConnect(WebSocketSession session, SshMessage message) throws JSchException {
    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 = new JSch();
    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 = new Thread(() -> {
    try {
    byte[] buffer = new byte[1024];
    int i;
    while ((i = channel.getInputStream().read(buffer)) != 1) {
    String output = new String(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异常
    */

    private void handleStdin(WebSocketSession session, SshMessage message) throws IOException {
    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 包含窗口尺寸信息的消息对象
    */

    private void handleResize(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会话对象
    */

    private void handleDisconnect(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 消息数据
    */

    private void sendToWebSocket(WebSocketSession session, String type, String data) {
    try {
    if (session.isOpen()) {
    SshMessage response = new SshMessage(type, data);
    session.sendMessage(new TextMessage(response.toJson()));
    }
    } catch (IOException e) {
    System.err.println("发送WebSocket消息失败: " + e.getMessage());
    }
    }

    /**
    * 向WebSocket客户端发送错误消息
    *
    * @param session WebSocket会话对象
    * @param errorMessage 错误消息内容
    */

    private void sendError(WebSocketSession session, String errorMessage) {
    try {
    if (session.isOpen()) {
    SshMessage errorMsg = new SshMessage("error", errorMessage);
    session.sendMessage(new TextMessage(errorMsg.toJson()));
    }
    } catch (IOException e) {
    System.err.println("发送错误消息失败: " + e.getMessage());
    }
    }

    /**
    * 解析前端发送的JSON格式消息
    * 提取消息类型、数据及SSH连接参数
    *
    * @param json 待解析的JSON字符串
    * @return 解析后的SshMessage对象
    */

    private SshMessage parseMessage(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
    }
    }

    return new SshMessage(type, data, host, username, password, port, cols, rows);
    } catch (Exception e) {
    throw new RuntimeException("解析消息失败: " + e.getMessage());
    }
    }

    /**
    * 从JSON字符串中提取指定键的值
    * 使用正则表达式解析JSON字符串
    *
    * @param json JSON字符串
    * @param key 要提取的键名
    * @return 对应键的值,如果不存在则返回null
    */

    private String extractJsonValue(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);
    }

    return null;
    }
    }

    上述代码主要是WebSocket SSH处理器,实现远程SSH终端功能。通过WebSocket接收前端消息,支持SSH连接、命令执行、终端重定向和断开连接操作。使用JSch库建立SSH连接,维护会话状态,并将SSH输出实时推送回前端显示。

    3.4 🌟配置Websocket端点🌟

    创建 WebSocketConfig.java 配置类来注册WebSocket处理器:

    import cn.edu.gzpt.blockchain.handler.WebSocketSshHandler;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.socket.config.annotation.EnableWebSocket;
    import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
    import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

    @Configuration
    @EnableWebSocket
    public class WebSocketConfig implements WebSocketConfigurer {

    @Autowired
    private WebSocketSshHandler webSocketSshHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry.addHandler(webSocketSshHandler, "/ws/ssh")
    .setAllowedOrigins("*"); // 生产环境中应设置具体域名
    }
    }

    4 🌟效果图🌟

    在这里插入图片描述

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

    赞(0)
    未经允许不得转载:171主机测评 » springboot项目中如何集成Webssh连接服务器?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址