SpringBoot 集成 FTP 与 SFTP

FTP(File Transfer Protocol)和 SFTP(SSH File Transfer Protocol)虽同为文件传输协议,但在底层原理、安全性、传输方式等方面存在显著差异,具体区别及各自优势如下:

核心区别

  1. 底层依赖与传输机制不同
    • FTP 基于TCP 协议独立运行,使用两个端口完成传输:21 端口用于控制指令(如连接、登录),20 端口(主动模式)或动态端口(被动模式)用于实际数据传输,整个过程中控制流和数据流分离。
    • SFTP 是SSH 协议的子协议,依赖 SSH 的加密通道运行,仅通过 22 端口即可同时处理控制指令和数据传输,无需额外端口,传输过程中所有数据(包括指令和文件内容)都在 SSH 加密通道中完成。
  2. 安全性差异
    • FTP 是明文传输,用户名、密码及文件内容在网络中均以未加密的形式发送,容易被监听、窃取或篡改,安全性极低,仅适用于内网等完全可信的环境。
    • SFTP 基于 SSH 的加密机制(如对称加密、非对称加密),所有传输数据都会经过加密处理,且支持身份验证(密码或 SSH 密钥),能有效防止数据泄露和篡改,安全性远高于 FTP。
  3. 端口与防火墙适配性
    • FTP 的端口使用复杂(固定控制端口 + 动态数据端口),在防火墙或 NAT 环境下需额外配置端口映射,否则可能因端口封锁导致连接失败,适配性较差。
    • SFTP 仅使用 22 端口,端口单一且固定,防火墙规则配置简单,在复杂网络环境(如跨网传输、云服务器)中更易适配,无需额外开放多个端口。
  4. 兼容性与普及度
    • FTP 出现时间早(1971 年),是传统文件传输的 “标准协议”,支持几乎所有操作系统和设备,兼容性极强,老旧系统或简易设备(如嵌入式设备)通常默认支持 FTP。
    • SFTP 是较新的协议(基于 SSH 发展而来),依赖 SSH 环境,部分老旧系统或设备可能未预装 SSH 服务,兼容性略逊于 FTP,但随着安全需求提升,主流系统(如 Linux、Windows 10+)已普遍支持。

各自的优点与适用场景

  • SFTP 的核心优点
    1. 安全性碾压:加密传输避免数据泄露,适合传输敏感文件(如用户数据、财务报表等)。
    2. 端口简化:仅需 22 端口,降低防火墙配置复杂度,尤其适合云服务器、跨网络环境。
    3. 认证灵活:支持 SSH 密钥认证,无需明文存储密码,进一步提升访问安全性。
  • FTP 的核心优点
    1. 兼容性极强:适用于所有支持 TCP/IP 的设备,尤其在老旧系统或简易嵌入式设备中更易部署。
    2. 传输效率略高:无加密开销,在完全可信的内网环境中,纯文件传输速度可能略快于 SFTP。

FTP

参考:https://bbs.huaweicloud.com/blogs/451602

  1. 在pom文件中添加这个
<dependency>
    <groupId>commons-net</groupId>
    <artifactId>commons-net</artifactId>
    <version>3.9.0</version>
</dependency>
  1. 增加配置信息

    ftp:
      host:
        ip: 你的ftp ip地址
      port: 21
      username: 你的账号
      password: 你的密码
      basePath: 要上传的路径(要有权限)
    
@Data
@Component
@ConfigurationProperties(prefix = "ftp")
public class FtpConfig {
    @Value("${ftp.host.ip}")
    private String host;

    @Value("${ftp.port}")
    private int port;

    @Value("${ftp.username}")
    private String username;

    @Value("${ftp.password}")
    private String password;
    
    @Value("${ftp.basePath}")
    private String basePath;
}
  1. 编写相关的工具类
@Component
public class FtpUtil {

    @Autowired
    private SFtpConfig ftpConfig;

