添加SSH连接信息存储

This commit is contained in:
柏码の讲师 2023-12-04 22:42:54 +08:00
parent 4647fc030a
commit da711b157b
14 changed files with 292 additions and 30 deletions

View File

@ -99,6 +99,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>

View File

@ -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()

View File

@ -5,10 +5,8 @@ import com.example.entity.dto.Account;
import com.example.entity.vo.request.RenameClientVO; import com.example.entity.vo.request.RenameClientVO;
import com.example.entity.vo.request.RenameNodeVO; import com.example.entity.vo.request.RenameNodeVO;
import com.example.entity.vo.request.RuntimeDetailVO; import com.example.entity.vo.request.RuntimeDetailVO;
import com.example.entity.vo.response.ClientDetailsVO; import com.example.entity.vo.request.SshConnectionVO;
import com.example.entity.vo.response.ClientPreviewVO; import com.example.entity.vo.response.*;
import com.example.entity.vo.response.ClientSimpleVO;
import com.example.entity.vo.response.RuntimeDetailsVO;
import com.example.service.AccountService; import com.example.service.AccountService;
import com.example.service.ClientService; import com.example.service.ClientService;
import com.example.utils.Const; import com.example.utils.Const;
@ -128,6 +126,29 @@ public class MonitorController {
} }
} }
@PostMapping("/ssh-save")
public RestBean<Void> saveSshConnection(@RequestBody @Valid SshConnectionVO vo,
@RequestAttribute(Const.ATTR_USER_ID) int userId,
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
if(this.permissionCheck(userId, userRole, vo.getId())) {
service.saveClientSshConnection(vo);
return RestBean.success();
} else {
return RestBean.noPermission();
}
}
@GetMapping("/ssh")
public RestBean<SshSettingsVO> sshSettings(int clientId,
@RequestAttribute(Const.ATTR_USER_ID) int userId,
@RequestAttribute(Const.ATTR_USER_ROLE) String userRole) {
if(this.permissionCheck(userId, userRole, clientId)) {
return RestBean.success(service.sshSettings(clientId));
} else {
return RestBean.noPermission();
}
}
private List<Integer> accountAccessClients(int uid) { private List<Integer> accountAccessClients(int uid) {
Account account = accountService.getById(uid); Account account = accountService.getById(uid);
return account.getClientList(); return account.getClientList();

View File

@ -0,0 +1,16 @@
package com.example.entity.dto;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.entity.BaseData;
import lombok.Data;
@Data
@TableName("db_client_ssh")
public class ClientSsh implements BaseData {
@TableId
Integer id;
Integer port;
String username;
String password;
}

View File

@ -0,0 +1,18 @@
package com.example.entity.vo.request;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
@Data
public class SshConnectionVO {
int id;
@NotNull
int port;
@NotNull
@Length(min = 1)
String username;
@NotNull
@Length(min = 1)
String password;
}

View File

@ -0,0 +1,11 @@
package com.example.entity.vo.response;
import lombok.Data;
@Data
public class SshSettingsVO {
String ip;
int port = 22;
String username;
String password;
}

View File

@ -0,0 +1,9 @@
package com.example.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.dto.ClientSsh;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ClientSshMapper extends BaseMapper<ClientSsh> {
}

View File

@ -2,14 +2,8 @@ package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.dto.Client; import com.example.entity.dto.Client;
import com.example.entity.vo.request.ClientDetailVO; import com.example.entity.vo.request.*;
import com.example.entity.vo.request.RenameClientVO; import com.example.entity.vo.response.*;
import com.example.entity.vo.request.RenameNodeVO;
import com.example.entity.vo.request.RuntimeDetailVO;
import com.example.entity.vo.response.ClientDetailsVO;
import com.example.entity.vo.response.ClientPreviewVO;
import com.example.entity.vo.response.ClientSimpleVO;
import com.example.entity.vo.response.RuntimeDetailsVO;
import java.util.List; import java.util.List;
@ -28,4 +22,6 @@ public interface ClientService extends IService<Client> {
RuntimeDetailsVO clientRuntimeDetailsHistory(int clientId); RuntimeDetailsVO clientRuntimeDetailsHistory(int clientId);
RuntimeDetailVO clientRuntimeDetailsNow(int clientId); RuntimeDetailVO clientRuntimeDetailsNow(int clientId);
void deleteClient(int clientId); void deleteClient(int clientId);
void saveClientSshConnection(SshConnectionVO vo);
SshSettingsVO sshSettings(int clientId);
} }

View File

@ -0,0 +1,4 @@
package com.example.service;
public interface TerminalService {
}

View File

@ -4,16 +4,12 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.dto.Client; import com.example.entity.dto.Client;
import com.example.entity.dto.ClientDetail; import com.example.entity.dto.ClientDetail;
import com.example.entity.vo.request.ClientDetailVO; import com.example.entity.dto.ClientSsh;
import com.example.entity.vo.request.RenameClientVO; import com.example.entity.vo.request.*;
import com.example.entity.vo.request.RenameNodeVO; import com.example.entity.vo.response.*;
import com.example.entity.vo.request.RuntimeDetailVO;
import com.example.entity.vo.response.ClientDetailsVO;
import com.example.entity.vo.response.ClientPreviewVO;
import com.example.entity.vo.response.ClientSimpleVO;
import com.example.entity.vo.response.RuntimeDetailsVO;
import com.example.mapper.ClientDetailMapper; import com.example.mapper.ClientDetailMapper;
import com.example.mapper.ClientMapper; import com.example.mapper.ClientMapper;
import com.example.mapper.ClientSshMapper;
import com.example.service.ClientService; import com.example.service.ClientService;
import com.example.utils.InfluxDbUtils; import com.example.utils.InfluxDbUtils;
import jakarta.annotation.PostConstruct; import jakarta.annotation.PostConstruct;
@ -37,6 +33,9 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
@Resource @Resource
private ClientDetailMapper detailMapper; private ClientDetailMapper detailMapper;
@Resource
private ClientSshMapper sshMapper;
@Resource @Resource
private InfluxDbUtils influx; private InfluxDbUtils influx;
@ -163,6 +162,33 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
lastRuntime.remove(clientId); lastRuntime.remove(clientId);
} }
@Override
public void saveClientSshConnection(SshConnectionVO vo) {
Client client = this.getById(vo.getId());
if(client == null) return;
ClientSsh ssh = new ClientSsh();
BeanUtils.copyProperties(vo, ssh);
if(Objects.nonNull(sshMapper.selectById(client.getId()))) {
sshMapper.updateById(ssh);
} else {
sshMapper.insert(ssh);
}
}
@Override
public SshSettingsVO sshSettings(int clientId) {
ClientDetail detail = detailMapper.selectById(clientId);
ClientSsh ssh = sshMapper.selectById(clientId);
SshSettingsVO vo;
if(ssh == null) {
vo = new SshSettingsVO();
} else {
vo = ssh.asViewObject(SshSettingsVO.class);
}
vo.setIp(detail.getIp());
return vo;
}
private boolean isOnline(RuntimeDetailVO runtime) { private boolean isOnline(RuntimeDetailVO runtime) {
return runtime != null && System.currentTimeMillis() - runtime.getTimestamp() < 60 * 1000; return runtime != null && System.currentTimeMillis() - runtime.getTimestamp() < 60 * 1000;
} }

