什么是 WebSSH?
WebSSH 是基于浏览器的 SSH 客户端,依托 WebSocket+HTTPS 加密传输,无需本地安装工具即可远程管理服务器。其优势在于跨平台兼容各类终端设备,支持集中权限管控与操作审计,能绕过部分防火墙限制,部署灵活且可与运维平台快速集成,大幅降低管理成本与使用门槛。
本教程将详细介绍如何在 Vue 项目中集成 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>{{ host }}</span>:<span>{{ port }}</span>
</div>
<div class="controls">
<!-- 连接/断开按钮 -->
<button @click="connect" :disabled="connected">
<i></i>
</button>
<button @click="disconnect" :disabled="!connected">
<i></i>
</button>
</div>
</div>
<div class="terminal-wrapper">
<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: props.username,
password: props.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>



