16 KiB
16 KiB
模块03:连接管理功能
🎨 UI设计系统概览
完整设计系统文档请参考:
UI设计系统.md
核心设计原则
- 现代简约:界面清晰,层次分明
- 专业高效:减少操作步骤,提升工作效率
- 一致性:统一的视觉语言和交互模式
- 可访问性:符合WCAG 2.1 AA标准
关键设计令牌
颜色系统:
- 主色:
#0d6efd(操作按钮、选中状态) - 成功:
#198754(连接成功状态) - 危险:
#dc3545(删除操作、错误提示) - 深灰:
#212529(导航栏背景) - 浅灰:
#e9ecef(工具栏背景)
字体系统:
- 字体族:系统字体栈(-apple-system, Segoe UI, Roboto等)
- 正文:14px,行高1.5
- 标题:20-32px,行高1.2-1.4
- 小号文字:12px(文件大小、日期等)
间距系统:
- 基础单位:8px
- 标准间距:16px(1rem)
- 组件内边距:8px-16px
组件规范:
- 导航栏:高度48px,深色背景
- 工具栏:浅灰背景,按钮间距8px
- 文件项:最小高度44px,悬停效果150ms
- 按钮:圆角4px,过渡150ms
交互规范:
- 悬停效果:150ms过渡
- 触摸目标:最小44x44px
- 键盘导航:Tab、Enter、Delete、F2、F5、Esc
- 焦点状态:2px蓝色轮廓
响应式断点:
- 移动端:< 768px(双面板垂直排列)
- 平板:768px - 1024px
- 桌面:> 1024px(标准布局)
3.1 功能概述
实现SFTP连接的建立、断开、保存、加载和删除功能,支持多连接同时管理。
3.2 后端设计
3.2.1 ConnectionRepository接口
package com.sftp.manager.repository;
import com.sftp.manager.model.Connection;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface ConnectionRepository extends JpaRepository<Connection, Long> {
List<Connection> findByOrderByCreatedAtDesc(); // 按创建时间倒序查询
Optional<Connection> findByName(String name); // 按名称查询
}
3.2.2 SessionManager会话管理
package com.sftp.manager.service;
import com.jcraft.jsch.ChannelSftp;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
@Component
public class SessionManager {
private final Map<String, ChannelSftp> activeSessions = new ConcurrentHashMap<>();
private final Map<String, Connection> sessionConnections = new ConcurrentHashMap<>();
public String addSession(ChannelSftp channel, Connection connection) {
String sessionId = "sftp-" + UUID.randomUUID().toString();
activeSessions.put(sessionId, channel);
sessionConnections.put(sessionId, connection);
return sessionId;
}
public ChannelSftp getSession(String sessionId) {
return activeSessions.get(sessionId);
}
public Connection getConnection(String sessionId) {
return sessionConnections.get(sessionId);
}
public void removeSession(String sessionId) {
ChannelSftp channel = activeSessions.get(sessionId);
if (channel != null) {
try {
channel.disconnect();
} catch (Exception e) {
// 忽略关闭异常
}
}
activeSessions.remove(sessionId);
sessionConnections.remove(sessionId);
}
public boolean isActive(String sessionId) {
return activeSessions.containsKey(sessionId);
}
public Map<String, Connection> getAllActiveConnections() {
return new ConcurrentHashMap<>(sessionConnections);
}
public int getActiveSessionCount() {
return activeSessions.size();
}
}
3.2.3 ConnectionService连接服务
package com.sftp.manager.service;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.sftp.manager.dto.ConnectionRequest;
import com.sftp.manager.model.Connection;
import com.sftp.manager.repository.ConnectionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Service
public class ConnectionService {
@Autowired
private ConnectionRepository connectionRepository;
@Autowired
private SessionManager sessionManager;
@Value("${app.sftp.connection-timeout:10000}")
private int connectionTimeout;
@Value("${app.sftp.max-retries:3}")
private int maxRetries;
public String connect(ConnectionRequest request) throws Exception {
JSch jsch = new JSch();
Session session = null;
Channel channel = null;
com.jcraft.jsch.ChannelSftp sftpChannel = null;
int retryCount = 0;
while (retryCount < maxRetries) {
try {
// 配置私钥(如果提供)
if (request.getPrivateKeyPath() != null && !request.getPrivateKeyPath().isEmpty()) {
jsch.addIdentity(request.getPrivateKeyPath(),
request.getPassPhrase() != null ? request.getPassPhrase() : "");
}
// 创建会话
session = jsch.getSession(request.getUsername(),
request.getHost(),
request.getPort() != null ? request.getPort() : 22);
// 配置密码(如果使用密码认证)
if (request.getPassword() != null && !request.getPassword().isEmpty()) {
session.setPassword(request.getPassword());
}
// 跳过主机密钥验证
java.util.Properties config = new java.util.Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
// 设置超时
session.setTimeout(connectionTimeout);
// 连接
session.connect();
channel = session.openChannel("sftp");
channel.connect();
sftpChannel = (com.jcraft.jsch.ChannelSftp) channel;
// 如果指定了默认路径,切换到该路径
if (request.getRootPath() != null && !request.getRootPath().isEmpty()) {
try {
sftpChannel.cd(request.getRootPath());
} catch (Exception e) {
// 路径不存在,使用默认路径
}
}
// 创建连接对象(用于保存配置)
Connection connection = new Connection();
connection.setName(request.getName());
connection.setHost(request.getHost());
connection.setPort(request.getPort() != null ? request.getPort() : 22);
connection.setUsername(request.getUsername());
connection.setPassword(request.getPassword());
connection.setPrivateKeyPath(request.getPrivateKeyPath());
connection.setPassPhrase(request.getPassPhrase());
connection.setRootPath(request.getRootPath());
connection.setConnectTimeout(connectionTimeout);
// 添加到会话管理器
return sessionManager.addSession(sftpChannel, connection);
} catch (Exception e) {
retryCount++;
if (retryCount >= maxRetries) {
throw new Exception("连接失败: " + e.getMessage(), e);
}
Thread.sleep(1000); // 等待1秒后重试
}
}
throw new Exception("连接失败");
}
public void disconnect(String sessionId) {
sessionManager.removeSession(sessionId);
}
public Connection saveConnection(Connection connection) {
return connectionRepository.save(connection);
}
public List<Connection> listConnections() {
return connectionRepository.findByOrderByCreatedAtDesc();
}
public Connection getConnectionById(Long id) {
return connectionRepository.findById(id).orElse(null);
}
public void deleteConnection(Long id) {
connectionRepository.deleteById(id);
}
}
3.2.4 ConnectionController连接控制器
package com.sftp.manager.controller;
import com.sftp.manager.dto.ApiResponse;
import com.sftp.manager.dto.ConnectionRequest;
import com.sftp.manager.model.Connection;
import com.sftp.manager.service.ConnectionService;
import com.sftp.manager.service.SessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/connection")
@CrossOrigin(origins = "*")
public class ConnectionController {
@Autowired
private ConnectionService connectionService;
@Autowired
private SessionManager sessionManager;
@PostMapping("/connect")
public ApiResponse<String> connect(@RequestBody ConnectionRequest request) {
try {
String sessionId = connectionService.connect(request);
return ApiResponse.success("连接成功", sessionId);
} catch (Exception e) {
return ApiResponse.error("连接失败: " + e.getMessage());
}
}
@PostMapping("/disconnect")
public ApiResponse<Void> disconnect(@RequestBody Map<String, String> request) {
try {
String sessionId = request.get("sessionId");
connectionService.disconnect(sessionId);
return ApiResponse.success("断开成功", null);
} catch (Exception e) {
return ApiResponse.error("断开失败: " + e.getMessage());
}
}
@PostMapping("/save")
public ApiResponse<Connection> saveConnection(@RequestBody Connection connection) {
try {
Connection saved = connectionService.saveConnection(connection);
return ApiResponse.success("保存成功", saved);
} catch (Exception e) {
return ApiResponse.error("保存失败: " + e.getMessage());
}
}
@GetMapping("/list")
public ApiResponse<List<Connection>> listConnections() {
try {
List<Connection> connections = connectionService.listConnections();
return ApiResponse.success("查询成功", connections);
} catch (Exception e) {
return ApiResponse.error("查询失败: " + e.getMessage());
}
}
@GetMapping("/{id}")
public ApiResponse<Connection> getConnection(@PathVariable Long id) {
try {
Connection connection = connectionService.getConnectionById(id);
if (connection != null) {
return ApiResponse.success("查询成功", connection);
} else {
return ApiResponse.error("连接不存在");
}
} catch (Exception e) {
return ApiResponse.error("查询失败: " + e.getMessage());
}
}
@DeleteMapping("/{id}")
public ApiResponse<Void> deleteConnection(@PathVariable Long id) {
try {
connectionService.deleteConnection(id);
return ApiResponse.success("删除成功", null);
} catch (Exception e) {
return ApiResponse.error("删除失败: " + e.getMessage());
}
}
@GetMapping("/active")
public ApiResponse<Map<String, Connection>> getActiveConnections() {
try {
return ApiResponse.success("查询成功", sessionManager.getAllActiveConnections());
} catch (Exception e) {
return ApiResponse.error("查询失败: " + e.getMessage());
}
}
}
3.3 API接口说明
接口列表
| 方法 | 路径 | 说明 | 请求参数 |
|---|---|---|---|
| POST | /api/connection/connect | 建立SFTP连接 | ConnectionRequest |
| POST | /api/connection/disconnect | 断开连接 | sessionId |
| POST | /api/connection/save | 保存连接配置 | Connection |
| GET | /api/connection/list | 获取所有保存的连接 | - |
| GET | /api/connection/{id} | 获取指定连接 | 路径参数id |
| DELETE | /api/connection/{id} | 删除连接配置 | 路径参数id |
| GET | /api/connection/active | 获取所有活跃连接 | - |
请求/响应示例
1. 建立连接
请求:
POST /api/connection/connect
{
"name": "测试服务器",
"host": "192.168.1.100",
"port": 22,
"username": "root",
"password": "123456"
}
响应:
{
"success": true,
"message": "连接成功",
"data": "sftp-12345678-1234-1234-1234-123456789abc"
}
2. 保存连接配置
请求:
POST /api/connection/save
{
"name": "测试服务器",
"host": "192.168.1.100",
"port": 22,
"username": "root",
"password": "encrypted_password"
}
响应:
{
"success": true,
"message": "保存成功",
"data": {
"id": 1,
"name": "测试服务器",
"host": "192.168.1.100",
"port": 22,
"username": "root",
"createdAt": "2024-02-02T10:00:00",
"updatedAt": "2024-02-02T10:00:00"
}
}
3. 获取连接列表
请求:
GET /api/connection/list
响应:
{
"success": true,
"message": "查询成功",
"data": [
{
"id": 1,
"name": "测试服务器",
"host": "192.168.1.100",
"port": 22,
"username": "root",
"createdAt": "2024-02-02T10:00:00",
"updatedAt": "2024-02-02T10:00:00"
}
]
}
4. 获取活跃连接
请求:
GET /api/connection/active
响应:
{
"success": true,
"message": "查询成功",
"data": {
"sftp-12345678-1234-1234-1234-123456789abc": {
"name": "测试服务器",
"host": "192.168.1.100",
"port": 22,
"username": "root"
}
}
}
3.4 关键技术点
3.4.1 JSch连接配置
- StrictHostKeyChecking=no:跳过主机密钥验证(仅用于开发环境)
- 连接超时:配置合理的超时时间
- 双重认证:支持密码和私钥两种认证方式
3.4.2 会话管理机制
使用ConcurrentHashMap存储活跃会话:
activeSessions:存储sessionId到ChannelSftp的映射sessionConnections:存储sessionId到Connection的映射- 线程安全:使用ConcurrentHashMap确保多线程安全
3.4.3 会话ID规则
- "local":表示本地文件系统
- "sftp-{uuid}":表示SFTP连接会话
3.4.4 错误处理
- 连接超时:自动重试(最多3次)
- 认证失败:返回明确错误信息
- 网络异常:捕获并友好提示
实施步骤
-
创建Repository接口
touch src/main/java/com/sftp/manager/repository/ConnectionRepository.java -
创建SessionManager服务
touch src/main/java/com/sftp/manager/service/SessionManager.java -
创建ConnectionService服务
touch src/main/java/com/sftp/manager/service/ConnectionService.java -
创建ConnectionController控制器
touch src/main/java/com/sftp/manager/controller/ConnectionController.java -
编译测试
mvn clean compile -
启动服务并测试
mvn spring-boot:run
测试验证
使用Postman或curl测试以下API:
-
保存连接配置
curl -X POST http://localhost:8080/sftp-manager/api/connection/save \ -H "Content-Type: application/json" \ -d '{"name":"测试","host":"192.168.1.100","port":22,"username":"root","password":"123456"}' -
获取连接列表
curl http://localhost:8080/sftp-manager/api/connection/list -
建立连接
curl -X POST http://localhost:8080/sftp-manager/api/connection/connect \ -H "Content-Type: application/json" \ -d '{"name":"测试","host":"192.168.1.100","port":22,"username":"root","password":"123456"}'
注意事项
- 密码安全:生产环境应加密存储密码
- 连接池:可考虑使用连接池管理SFTP连接
- 心跳检测:定期检测连接状态,自动重连
- 资源清理:确保连接断开时正确释放资源
下一步
完成模块03后,继续模块04:文件浏览功能