View File

@ -0,0 +1,35 @@
package com.example.service.impl;
import com.example.service.TerminalService;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.IOException;
@Slf4j
@Service
@ServerEndpoint("/terminal/{clientId}")
public class TerminalServiceImpl implements TerminalService {
@OnOpen
public void onOpen(Session session) throws IOException {
}
@OnClose
public void onClose(Session session) {
}
@OnMessage
public void onMessage(Session session, String message) {
}
@OnError
public void onError(Session session, Throwable error) {
}
}

View File

@ -5,7 +5,7 @@ import {useClipboard} from "@vueuse/core";
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
import {cpuNameToImage, fitToRightByteUnit, osNameToIcon, percentageToStatus, rename} from "@/tools"; import {cpuNameToImage, fitToRightByteUnit, osNameToIcon, percentageToStatus, rename} from "@/tools";
import RuntimeHistroy from "@/component/RuntimeHistroy.vue"; import RuntimeHistroy from "@/component/RuntimeHistroy.vue";
import {Delete} from "@element-plus/icons-vue"; import {Connection, Delete, FolderOpened} from "@element-plus/icons-vue";
import {useStore} from "@/store"; import {useStore} from "@/store";
const store = useStore() const store = useStore()
@ -16,7 +16,7 @@ const props = defineProps({
update: Function, update: Function,
show: Boolean show: Boolean
}) })
const emits = defineEmits(['delete']) const emits = defineEmits(['delete', 'terminal'])
const details = reactive({ const details = reactive({
base: {}, base: {},
@ -100,13 +100,16 @@ watch(() => props.id, init, { immediate: true })
<div class="client-details" <div class="client-details"
v-loading="Object.keys(details.base).length === 0"> v-loading="Object.keys(details.base).length === 0">
<div v-if="Object.keys(details.base).length"> <div v-if="Object.keys(details.base).length">
<div style="display: flex;justify-content: space-between"> <div style="margin-bottom: 15px">
<div class="title"> <el-button :icon="Connection" type="info" size="small" plain
<i class="fa-solid fa-server"></i> @click="emits('terminal', id)">SSH远程连接</el-button>
服务器信息 <el-button :icon="FolderOpened" type="primary" size="small" plain>在线SFTP文件管理</el-button>
</div> <el-button :icon="Delete" type="danger" size="small"
<el-button :icon="Delete" type="danger" @click="deleteClient" plain>删除此主机</el-button>
@click="deleteClient" plain text>删除此主机</el-button> </div>
<div class="title">
<i class="fa-solid fa-server"></i>
服务器信息
</div> </div>
<el-divider style="margin: 10px 0"/> <el-divider style="margin: 10px 0"/>
<div class="details-list"> <div class="details-list">

View File

@ -0,0 +1,89 @@
<script setup>
import {reactive, ref, watch} from "vue";
import {get, post} from "@/net";
import {ElMessage} from "element-plus";
const props = defineProps({
id: Number
})
const connection = reactive({
ip: '',
port: 22,
username: '',
password: ''
})
const rules = {
port: [
{ required: true, message: '请输入端口', trigger: ['blur', 'change'] },
],
username: [
{ required: true, message: '请输入用户名', trigger: ['blur', 'change'] },
],
password: [
{ required: true, message: '请输入密码', trigger: ['blur', 'change'] },
]
}
const formRef = ref()
function saveConnection() {
formRef.value.validate((isValid) => {
if(isValid) {
post('/api/monitor/ssh-save', {
...connection,
id: props.id
}, () => {
ElMessage.success('正在连接')
})
}
})
}
watch(() => props.id, id => {
connection.ip = ''
get(`/api/monitor/ssh?clientId=${id}`, data => Object.assign(connection, data))
}, {immediate: true})
</script>
<template>
<div class="terminal-main">
<div class="login" v-loading="!connection.ip">
<i style="font-size: 50px" class="fa-solid fa-terminal"></i>
<div style="margin-top: 10px;font-weight: bold;font-size: 20px">服务端连接信息</div>
<el-form style="width: 400px;margin: 20px auto" :model="connection"
:rules="rules" ref="formRef" label-width="100">
<div style="display: flex;gap: 10px">
<el-form-item style="width: 100%" label="服务器IP地址" prop="ip">
<el-input v-model="connection.ip" disabled/>
</el-form-item>
<el-form-item style="width: 80px" prop="port" label-width="0">
<el-input placeholder="端口" v-model="connection.port"/>
</el-form-item>
</div>
<el-form-item prop="username" label="登录用户名">
<el-input placeholder="请输入用户名..." v-model="connection.username"/>
</el-form-item>
<el-form-item prop="password" label="登录密码">
<el-input placeholder="请输入密码..." type="password" v-model="connection.password"/>
</el-form-item>
<el-button type="success" @click="saveConnection" plain>立即连接</el-button>
</el-form>
</div>
</div>
</template>
<style scoped>
.terminal-main {
width: 100%;
height: 100%;
.login {
text-align: center;
padding-top: 50px;
height: 100%;
box-sizing: border-box;
}
}
</style>

View File

@ -7,6 +7,7 @@ import {Plus} from "@element-plus/icons-vue";
import RegisterCard from "@/component/RegisterCard.vue"; import RegisterCard from "@/component/RegisterCard.vue";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {useStore} from "@/store"; import {useStore} from "@/store";
import TerminalWindow from "@/component/TerminalWindow.vue";
const store = useStore() const store = useStore()
const route = useRoute() const route = useRoute()
@ -42,6 +43,16 @@ const updateList = () => {
} }
setInterval(updateList, 10000) setInterval(updateList, 10000)
updateList() updateList()
const openTerminal = id => {
terminal.show = true
terminal.id = id
detail.show = false
}
const terminal = reactive({
show: false,
id: detail.id
})
</script> </script>
<template> <template>
@ -71,13 +82,26 @@ updateList()
</div> </div>
<el-drawer size="520" :show-close="false" v-model="detail.show" <el-drawer size="520" :show-close="false" v-model="detail.show"
:with-header="false" v-if="list.length" @close="detail.id = -1"> :with-header="false" v-if="list.length" @close="detail.id = -1">
<client-details :id="detail.id" :update="updateList" :show="detail.show" @delete="detail.show = false"/> <client-details :id="detail.id" :update="updateList" :show="detail.show"
@delete="detail.show = false" @terminal="openTerminal"/>
</el-drawer> </el-drawer>
<el-drawer style="width: 600px;margin: 10px auto" v-model="register.show" <el-drawer style="width: 600px;margin: 10px auto" v-model="register.show"
direction="btt" :show-close="false" :with-header="false" direction="btt" :show-close="false" :with-header="false"
:size="320" @open="refreshToken"> :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" v-model="terminal.show"
:close-on-click-modal="false">
<template #header>
<div>
<div style="font-size: 18px;color: dodgerblue;font-weight: bold;">SSH远程连接</div>
<div style="font-size: 14px">
远程连接的建立将由服务端完成因此在内网环境下也可以正常使用
</div>
</div>
</template>
<terminal-window :id="terminal.id"/>
</el-drawer>
</div> </div>
</template> </template>