重写复现方法
This commit is contained in:
@@ -1,25 +0,0 @@
|
||||
package com.mini.capi.config;
|
||||
|
||||
import com.mini.capi.biz.domain.ApiUser;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Object handler) throws Exception {
|
||||
HttpSession session = request.getSession();
|
||||
ApiUser apiUser = (ApiUser) session.getAttribute("userInfo");
|
||||
if (apiUser == null) {
|
||||
response.sendRedirect(request.getContextPath() + "/login");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package com.mini.capi.config;
|
||||
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.resource.PathResourceResolver;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
private final AuthInterceptor authInterceptor;
|
||||
|
||||
|
||||
@Override
|
||||
public void addViewControllers(ViewControllerRegistry registry) {
|
||||
registry.addViewController("/login").setViewName("forward:/index.html");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
registry.addResourceHandler("/**")
|
||||
.addResourceLocations("classpath:/static/")
|
||||
.resourceChain(true)
|
||||
.addResolver(new PathResourceResolver() {
|
||||
@Override
|
||||
protected Resource getResource(String resourcePath, Resource location) throws IOException {
|
||||
Resource requested = super.getResource(resourcePath, location);
|
||||
// 文件存在就返回,不存在返回 index.html
|
||||
return requested != null ? requested : new ClassPathResource("/static/index.html");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.addPathPatterns("/**") // 拦截所有请求
|
||||
.excludePathPatterns(
|
||||
// ① 排除 Vue 静态资源(匹配 static 目录下所有 js/css/img 等)
|
||||
"/js/**",
|
||||
"/css/**",
|
||||
"/img/**",
|
||||
"/favicon.ico",
|
||||
"/index.html", // 排除首页(Vue 入口)
|
||||
"/assets/**", // 若 Vue 打包后有 assets 目录,需排除
|
||||
"/resource/**", // 若有其他静态资源目录,需排除
|
||||
"/cApi/index/**",
|
||||
|
||||
"/login",
|
||||
"/Sys/login/**", // 你的登录接口(原 /Sys/login/** 缺少 /cApi 前缀,修复)
|
||||
"/Sys/jobs/**", // 原路径补充 /cApi 前缀
|
||||
"/Sys/hosts/**",
|
||||
"/Sys/dbs/**",
|
||||
|
||||
"/cApi/swagger-ui/**",
|
||||
"/cApi/v3/api-docs/**"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package com.mini.capi.model;
|
||||
|
||||
import com.mini.capi.biz.domain.SyncTablesView;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ApiResult<T> implements Serializable {
|
||||
|
||||
@@ -15,12 +15,6 @@ public class CustomErrorController implements ErrorController {
|
||||
@RequestMapping("/error")
|
||||
public Object handleError(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
|
||||
// 1. 前端路由:统一转发到 index.html
|
||||
if (uri.startsWith("/cApi/index/")) {
|
||||
return "forward:/index.html";
|
||||
}
|
||||
|
||||
// 2. 其他 404:返回 JSON
|
||||
HttpStatus status = getStatus(request);
|
||||
return ResponseEntity
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.mini.capi.sys.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class RootController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String redirectToSwagger() {
|
||||
return "redirect:/swagger-ui.html";
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package com.mini.capi.webssh.config;
|
||||
|
||||
import com.mini.capi.webssh.websocket.SSHWebSocketHandler;
|
||||
import jakarta.annotation.Resource;
|
||||
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 {
|
||||
|
||||
@Resource
|
||||
private SSHWebSocketHandler sshWebSocketHandler;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(sshWebSocketHandler, "/ssh")
|
||||
.setAllowedOriginPatterns("*"); // 生产环境中应该限制域名
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
package com.mini.capi.webssh.controller;
|
||||
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import com.mini.capi.biz.service.SshServersService;
|
||||
import com.mini.capi.webssh.service.FileTransferService;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/files")
|
||||
public class FileTransferController {
|
||||
|
||||
@Resource
|
||||
private FileTransferService fileTransferService;
|
||||
|
||||
@Resource
|
||||
private SshServersService serverService;
|
||||
|
||||
/**
|
||||
* 上传文件到服务器
|
||||
*/
|
||||
@PostMapping("/upload/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> uploadFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("file") MultipartFile file,
|
||||
@RequestParam("remotePath") String remotePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "文件不能为空"));
|
||||
}
|
||||
|
||||
fileTransferService.uploadFile(serverOpt.get(), file, remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "文件上传成功",
|
||||
"filename", file.getOriginalFilename(),
|
||||
"size", file.getSize()
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "上传失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件
|
||||
*/
|
||||
@PostMapping("/upload-batch/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> uploadFiles(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("files") MultipartFile[] files,
|
||||
@RequestParam("remotePath") String remotePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
if (files == null || files.length == 0) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "请选择要上传的文件"));
|
||||
}
|
||||
|
||||
fileTransferService.uploadFiles(serverOpt.get(), files, remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "批量上传成功",
|
||||
"count", files.length
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "批量上传失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从服务器下载文件
|
||||
*/
|
||||
@GetMapping("/download/{serverId}")
|
||||
public ResponseEntity<byte[]> downloadFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("remoteFilePath") String remoteFilePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
byte[] fileContent = fileTransferService.downloadFile(serverOpt.get(), remoteFilePath);
|
||||
|
||||
// 从路径中提取文件名
|
||||
String filename = remoteFilePath.substring(remoteFilePath.lastIndexOf('/') + 1);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.body(fileContent);
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出远程目录内容
|
||||
*/
|
||||
@GetMapping("/list/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> listDirectory(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("remotePath") String remotePath) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
List<FileTransferService.FileInfo> files =
|
||||
fileTransferService.listDirectory(serverOpt.get(), remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"files", files,
|
||||
"path", remotePath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "获取目录列表失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远程目录
|
||||
*/
|
||||
@PostMapping("/mkdir/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> createDirectory(
|
||||
@PathVariable Long serverId,
|
||||
@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
String remotePath = request.get("remotePath");
|
||||
if (remotePath == null || remotePath.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "目录路径不能为空"));
|
||||
}
|
||||
|
||||
fileTransferService.createRemoteDirectory(serverOpt.get(), remotePath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "目录创建成功",
|
||||
"path", remotePath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "创建目录失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除远程文件或目录
|
||||
*/
|
||||
@DeleteMapping("/delete/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> deleteFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestParam("remotePath") String remotePath,
|
||||
@RequestParam(value = "isDirectory", defaultValue = "false") boolean isDirectory) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
fileTransferService.deleteRemoteFile(serverOpt.get(), remotePath, isDirectory);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", (isDirectory ? "目录" : "文件") + "删除成功",
|
||||
"path", remotePath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "删除失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名远程文件
|
||||
*/
|
||||
@PostMapping("/rename/{serverId}")
|
||||
public ResponseEntity<Map<String, Object>> renameFile(
|
||||
@PathVariable Long serverId,
|
||||
@RequestBody Map<String, String> request) {
|
||||
try {
|
||||
Optional<SshServers> serverOpt = serverService.getOptById(serverId);
|
||||
if (!serverOpt.isPresent()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "服务器不存在"));
|
||||
}
|
||||
|
||||
String oldPath = request.get("oldPath");
|
||||
String newPath = request.get("newPath");
|
||||
|
||||
if (oldPath == null || newPath == null || oldPath.trim().isEmpty() || newPath.trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "路径不能为空"));
|
||||
}
|
||||
|
||||
fileTransferService.renameRemoteFile(serverOpt.get(), oldPath, newPath);
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"success", true,
|
||||
"message", "重命名成功",
|
||||
"oldPath", oldPath,
|
||||
"newPath", newPath
|
||||
));
|
||||
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", "重命名失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package com.mini.capi.webssh.controller;
|
||||
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import com.jcraft.jsch.JSch;
|
||||
import com.jcraft.jsch.Session;
|
||||
import com.mini.capi.biz.service.SshServersService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/servers")
|
||||
public class ServerController {
|
||||
|
||||
@Resource
|
||||
private SshServersService serverService;
|
||||
|
||||
/**
|
||||
* 获取服务器列表
|
||||
*/
|
||||
@GetMapping
|
||||
public ResponseEntity<List<SshServers>> getServers() {
|
||||
List<SshServers> servers = serverService.list();
|
||||
return ResponseEntity.ok(servers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个服务器配置
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<SshServers> getServer(@PathVariable Long id) {
|
||||
try {
|
||||
Optional<SshServers> server = serverService.getOptById(id);
|
||||
if (server.isPresent()) {
|
||||
return ResponseEntity.ok(server.get());
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加服务器
|
||||
*/
|
||||
@PostMapping
|
||||
public ResponseEntity<Map<String, Object>> addServer(@RequestBody SshServers server) {
|
||||
try {
|
||||
// 验证必要参数
|
||||
if (server.getHost() == null || server.getHost().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "服务器地址不能为空"));
|
||||
}
|
||||
if (server.getUsername() == null || server.getUsername().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "用户名不能为空"));
|
||||
}
|
||||
if (server.getPassword() == null || server.getPassword().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "密码不能为空"));
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if (server.getPort() == null) {
|
||||
server.setPort(22);
|
||||
}
|
||||
if (server.getName() == null || server.getName().trim().isEmpty()) {
|
||||
server.setName(server.getUsername() + "@" + server.getHost());
|
||||
}
|
||||
|
||||
serverService.save(server);
|
||||
return ResponseEntity.ok(Map.of("success", true));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除服务器
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Map<String, Object>> deleteServer(@PathVariable Long id) {
|
||||
try {
|
||||
serverService.removeById(id);
|
||||
return ResponseEntity.ok(Map.of("success", true));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("success", false, "message", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试服务器连接
|
||||
*/
|
||||
@PostMapping("/test")
|
||||
public ResponseEntity<Map<String, Object>> testConnection(@RequestBody SshServers server) {
|
||||
try {
|
||||
// 验证必要参数
|
||||
if (server.getHost() == null || server.getHost().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "服务器地址不能为空"));
|
||||
}
|
||||
if (server.getUsername() == null || server.getUsername().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "用户名不能为空"));
|
||||
}
|
||||
if (server.getPassword() == null || server.getPassword().trim().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("success", false, "message", "密码不能为空"));
|
||||
}
|
||||
|
||||
// 设置默认端口
|
||||
int port = server.getPort() != null ? server.getPort() : 22;
|
||||
|
||||
// 简单的连接测试
|
||||
JSch jsch = new JSch();
|
||||
Session session = jsch.getSession(server.getUsername(), server.getHost(), port);
|
||||
session.setPassword(server.getPassword());
|
||||
session.setConfig("StrictHostKeyChecking", "no");
|
||||
session.connect(5000); // 5秒超时
|
||||
session.disconnect();
|
||||
return ResponseEntity.ok(Map.of("success", true, "message", "连接测试成功"));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.ok(Map.of("success", false, "message", "连接测试失败: " + e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,301 +0,0 @@
|
||||
package com.mini.capi.webssh.service;
|
||||
|
||||
import com.jcraft.jsch.*;
|
||||
import com.mini.capi.biz.domain.SshServers;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.Vector;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class FileTransferService {
|
||||
|
||||
/**
|
||||
* 上传文件到远程服务器
|
||||
*/
|
||||
public void uploadFile(SshServers server, MultipartFile file, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
// 确保远程目录存在
|
||||
createRemoteDirectory(sftpChannel, remotePath);
|
||||
|
||||
// 上传文件
|
||||
String remoteFilePath = remotePath + "/" + file.getOriginalFilename();
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
sftpChannel.put(inputStream, remoteFilePath);
|
||||
}
|
||||
|
||||
log.info("文件上传成功: {} -> {}", file.getOriginalFilename(), remoteFilePath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从远程服务器下载文件
|
||||
*/
|
||||
public byte[] downloadFile(SshServers server, String remoteFilePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
InputStream inputStream = sftpChannel.get(remoteFilePath)) {
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
|
||||
log.info("文件下载成功: {}", remoteFilePath);
|
||||
return outputStream.toByteArray();
|
||||
}
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出远程目录内容
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public List<FileInfo> listDirectory(SshServers server, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
List<FileInfo> files = new ArrayList<>();
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
Vector<ChannelSftp.LsEntry> entries = sftpChannel.ls(remotePath);
|
||||
|
||||
for (ChannelSftp.LsEntry entry : entries) {
|
||||
String filename = entry.getFilename();
|
||||
if (!filename.equals(".") && !filename.equals("..")) {
|
||||
SftpATTRS attrs = entry.getAttrs();
|
||||
files.add(new FileInfo(
|
||||
filename,
|
||||
attrs.isDir(),
|
||||
attrs.getSize(),
|
||||
attrs.getMTime() * 1000L, // Convert to milliseconds
|
||||
getPermissionString(attrs.getPermissions())
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
log.info("目录列表获取成功: {}, 文件数: {}", remotePath, files.size());
|
||||
return files;
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建远程目录
|
||||
*/
|
||||
public void createRemoteDirectory(SshServers server, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
createRemoteDirectory(sftpChannel, remotePath);
|
||||
log.info("远程目录创建成功: {}", remotePath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除远程文件或目录
|
||||
*/
|
||||
public void deleteRemoteFile(SshServers server, String remotePath, boolean isDirectory) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
if (isDirectory) {
|
||||
sftpChannel.rmdir(remotePath);
|
||||
} else {
|
||||
sftpChannel.rm(remotePath);
|
||||
}
|
||||
|
||||
log.info("远程文件删除成功: {}", remotePath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重命名远程文件
|
||||
*/
|
||||
public void renameRemoteFile(SshServers server, String oldPath, String newPath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
sftpChannel.rename(oldPath, newPath);
|
||||
log.info("文件重命名成功: {} -> {}", oldPath, newPath);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量上传文件
|
||||
*/
|
||||
public void uploadFiles(SshServers server, MultipartFile[] files, String remotePath) throws Exception {
|
||||
Session session = null;
|
||||
ChannelSftp sftpChannel = null;
|
||||
|
||||
try {
|
||||
session = createSession(server);
|
||||
sftpChannel = (ChannelSftp) session.openChannel("sftp");
|
||||
sftpChannel.connect();
|
||||
|
||||
// 确保远程目录存在
|
||||
createRemoteDirectory(sftpChannel, remotePath);
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
if (!file.isEmpty()) {
|
||||
String remoteFilePath = remotePath + "/" + file.getOriginalFilename();
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
sftpChannel.put(inputStream, remoteFilePath);
|
||||
log.info("文件上传成功: {}", file.getOriginalFilename());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.info("批量上传完成,共上传 {} 个文件", files.length);
|
||||
|
||||
} finally {
|
||||
closeConnections(sftpChannel, session);
|
||||
}
|
||||
}
|
||||
|
||||
// 私有辅助方法
|
||||
|
||||
private Session createSession(SshServers server) throws JSchException {
|
||||
JSch jsch = new JSch();
|
||||
Session session = jsch.getSession(server.getUsername(), server.getHost(), server.getPort());
|
||||
session.setPassword(server.getPassword());
|
||||
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
config.put("PreferredAuthentications", "password");
|
||||
session.setConfig(config);
|
||||
session.connect(10000); // 10秒超时
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
private void createRemoteDirectory(ChannelSftp sftpChannel, String remotePath) {
|
||||
try {
|
||||
String[] pathParts = remotePath.split("/");
|
||||
String currentPath = "";
|
||||
|
||||
for (String part : pathParts) {
|
||||
if (!part.isEmpty()) {
|
||||
currentPath += "/" + part;
|
||||
try {
|
||||
sftpChannel.mkdir(currentPath);
|
||||
} catch (SftpException e) {
|
||||
log.error(e.getMessage(),e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("创建远程目录失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void closeConnections(ChannelSftp sftpChannel, Session session) {
|
||||
if (sftpChannel != null && sftpChannel.isConnected()) {
|
||||
sftpChannel.disconnect();
|
||||
}
|
||||
if (session != null && session.isConnected()) {
|
||||
session.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private String getPermissionString(int permissions) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// Owner permissions
|
||||
sb.append((permissions & 0400) != 0 ? 'r' : '-');
|
||||
sb.append((permissions & 0200) != 0 ? 'w' : '-');
|
||||
sb.append((permissions & 0100) != 0 ? 'x' : '-');
|
||||
|
||||
// Group permissions
|
||||
sb.append((permissions & 0040) != 0 ? 'r' : '-');
|
||||
sb.append((permissions & 0020) != 0 ? 'w' : '-');
|
||||
sb.append((permissions & 0010) != 0 ? 'x' : '-');
|
||||
|
||||
// Others permissions
|
||||
sb.append((permissions & 0004) != 0 ? 'r' : '-');
|
||||
sb.append((permissions & 0002) != 0 ? 'w' : '-');
|
||||
sb.append((permissions & 0001) != 0 ? 'x' : '-');
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
// 文件信息内部类
|
||||
public static class FileInfo {
|
||||
private String name;
|
||||
private boolean isDirectory;
|
||||
private long size;
|
||||
private long lastModified;
|
||||
private String permissions;
|
||||
|
||||
public FileInfo(String name, boolean isDirectory, long size, long lastModified, String permissions) {
|
||||
this.name = name;
|
||||
this.isDirectory = isDirectory;
|
||||
this.size = size;
|
||||
this.lastModified = lastModified;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
// Getters
|
||||
public String getName() { return name; }
|
||||
public boolean isDirectory() { return isDirectory; }
|
||||
public long getSize() { return size; }
|
||||
public long getLastModified() { return lastModified; }
|
||||
public String getPermissions() { return permissions; }
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package com.mini.capi.webssh.service;
|
||||
|
||||
import com.jcraft.jsch.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SSHConnectionManager {
|
||||
|
||||
private final Map<String, Session> connections = new ConcurrentHashMap<>();
|
||||
private final Map<String, ChannelShell> channels = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 建立SSH连接
|
||||
*/
|
||||
public String createConnection(String host, int port, String username, String password) {
|
||||
try {
|
||||
JSch jsch = new JSch();
|
||||
Session session = jsch.getSession(username, host, port);
|
||||
|
||||
// 配置连接参数
|
||||
Properties config = new Properties();
|
||||
config.put("StrictHostKeyChecking", "no");
|
||||
config.put("PreferredAuthentications", "password");
|
||||
session.setConfig(config);
|
||||
session.setPassword(password);
|
||||
|
||||
// 建立连接
|
||||
session.connect(30000); // 30秒超时
|
||||
|
||||
// 创建Shell通道
|
||||
ChannelShell channel = (ChannelShell) session.openChannel("shell");
|
||||
channel.setPty(true);
|
||||
channel.setPtyType("xterm", 80, 24, 640, 480);
|
||||
|
||||
// 生成连接ID
|
||||
String connectionId = UUID.randomUUID().toString();
|
||||
|
||||
// 保存连接和通道
|
||||
connections.put(connectionId, session);
|
||||
channels.put(connectionId, channel);
|
||||
|
||||
log.info("SSH连接建立成功: {}@{}:{}", username, host, port);
|
||||
return connectionId;
|
||||
|
||||
} catch (JSchException e) {
|
||||
log.error("SSH连接失败: {}", e.getMessage());
|
||||
throw new RuntimeException("SSH连接失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SSH通道
|
||||
*/
|
||||
public ChannelShell getChannel(String connectionId) {
|
||||
return channels.get(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SSH会话
|
||||
*/
|
||||
public Session getSession(String connectionId) {
|
||||
return connections.get(connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭SSH连接
|
||||
*/
|
||||
public void closeConnection(String connectionId) {
|
||||
ChannelShell channel = channels.remove(connectionId);
|
||||
if (channel != null && channel.isConnected()) {
|
||||
channel.disconnect();
|
||||
}
|
||||
|
||||
Session session = connections.remove(connectionId);
|
||||
if (session != null && session.isConnected()) {
|
||||
session.disconnect();
|
||||
}
|
||||
|
||||
log.info("SSH连接已关闭: {}", connectionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查连接状态
|
||||
*/
|
||||
public boolean isConnected(String connectionId) {
|
||||
Session session = connections.get(connectionId);
|
||||
return session != null && session.isConnected();
|
||||
}
|
||||
}
|
||||
@@ -1,263 +0,0 @@
|
||||
package com.mini.capi.webssh.websocket;
|
||||
|
||||
import com.mini.capi.webssh.service.SSHConnectionManager;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.jcraft.jsch.ChannelShell;
|
||||
import com.jcraft.jsch.JSchException;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SSHWebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
@Resource
|
||||
private SSHConnectionManager connectionManager;
|
||||
|
||||
private final Map<WebSocketSession, String> sessionConnections = new ConcurrentHashMap<>();
|
||||
private final Map<WebSocketSession, String> sessionUsers = new ConcurrentHashMap<>();
|
||||
|
||||
// 为每个WebSocket会话添加同步锁
|
||||
private final Map<WebSocketSession, Object> sessionLocks = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(WebSocketSession session) {
|
||||
log.info("WebSocket连接建立: {}", session.getId());
|
||||
// 为每个会话创建同步锁
|
||||
sessionLocks.put(session, new Object());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
|
||||
try {
|
||||
String payload = message.getPayload();
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
JsonNode jsonNode = mapper.readTree(payload);
|
||||
|
||||
String type = jsonNode.get("type").asText();
|
||||
|
||||
switch (type) {
|
||||
case "connect":
|
||||
handleConnect(session, jsonNode);
|
||||
break;
|
||||
case "command":
|
||||
handleCommand(session, jsonNode);
|
||||
break;
|
||||
case "resize":
|
||||
handleResize(session, jsonNode);
|
||||
break;
|
||||
case "disconnect":
|
||||
handleDisconnect(session);
|
||||
break;
|
||||
default:
|
||||
log.warn("未知的消息类型: {}", type);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("处理WebSocket消息失败", e);
|
||||
sendError(session, "处理消息失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SSH连接请求
|
||||
*/
|
||||
private void handleConnect(WebSocketSession session, JsonNode jsonNode) {
|
||||
try {
|
||||
String host = jsonNode.get("host").asText();
|
||||
int port = jsonNode.get("port").asInt(22);
|
||||
String username = jsonNode.get("username").asText();
|
||||
String password = jsonNode.get("password").asText();
|
||||
boolean enableCollaboration = jsonNode.has("enableCollaboration") &&
|
||||
jsonNode.get("enableCollaboration").asBoolean();
|
||||
|
||||
// 存储用户信息
|
||||
sessionUsers.put(session, username);
|
||||
|
||||
// 建立SSH连接
|
||||
String connectionId = connectionManager.createConnection(host, port, username, password);
|
||||
sessionConnections.put(session, connectionId);
|
||||
|
||||
// 启动SSH通道
|
||||
ChannelShell channel = connectionManager.getChannel(connectionId);
|
||||
startSSHChannel(session, channel);
|
||||
|
||||
// 发送连接成功消息
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("type", "connected");
|
||||
response.put("message", "SSH连接建立成功");
|
||||
sendMessage(session, response);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("建立SSH连接失败", e);
|
||||
sendError(session, "连接失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理命令执行请求
|
||||
*/
|
||||
private void handleCommand(WebSocketSession session, JsonNode jsonNode) {
|
||||
String connectionId = sessionConnections.get(session);
|
||||
if (connectionId == null) {
|
||||
sendError(session, "SSH连接未建立");
|
||||
return;
|
||||
}
|
||||
|
||||
String command = jsonNode.get("command").asText();
|
||||
ChannelShell channel = connectionManager.getChannel(connectionId);
|
||||
String username = sessionUsers.get(session);
|
||||
|
||||
if (channel != null && channel.isConnected()) {
|
||||
try {
|
||||
// 发送命令到SSH通道
|
||||
OutputStream out = channel.getOutputStream();
|
||||
out.write(command.getBytes());
|
||||
out.flush();
|
||||
} catch (IOException e) {
|
||||
log.error("发送SSH命令失败", e);
|
||||
sendError(session, "命令执行失败");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动SSH通道并处理输出
|
||||
*/
|
||||
private void startSSHChannel(WebSocketSession session, ChannelShell channel) {
|
||||
try {
|
||||
// 连接通道
|
||||
channel.connect();
|
||||
|
||||
// 处理SSH输出
|
||||
InputStream in = channel.getInputStream();
|
||||
|
||||
// 在单独的线程中读取SSH输出
|
||||
new Thread(() -> {
|
||||
byte[] buffer = new byte[4096];
|
||||
try {
|
||||
while (channel.isConnected() && session.isOpen()) {
|
||||
if (in.available() > 0) {
|
||||
int len = in.read(buffer);
|
||||
if (len > 0) {
|
||||
String output = new String(buffer, 0, len, "UTF-8");
|
||||
|
||||
// 发送给当前会话
|
||||
sendMessage(session, Map.of(
|
||||
"type", "output",
|
||||
"data", output
|
||||
));
|
||||
}
|
||||
} else {
|
||||
// 没有数据时短暂休眠,避免CPU占用过高
|
||||
Thread.sleep(10);
|
||||
}
|
||||
}
|
||||
} catch (IOException | InterruptedException e) {
|
||||
log.warn("SSH输出读取中断: {}", e.getMessage());
|
||||
}
|
||||
}, "SSH-Output-Reader-" + session.getId()).start();
|
||||
|
||||
} catch (JSchException | IOException e) {
|
||||
log.error("启动SSH通道失败", e);
|
||||
sendError(session, "通道启动失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理终端大小调整
|
||||
*/
|
||||
private void handleResize(WebSocketSession session, JsonNode jsonNode) {
|
||||
String connectionId = sessionConnections.get(session);
|
||||
if (connectionId != null) {
|
||||
ChannelShell channel = connectionManager.getChannel(connectionId);
|
||||
if (channel != null) {
|
||||
try {
|
||||
int cols = jsonNode.get("cols").asInt();
|
||||
int rows = jsonNode.get("rows").asInt();
|
||||
channel.setPtySize(cols, rows, cols * 8, rows * 16);
|
||||
} catch (Exception e) {
|
||||
log.warn("调整终端大小失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理断开连接
|
||||
*/
|
||||
private void handleDisconnect(WebSocketSession session) {
|
||||
String connectionId = sessionConnections.remove(session);
|
||||
String username = sessionUsers.remove(session);
|
||||
|
||||
if (connectionId != null) {
|
||||
connectionManager.closeConnection(connectionId);
|
||||
}
|
||||
// 清理锁资源
|
||||
sessionLocks.remove(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||
handleDisconnect(session);
|
||||
log.info("WebSocket连接关闭: {}", session.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到WebSocket客户端(线程安全)
|
||||
*/
|
||||
private void sendMessage(WebSocketSession session, Object message) {
|
||||
Object lock = sessionLocks.get(session);
|
||||
if (lock == null) return;
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
String json = mapper.writeValueAsString(message);
|
||||
session.sendMessage(new TextMessage(json));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("发送WebSocket消息失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误消息
|
||||
*/
|
||||
private void sendError(WebSocketSession session, String error) {
|
||||
sendMessage(session, Map.of(
|
||||
"type", "error",
|
||||
"message", error
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话中获取用户信息
|
||||
*/
|
||||
private String getUserFromSession(WebSocketSession session) {
|
||||
// 简化实现,实际应用中可以从session中获取认证用户信息
|
||||
return "anonymous";
|
||||
}
|
||||
|
||||
/**
|
||||
* 从会话中获取主机信息
|
||||
*/
|
||||
private String getHostFromSession(WebSocketSession session) {
|
||||
// 简化实现,实际应用中可以保存连接信息
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user