完成SSH连接操作功能
This commit is contained in:
parent
f06c2fd68f
commit
97ed24f592
@ -100,6 +100,15 @@
|
|||||||
<artifactId>influxdb-client-java</artifactId>
|
<artifactId>influxdb-client-java</artifactId>
|
||||||
<version>6.6.0</version>
|
<version>6.6.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.jcraft</groupId>
|
||||||
|
<artifactId>jsch</artifactId>
|
||||||
|
<version>0.1.55</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<profiles>
|
<profiles>
|
||||||
|
@ -53,6 +53,7 @@ public class SecurityConfiguration {
|
|||||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
return http
|
return http
|
||||||
.authorizeHttpRequests(conf -> conf
|
.authorizeHttpRequests(conf -> conf
|
||||||
|
.requestMatchers("/terminal/**").permitAll()
|
||||||
.requestMatchers("/api/auth/**", "/error").permitAll()
|
.requestMatchers("/api/auth/**", "/error").permitAll()
|
||||||
.requestMatchers("/monitor/**").permitAll()
|
.requestMatchers("/monitor/**").permitAll()
|
||||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package com.example.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class WebSocketConfiguration {
|
||||||
|
@Bean
|
||||||
|
public ServerEndpointExporter serverEndpointExporter() {
|
||||||
|
return new ServerEndpointExporter();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,155 @@
|
|||||||
|
package com.example.websocket;
|
||||||
|
|
||||||
|
import com.example.entity.dto.ClientDetail;
|
||||||
|
import com.example.entity.dto.ClientSsh;
|
||||||
|
import com.example.mapper.ClientDetailMapper;
|
||||||
|
import com.example.mapper.ClientSshMapper;
|
||||||
|
import com.jcraft.jsch.ChannelShell;
|
||||||
|
import com.jcraft.jsch.JSch;
|
||||||
|
import com.jcraft.jsch.JSchException;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.websocket.*;
|
||||||
|
import jakarta.websocket.server.PathParam;
|
||||||
|
import jakarta.websocket.server.ServerEndpoint;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Component
|
||||||
|
@ServerEndpoint("/terminal/{clientId}")
|
||||||
|
public class TerminalWebSocket {
|
||||||
|
|
||||||
|
private static ClientDetailMapper detailMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
public void setDetailMapper(ClientDetailMapper detailMapper) {
|
||||||
|
TerminalWebSocket.detailMapper = detailMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ClientSshMapper sshMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
public void setSshMapper(ClientSshMapper sshMapper) {
|
||||||
|
TerminalWebSocket.sshMapper = sshMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final Map<Session, Shell> sessionMap = new ConcurrentHashMap<>();
|
||||||
|
private final ExecutorService service = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
@OnOpen
|
||||||
|
public void onOpen(Session session,
|
||||||
|
@PathParam(value = "clientId") String clientId) throws Exception {
|
||||||
|
ClientDetail detail = detailMapper.selectById(clientId);
|
||||||
|
ClientSsh ssh = sshMapper.selectById(clientId);
|
||||||
|
if(detail == null || ssh == null) {
|
||||||
|
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, "无法识别此主机"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(this.createSshConnection(session, ssh, detail.getIp())) {
|
||||||
|
log.info("主机 {} 的SSH连接已创建", detail.getIp());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnMessage
|
||||||
|
public void onMessage(Session session, String message) throws IOException {
|
||||||
|
Shell shell = sessionMap.get(session);
|
||||||
|
OutputStream output = shell.output;
|
||||||
|
output.write(message.getBytes(StandardCharsets.UTF_8));
|
||||||
|
output.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnClose
|
||||||
|
public void onClose(Session session) throws IOException {
|
||||||
|
Shell shell = sessionMap.get(session);
|
||||||
|
if(shell != null) {
|
||||||
|
shell.close();
|
||||||
|
sessionMap.remove(session);
|
||||||
|
log.info("主机 {} 的SSH连接已断开", shell.js.getHost());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OnError
|
||||||
|
public void onError(Session session, Throwable error) throws IOException {
|
||||||
|
log.error("用户WebSocket连接出现错误", error);
|
||||||
|
session.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean createSshConnection(Session session, ClientSsh ssh, String ip) throws IOException{
|
||||||
|
try {
|
||||||
|
JSch jSch = new JSch();
|
||||||
|
com.jcraft.jsch.Session js = jSch.getSession(ssh.getUsername(), ip, ssh.getPort());
|
||||||
|
js.setPassword(ssh.getPassword());
|
||||||
|
js.setConfig("StrictHostKeyChecking", "no");
|
||||||
|
js.setTimeout(3000);
|
||||||
|
js.connect();
|
||||||
|
ChannelShell channel = (ChannelShell) js.openChannel("shell");
|
||||||
|
channel.setPtyType("xterm");
|
||||||
|
channel.connect(1000);
|
||||||
|
sessionMap.put(session, new Shell(session, js, channel));
|
||||||
|
return true;
|
||||||
|
} catch (JSchException e) {
|
||||||
|
String message = e.getMessage();
|
||||||
|
if(message.equals("Auth fail")) {
|
||||||
|
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT,
|
||||||
|
"登录SSH失败,用户名或密码错误"));
|
||||||
|
log.error("连接SSH失败,用户名或密码错误,登录失败");
|
||||||
|
} else if(message.contains("Connection refused")) {
|
||||||
|
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT,
|
||||||
|
"连接被拒绝,可能是没有启动SSH服务或是放开端口"));
|
||||||
|
log.error("连接SSH失败,连接被拒绝,可能是没有启动SSH服务或是放开端口");
|
||||||
|
} else {
|
||||||
|
session.close(new CloseReason(CloseReason.CloseCodes.CANNOT_ACCEPT, message));
|
||||||
|
log.error("连接SSH时出现错误", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Shell {
|
||||||
|
private final Session session;
|
||||||
|
private final com.jcraft.jsch.Session js;
|
||||||
|
private final ChannelShell channel;
|
||||||
|
private final InputStream input;
|
||||||
|
private final OutputStream output;
|
||||||
|
|
||||||
|
public Shell(Session session, com.jcraft.jsch.Session js, ChannelShell channel) throws IOException {
|
||||||
|
this.js = js;
|
||||||
|
this.session = session;
|
||||||
|
this.channel = channel;
|
||||||
|
this.input = channel.getInputStream();
|
||||||
|
this.output = channel.getOutputStream();
|
||||||
|
service.submit(this::read);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void read() {
|
||||||
|
try {
|
||||||
|
byte[] buffer = new byte[1024 * 1024];
|
||||||
|
int i;
|
||||||
|
while ((i = input.read(buffer)) != -1) {
|
||||||
|
String text = new String(Arrays.copyOfRange(buffer, 0, i), StandardCharsets.UTF_8);
|
||||||
|
session.getBasicRemote().sendText(text);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("读取SSH输入流时出现问题", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
input.close();
|
||||||
|
output.close();
|
||||||
|
channel.disconnect();
|
||||||
|
js.disconnect();
|
||||||
|
service.shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
itbaima-monitor-web/package-lock.json
generated
17
itbaima-monitor-web/package-lock.json
generated
@ -17,7 +17,9 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0",
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.4"
|
"vue-router": "^4.2.4",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-addon-attach": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
@ -1959,6 +1961,19 @@
|
|||||||
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
|
"integrity": "sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
|
||||||
|
},
|
||||||
|
"node_modules/xterm-addon-attach": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/xterm-addon-attach/-/xterm-addon-attach-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-NykWWOsobVZPPK3P9eFkItrnBK9Lw0f94uey5zhqIVB1bhswdVBfl+uziEzSOhe2h0rT9wD0wOeAYsdSXeavPw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zrender": {
|
"node_modules/zrender": {
|
||||||
"version": "5.4.4",
|
"version": "5.4.4",
|
||||||
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz",
|
"resolved": "https://registry.npmmirror.com/zrender/-/zrender-5.4.4.tgz",
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pinia-plugin-persistedstate": "^3.2.0",
|
"pinia-plugin-persistedstate": "^3.2.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.4"
|
"vue-router": "^4.2.4",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-addon-attach": "^0.9.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.2.3",
|
"@vitejs/plugin-vue": "^4.2.3",
|
||||||
|
59
itbaima-monitor-web/src/component/Terminal.vue
Normal file
59
itbaima-monitor-web/src/component/Terminal.vue
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
<script setup>
|
||||||
|
import {onBeforeUnmount, onMounted, ref} from "vue";
|
||||||
|
import {ElMessage} from "element-plus";
|
||||||
|
import {AttachAddon} from "xterm-addon-attach/src/AttachAddon";
|
||||||
|
import {Terminal} from "xterm";
|
||||||
|
import "xterm/css/xterm.css";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
id: Number
|
||||||
|
})
|
||||||
|
const emits = defineEmits(['dispose'])
|
||||||
|
|
||||||
|
const terminalRef = ref()
|
||||||
|
|
||||||
|
const socket = new WebSocket(`ws://127.0.0.1:8080/terminal/${props.id}`)
|
||||||
|
socket.onclose = evt => {
|
||||||
|
if(evt.code !== 1000) {
|
||||||
|
ElMessage.warning(`连接失败: ${evt.reason}`)
|
||||||
|
} else {
|
||||||
|
ElMessage.success('远程SSH连接已断开')
|
||||||
|
}
|
||||||
|
emits('dispose')
|
||||||
|
}
|
||||||
|
const attachAddon = new AttachAddon(socket);
|
||||||
|
const term = new Terminal({
|
||||||
|
lineHeight: 1.2,
|
||||||
|
rows: 20,
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
||||||
|
fontWeight: "bold",
|
||||||
|
theme: {
|
||||||
|
background: '#000000'
|
||||||
|
},
|
||||||
|
// 光标闪烁
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'underline',
|
||||||
|
scrollback: 100,
|
||||||
|
tabStopWidth: 4,
|
||||||
|
});
|
||||||
|
term.loadAddon(attachAddon);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
term.open(terminalRef.value)
|
||||||
|
term.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
socket.close()
|
||||||
|
term.dispose()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ref="terminalRef" class="xterm"/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
</style>
|
@ -2,6 +2,7 @@
|
|||||||
import {reactive, ref, watch} from "vue";
|
import {reactive, ref, watch} from "vue";
|
||||||
import {get, post} from "@/net";
|
import {get, post} from "@/net";
|
||||||
import {ElMessage} from "element-plus";
|
import {ElMessage} from "element-plus";
|
||||||
|
import Terminal from "@/component/Terminal.vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: Number
|
id: Number
|
||||||
@ -26,6 +27,7 @@ const rules = {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const state = ref(1)
|
||||||
const formRef = ref()
|
const formRef = ref()
|
||||||
|
|
||||||
function saveConnection() {
|
function saveConnection() {
|
||||||
@ -34,14 +36,13 @@ function saveConnection() {
|
|||||||
post('/api/monitor/ssh-save', {
|
post('/api/monitor/ssh-save', {
|
||||||
...connection,
|
...connection,
|
||||||
id: props.id
|
id: props.id
|
||||||
}, () => {
|
}, () => state.value = 2)
|
||||||
ElMessage.success('正在连接')
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.id, id => {
|
watch(() => props.id, id => {
|
||||||
|
state.value = 1
|
||||||
if(id !== -1) {
|
if(id !== -1) {
|
||||||
connection.ip = ''
|
connection.ip = ''
|
||||||
get(`/api/monitor/ssh?clientId=${id}`, data => Object.assign(connection, data))
|
get(`/api/monitor/ssh?clientId=${id}`, data => Object.assign(connection, data))
|
||||||
@ -51,7 +52,7 @@ watch(() => props.id, id => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="terminal-main">
|
<div class="terminal-main">
|
||||||
<div class="login" v-loading="!connection.ip">
|
<div class="login" v-loading="!connection.ip" v-if="state === 1">
|
||||||
<i style="font-size: 50px" class="fa-solid fa-terminal"></i>
|
<i style="font-size: 50px" class="fa-solid fa-terminal"></i>
|
||||||
<div style="margin-top: 10px;font-weight: bold;font-size: 20px">服务端连接信息</div>
|
<div style="margin-top: 10px;font-weight: bold;font-size: 20px">服务端连接信息</div>
|
||||||
<el-form style="width: 400px;margin: 20px auto" :model="connection"
|
<el-form style="width: 400px;margin: 20px auto" :model="connection"
|
||||||
@ -73,6 +74,11 @@ watch(() => props.id, id => {
|
|||||||
<el-button type="success" @click="saveConnection" plain>立即连接</el-button>
|
<el-button type="success" @click="saveConnection" plain>立即连接</el-button>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="state === 2">
|
||||||
|
<div style="overflow: hidden;padding: 0 10px 10px 10px">
|
||||||
|
<terminal :id="id" @dispose="state = 1"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -101,7 +101,8 @@ const terminal = reactive({
|
|||||||
style="width: 600px;margin: 10px auto" size="320" @open="refreshToken">
|
style="width: 600px;margin: 10px auto" size="320" @open="refreshToken">
|
||||||
<register-card :token="register.token"/>
|
<register-card :token="register.token"/>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
<el-drawer style="width: 800px" :size="500" direction="btt"
|
<el-drawer style="width: 800px" :size="520" direction="btt"
|
||||||
|
@close="terminal.id = -1"
|
||||||
v-model="terminal.show" :close-on-click-modal="false">
|
v-model="terminal.show" :close-on-click-modal="false">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div>
|
<div>
|
||||||
@ -117,6 +118,10 @@ const terminal = reactive({
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
:deep(.el-drawer__header) {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.el-checkbox-group .el-checkbox) {
|
:deep(.el-checkbox-group .el-checkbox) {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user