    /**
     * 连接FTP服务器
     */
    private FTPClient connect() throws IOException {
        FTPClient ftpClient = new FTPClient();
        ftpClient.setControlEncoding("UTF-8");
        ftpClient.connect(ftpConfig.getHost(), ftpConfig.getPort());
        ftpClient.login(ftpConfig.getUsername(), ftpConfig.getPassword());
        ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
        ftpClient.enterLocalPassiveMode();
        ftpClient.setBufferSize(1024 * 1024); // 设置缓冲区大小

        // 检查连接是否成功
        int replyCode = ftpClient.getReplyCode();
        if (!FTPReply.isPositiveCompletion(replyCode)) {
            ftpClient.disconnect();
            throw new IOException("FTP服务器拒绝连接");
        }

        return ftpClient;
    }

    /**
     * 上传文件到FTP服务器
     * @param remotePath 远程路径
     * @param fileName 文件名
     * @param inputStream 输入流
     * @return 是否上传成功
     */
    public boolean uploadFile(String remotePath, String fileName, InputStream inputStream) {
        FTPClient ftpClient = null;
        try {
            ftpClient = connect();

            // 切换目录
            if (!ftpClient.changeWorkingDirectory(remotePath)) {
                // 如果目录不存在,则创建
                String[] dirs = remotePath.split("/");
                String tempPath = "";
                for (String dir : dirs) {
                    if (dir.length() > 0) {
                        tempPath += "/" + dir;
                        if (!ftpClient.changeWorkingDirectory(tempPath)) {
                            if (!ftpClient.makeDirectory(tempPath)) {
                                return false;
                            }
                        }
                    }
                }
            }

            // 上传文件
            boolean success = ftpClient.storeFile(fileName, inputStream);
            return success;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            if (ftpClient != null && ftpClient.isConnected()) {
                try {
                    ftpClient.logout();
                    ftpClient.disconnect();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 从FTP服务器下载文件
     * @param remotePath 远程路径
     * @param fileName 文件名
     * @param outputStream 输出流
     * @return 是否下载成功
     */
    public boolean downloadFile(String remotePath, String fileName, OutputStream outputStream) {
        FTPClient ftpClient = null;
        try {
            ftpClient = connect();

            // 切换目录
            if (!ftpClient.changeWorkingDirectory(remotePath)) {
                return false;
            }

            // 下载文件
            boolean success = ftpClient.retrieveFile(fileName, outputStream);
            return success;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            if (ftpClient != null && ftpClient.isConnected()) {
                try {
                    ftpClient.logout();
                    ftpClient.disconnect();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 删除FTP服务器上的文件
     * @param remotePath 远程路径
     * @param fileName 文件名
     * @return 是否删除成功
     */
    public boolean deleteFile(String remotePath, String fileName) {
        FTPClient ftpClient = null;
        try {
            ftpClient = connect();

            // 切换目录
            if (!ftpClient.changeWorkingDirectory(remotePath)) {
                return false;
            }

            // 删除文件
            boolean success = ftpClient.deleteFile(fileName);
            return success;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            if (ftpClient != null && ftpClient.isConnected()) {
                try {
                    ftpClient.logout();
                    ftpClient.disconnect();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 检查文件是否存在
     * @param remotePath 远程路径
     * @param fileName 文件名
     * @return 文件是否存在
     */
    public boolean fileExists(String remotePath, String fileName) {
        FTPClient ftpClient = null;
        try {
            ftpClient = connect();

            // 切换目录
            if (!ftpClient.changeWorkingDirectory(remotePath)) {
                return false;
            }

            FTPFile[] files = ftpClient.listFiles();
            for (FTPFile file : files) {
                if (file.getName().equals(fileName)) {
                    return true;
                }
            }
            return false;
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        } finally {
            if (ftpClient != null && ftpClient.isConnected()) {
                try {
                    ftpClient.logout();
                    ftpClient.disconnect();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

写在工具类中,后续可以直接调用

SFTP

参考: https://blog.csdn.net/qq_33204709/article/details/135974528

配置类

<!-- SFTP -->
<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>

连接工具类

@Data
@Component
@ConfigurationProperties(prefix = "ftp")
public class SFtpConfig {
    @Value("${ftp.host.ip}")
    private String host;

    @Value("${ftp.port}")
    private int port;

    @Value("${ftp.username}")
    private String username;

    @Value("${ftp.password}")
    private String password;

    @Value("${ftp.basePath}")
    private String basePath;

    @Value("${ftp.timeOut}")
    private int timeOut;

    /*
    * sftp, 默认协议
     */
    private String protocol = "sftp";
}

接口(便于复用)

public interface SFtpService {
    /**
     * 上传文件
     * @param sftpPath SFTP目标路径
     * @param file 要上传的文件
     * @return 是否上传成功(true:成功,false:失败)
     */
    boolean upload(String sftpPath, MultipartFile file);

    /**
     * 下载文件
     * @param sftpPath SFTP文件路径
     * @param response HTTP响应对象,用于输出文件流
     * @return 是否下载成功(true:成功,false:失败)
     */
    boolean download(String sftpPath, HttpServletResponse response);

    /**
     * 下载文件 全量加载到内存,仅适合小文件
     * @param sftpPath SFTP文件路径
     * @return 文件字节数组
     */

    public byte[] download(String sftpPath);
    /**
     * 重命名文件(或移动文件)
     * @param oldPath 原文件路径
     * @param newPath 新文件路径
     * @return 是否重命名成功(true:成功,false:失败)
     */
    boolean rename(String oldPath, String newPath);

    /**
     * 删除文件(或目录)
     * @param sftpPath 要删除的文件或目录路径
     * @return 是否删除成功(true:成功,false:失败)
     */
    boolean delete(String sftpPath);
}

实现类

@Service
@Slf4j
public class SFtpServiceImpl implements SFtpService {
    @Resource
    private SftpUtils sftpUtils;
    @Override
    public boolean upload(String sftpPath, MultipartFile file) {
        // 上传文件
        ChannelSftp sftp = null;
        try (InputStream in = file.getInputStream()) {
            // 开启sftp连接
            sftp = sftpUtils.createSftp();

            // 进入sftp文件目录
            sftp.cd(sftpPath);
            log.info("切换到目录为:{}", sftpPath);

            // 上传文件
            sftp.put(in, file.getOriginalFilename());
            log.info("上传文件成功,目标目录:{}", sftpPath);
            return true;
        } catch (SftpException | JSchException | IOException e) {
            log.error("上传文件失败,原因:{}", e.getMessage(), e);
            return false;
        } finally {
            // 关闭sftp
            sftpUtils.disconnect(sftp);
        }
    }

    @Override
    public boolean download(String sftpPath, HttpServletResponse response) {
        ChannelSftp sftp = null;
        try {
            // 1. 建立SFTP连接
            sftp = sftpUtils.createSftp();

            // 2. 检查文件是否存在(直接尝试访问文件)
            try {
                // 如果文件不存在,会抛出 SftpException
                sftp.lstat(sftpPath);
            } catch (SftpException e) {
                log.error("文件不存在或无法访问: {}", sftpPath);
                return false;
            }

            // 3. 从路径中提取文件名(如 "/upload/test.txt" -> "test.txt")
            String fileName = sftpPath.substring(sftpPath.lastIndexOf("/") + 1);

            // 4. 设置HTTP响应头(支持中文文件名)
            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
            response.setHeader(
                HttpHeaders.CONTENT_DISPOSITION,
                "attachment; filename=" + URLEncoder.encode(fileName, StandardCharsets.UTF_8.name())
            );

            // 5. 下载文件(直接使用完整路径)
            sftp.get(sftpPath, response.getOutputStream());
            log.info("文件下载成功: {}", sftpPath);
            return true;

        } catch (SftpException e) {
            log.error("SFTP操作失败: {}", e.getMessage(), e);
            return false;
        } catch (IOException | JSchException e) {
            log.error("系统异常: {}", e.getMessage(), e);
            return false;
        } finally {
            sftpUtils.disconnect(sftp);
        }
    }

    @Override
    public byte[] download(String sftpPath) {
        ChannelSftp sftp = null;
        try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            // 1. 建立SFTP连接
            sftp = sftpUtils.createSftp();

            // 2. 检查文件是否存在
            try {
                sftp.lstat(sftpPath);
            } catch (SftpException e) {
                log.error("文件不存在: {}", sftpPath);
                return null;
            }

            // 3. 下载文件到内存
            sftp.get(sftpPath, outputStream);
            log.info("文件下载成功: {}", sftpPath);
            return outputStream.toByteArray();

        } catch (SftpException | JSchException | IOException e) {
            log.error("下载失败: {}", e.getMessage(), e);
            return null;
        } finally {
            sftpUtils.disconnect(sftp);
        }
    }

    @Override
    public boolean rename(String oldPath, String newPath) {
        // 重命名文件(移动)
        ChannelSftp sftp = null;
        try {
            // 开启sftp连接
            sftp = sftpUtils.createSftp();

            // 修改sftp文件路径
            sftp.rename(oldPath, newPath);
            log.info("sftp文件重命名成功,历史路径:{},新路径:{}", oldPath, newPath);
            return true;
        } catch (SftpException | JSchException e) {
            log.error("sftp文件重命名失败,原因:{}", e.getMessage(), e);
            return false;
        } finally {
            // 关闭sftp
            sftpUtils.disconnect(sftp);
        }
    }

    @Override
    public boolean delete(String sftpPath) {
        // 删除文件
        ChannelSftp sftp = null;
        try {
            // 开启sftp连接
            sftp = sftpUtils.createSftp();

            // 判断sftp文件存在
            boolean isExist = isFileExist(sftpPath, sftp);
            if (!isExist) {
                log.error("sftp文件删除失败,sftp文件不存在:" + sftpPath);
                return false;
            }

            // 删除文件
            SftpATTRS sftpATTRS = sftp.lstat(sftpPath);
            if (sftpATTRS.isDir()) {
                sftp.rmdir(sftpPath);
            } else {
                sftp.rm(sftpPath);
            }
            log.info("sftp文件删除成功,目标文件:{}.", sftpPath);
            return true;
        } catch (SftpException | JSchException e) {
            log.error("sftp文件删除失败,原因:{}", e.getMessage(), e);
            return false;
        } finally {
            // 关闭sftp
            sftpUtils.disconnect(sftp);
        }
    }



    /**
     * 判断目录是否存在
     */
    private boolean isFileExist(String sftpPath, ChannelSftp sftp) {
        try {
            // 获取文件信息
            SftpATTRS sftpATTRS = sftp.lstat(sftpPath);
            return sftpATTRS != null;
        } catch (Exception e) {
            log.error("判断文件是否存在失败,原因:{}", e.getMessage(), e);
            return false;
        }
    }
}

control层调用

@RestController
@RequestMapping("/sftp")
public class SFtpController {

    @Resource
    private SFtpService sftpService;

    /**
     * 上传文件
     */
    @PostMapping("/upload")
    public R<Object> upload(@RequestParam String sftpPath, @RequestParam MultipartFile file) {
        sftpService.upload(sftpPath, file);
        return R.success();
    }

    /**
     * 下载文件
     */
    @GetMapping("/download")
    public void download(@RequestParam String sftpPath,  HttpServletResponse response) {
        sftpService.download(sftpPath, response);
    }

    /**
     * 重命名文件(移动)
     */
    @GetMapping("/rename")
    public R<Object> rename(@RequestParam String oldPath,  @RequestParam String newPath) {
        sftpService.rename(oldPath, newPath);
        return R.success();
    }

    /**
     * 删除文件
     */
    @GetMapping("/delete")
    public R<Object> delete(@RequestParam String sftpPath) {
        sftpService.delete(sftpPath);
        return R.success();
    }
}

这里用SFTP容易有个坑,可能你写的你能连接上,到了生产上连接不上。这个后续公众号会发,或者详细见我的csdn这篇文章https://blog.csdn.net/m0_58680378/article/details/147094033?spm=1011.2415.3001.5331

posted @ 2025-07-10 19:01  叮咚~到账一个亿  阅读(159)  评论(0)    收藏  举报