Files
sftp-manager/docs/03-连接管理功能.md
liu 14289beb66 Initial commit
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-03 10:10:11 +08:00

579 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 模块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
- 标准间距16px1rem
- 组件内边距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接口
```java
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会话管理
```java
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连接服务
```java
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连接控制器
```java
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. 建立连接
**请求:**
```json
POST /api/connection/connect
{
"name": "测试服务器",
"host": "192.168.1.100",
"port": 22,
"username": "root",
"password": "123456"
}
```
**响应:**
```json
{
"success": true,
"message": "连接成功",
"data": "sftp-12345678-1234-1234-1234-123456789abc"
}
```
#### 2. 保存连接配置
**请求:**
```json
POST /api/connection/save
{
"name": "测试服务器",
"host": "192.168.1.100",
"port": 22,
"username": "root",
"password": "encrypted_password"
}
```
**响应:**
```json
{
"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
```
**响应:**
```json
{
"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
```
**响应:**
```json
{
"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次
- **认证失败**:返回明确错误信息
- **网络异常**:捕获并友好提示
## 实施步骤
1. **创建Repository接口**
```
touch src/main/java/com/sftp/manager/repository/ConnectionRepository.java
```
2. **创建SessionManager服务**
```
touch src/main/java/com/sftp/manager/service/SessionManager.java
```
3. **创建ConnectionService服务**
```
touch src/main/java/com/sftp/manager/service/ConnectionService.java
```
4. **创建ConnectionController控制器**
```
touch src/main/java/com/sftp/manager/controller/ConnectionController.java
```
5. **编译测试**
```
mvn clean compile
```
6. **启动服务并测试**
```
mvn spring-boot:run
```
## 测试验证
使用Postman或curl测试以下API
1. **保存连接配置**
```
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"}'
```
2. **获取连接列表**
```
curl http://localhost:8080/sftp-manager/api/connection/list
```
3. **建立连接**
```
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"}'
```
## 注意事项
1. **密码安全**:生产环境应加密存储密码
2. **连接池**可考虑使用连接池管理SFTP连接
3. **心跳检测**:定期检测连接状态,自动重连
4. **资源清理**:确保连接断开时正确释放资源
## 下一步
完成模块03后继续模块04文件浏览功能