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

16 KiB
Raw Blame History

模块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接口

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次
  • 认证失败:返回明确错误信息
  • 网络异常:捕获并友好提示

实施步骤

  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文件浏览功能