Loading

FTP服务器搭建踩坑和工具类实现

FTP服务器搭建

    这边不再介绍安装方法,可以自己GPT下,主要记录一些安装过程中踩到的坑。

vsftpd(Linux)

如何配置SSL/TLS隐式加密连接

  1. 生成公钥和密钥
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/vsftpd/vsftpd.pem -out /etc/vsftpd/vsftpd.pem
  1. 在配置文件中添加如下配置
# 启用SSL
ssl_enable=YES

# 使用隐式FTP over TLS
implicit_ssl=YES

# 指定证书文件路径
rsa_cert_file=/etc/vsftpd/vsftpd.pem
rsa_private_key_file=/etc/vsftpd/vsftpd.pem

# 允许匿名用户使用TLS
allow_anon_ssl=NO

# 强制使用TLS进行数据传输
force_local_data_ssl=YES
force_local_logins_ssl=YES

# 设置允许的最小TLS版本
ssl_tlsv1=YES
ssl_sslv2=NO
ssl_sslv3=NO

# 设置允许的加密套件
ssl_ciphers=HIGH

FileZilla Server(Windows)

SSL/TLS加密连接失败可能出现的原因

  1. 公钥和私钥的路径不要有中文,不然你安装的版本可能出现识别不到,导致一直连接失败!

工具类实现

    为了实现FTP服务器上上传和下载,并且支持FTPS,编写了如下工具类。FTP连接主要使用hutool工具,FTPS连接使用Apache Common Net。故操作前需导入对应依赖。

  <dependency>
      <groupId>cn.hutool</groupId>
      <artifactId>hutool-all</artifactId>
      <version>5.7.11</version>
  </dependency>
  <dependency>
      <groupId>commons-net</groupId>
      <artifactId>commons-net</artifactId>
      <version>3.8.0</version>
  </dependency>
  1. 工具类所需的登录参数DTO
@Data
@ApiModel(value = "FTP登录信息DTO")
public class FtpLoginDTO {

    @ApiModelProperty(value = "FTP服务器IP地址")
    private String host;

    @ApiModelProperty(value = "FTP服务器端口")
    private int port;

    @ApiModelProperty(value = "FTP服务器用户名")
    private String username;

    @ApiModelProperty(value = "FTP服务器密码")
    private String password;

    @ApiModelProperty(value = "连接方式 NONE:不加密 EXPLICIT:SSL/TLS显式加密 IMPLICIT:SSL/TLS隐式加密")
    private String encryptionMode;

    @ApiModelProperty(value = "传输模式 ACTIVE:主动 PASSIVE:被动")
    private String transmissionMode;

    @ApiModelProperty(value = "最大重试次数")
    private int maxRetries;
}
  1. 所需用到的常量类
public class FtpConstant {

    /**
     * 加密方式:不加密
     */
    public static final String ENCRYPTION_MODE_NONE = "NONE";

    /**
     * 加密方式:SSL/TLS 显式加密
     */
    public static final String ENCRYPTION_MODE_EXPLICIT = "EXPLICIT";

    /**
     * 加密方式:SSL/TLS 隐式加密
     */
    public static final String ENCRYPTION_MODE_IMPLICIT = "IMPLICIT";


    /**
     * 传输模式:主动
     */
    public static final String TRANSMISSION_MODE_ACTIVE = "ACTIVE";

    /**
     * 传输模式:被动
     */
    public static final String TRANSMISSION_MODE_PASSIVE = "PASSIVE";
}
  1. FTPS工具类实现
package net.evecom.iaplatform.common.utils;

import cn.hutool.core.util.ObjectUtil;
import net.evecom.iaplatform.common.constants.FtpConstant;
import org.apache.commons.net.PrintCommandListener;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import org.apache.commons.net.ftp.FTPSClient;
import org.slf4j.Logger;

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * <p>
 * <B>Description: Ftps工具</B>
 * </P>
 *
 * @author Ryan Huang
 * @version 1.0
 */
public class FtpsUtil {

    private static final Logger logger = org.slf4j.LoggerFactory.getLogger(FtpsUtil.class);
    private final String host;
    private final int port;
    private final String username;
    private final String password;
    /**
     * 传输模式 ACTIVE:主动 PASSIVE:被动
     */
    private final String transmissionMode;

    private final FTPSClient ftpsClient;
    private final int maxRetries;


    /**
     * 构造函数,用于初始化FTPS工具类,设置服务器凭证、重试次数和加密模式。
     *
     * @param host          FTPS服务器主机名。
     * @param port          FTPS服务器端口。
     * @param username      认证用户名。
     * @param password      认证密码。
     * @param maxRetries    操作的最大重试次数。
     */
    public FtpsUtil(String host, int port, String username, String password, String transmissionMode, String encryptionMode, int maxRetries) {
        this.host = host;
        this.port = port;
        this.username = username;
        this.password = password;
        this.transmissionMode = transmissionMode;
        this.maxRetries = maxRetries;


        try {
            if(ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_IMPLICIT)) {
                ftpsClient = new FTPSClient("TLS", true);
            }else if(ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_EXPLICIT)){
                ftpsClient = new FTPSClient("TLS", false);
            }else{
                throw new RuntimeException("加密方式不正确");
            }
            ftpsClient.addProtocolCommandListener(new PrintCommandListener(new PrintWriter(System.out)));

        }catch (Exception e){
            logger.error("FTP连接失败:", e);
            throw new RuntimeException("FTP连接失败:" + e.getMessage());
        }
    }


    /**
     * 初始化与FTPS服务器的连接,并在连接失败时实现重试机制。
     *
     * @return 如果连接成功返回true,否则返回false。
     */
    public boolean connect() {
        int retries = 0;
        while (retries < maxRetries) {
            try {
                ftpsClient.connect(host, port);
                int reply = ftpsClient.getReplyCode();
                if (!FTPReply.isPositiveCompletion(reply)) {
                    ftpsClient.disconnect();
                    logger.error("FTP服务器拒绝连接。");
                    retries++;
                    continue;
                }
                if (ftpsClient.login(username, password)) {
                    ftpsClient.execPBSZ(0);
                    ftpsClient.execPROT("P");
                    ftpsClient.setFileType(FTP.BINARY_FILE_TYPE);
                    //传输模式
                    if(ObjectUtil.equals(transmissionMode, FtpConstant.TRANSMISSION_MODE_ACTIVE)){
                        ftpsClient.enterLocalActiveMode();
                    }else if(ObjectUtil.equals(transmissionMode, FtpConstant.TRANSMISSION_MODE_PASSIVE)) {
                        ftpsClient.enterLocalPassiveMode();
                    }
                    logger.info("已连接到FTPS服务器。");
                    return true;
                } else {
                    ftpsClient.logout();
                    logger.error("FTPS登录失败。");
                    retries++;
                }
            } catch (IOException ex) {
                logger.error("连接尝试失败: " + ex.getMessage());
                retries++;
            }
            try {
                Thread.sleep(2000); // 重试前等待2秒
            } catch (InterruptedException e) {
                logger.error("线程中断", e);
            }
        }
        logger.error("FTP连接失败。");
        throw new RuntimeException("FTP连接失败。");
    }

    /**
     * 从FTPS服务器下载文件到本地文件系统,并在下载失败时实现重试机制。
     *
     * @param remoteFilePath 服务器上的文件路径。
     * @param localFilePath  本地文件系统的目标路径。
     * @return 如果文件下载成功返回true,否则返回false。
     */
    public boolean downloadFile(String remoteFilePath, String localFilePath) {
        int retries = 0;
        while (retries < maxRetries) {
            if (!ftpsClient.isConnected()) {
                if (!connect()) {
                    retries++;
                    continue;
                }
            }
            try (OutputStream outputStream = Files.newOutputStream(Paths.get(localFilePath))) {
                boolean success = ftpsClient.retrieveFile(remoteFilePath, outputStream);
                if (success) {
                    logger.info("文件【{}】下载成功。", remoteFilePath);
                    return true;
                } else {
                    retries++;
                }
            } catch (IOException ex) {
                logger.error("下载【{}】文件时出错: {}", remoteFilePath, ex.getMessage());
                retries++;
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                logger.error("线程中断", e);
            }
        }
        return false;
    }

    /**
     * 从本地文件系统上传文件到FTPS服务器,并在上传失败时实现重试机制。
     *
     * @param localFilePath  本地文件的路径。
     * @param remoteFilePath 服务器上的目标路径。
     * @return 如果文件上传成功返回true,否则返回false。
     */
    public boolean uploadFile(String localFilePath, String remoteFilePath) {
        int retries = 0;
        while (retries < maxRetries) {
            if (!ftpsClient.isConnected()) {
                if (!connect()) {
                    retries++;
                    continue;
                }
            }
            try (InputStream inputStream = Files.newInputStream(Paths.get(localFilePath))) {
                boolean success = ftpsClient.storeFile(remoteFilePath, inputStream);
                if (success) {
                    logger.info("文件【{}】上传成功。", remoteFilePath);
                    return true;
                } else {
                    logger.error("文件【{}】上传失败。", remoteFilePath);
                    retries++;
                }
            } catch (IOException ex) {
                logger.error("上传【{}】文件时出错: {}", remoteFilePath, ex.getMessage());
                retries++;
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                logger.error("线程中断", e);
            }
        }
        return false;
    }


    /**
     * 判断文件或目录是否存在
     *
     * @param path 文件或目录路径
     * @return true:存在,false:不存在
     */
    public boolean isExist(String path) {
        try {
            // 切换到指定目录
            boolean result = ftpsClient.changeWorkingDirectory(path);
            if (result) {
                return true; // 目录存在
            }

            // 尝试获取文件信息
            FTPFile[] files = ftpsClient.listFiles(path);
            if (files != null && files.length > 0) {
                return true; // 文件存在
            }
        } catch (Exception e) {
            // 发生异常,文件或目录不存在
            return false;
        }
        return false;
    }

    /**
     * 创建多级目录
     *
     * @param directory 目录路径
     */
    public void mkdirs(String directory) {
        String[] dirs = directory.split("/");
        String currentDir = "";
        for (String dir : dirs) {
            currentDir += dir + "/";
            if (!isExist(currentDir)) {
                try {
                    if (!ftpsClient.makeDirectory(currentDir)) {
                        break;
                    }
                } catch (IOException e) {
                    logger.error("创建目录 " + currentDir + " 失败:" + e.getMessage());
                }
            }
        }
    }


    /**
     * 断开与FTPS服务器的连接。
     */
    public void disconnect() {
        if (ftpsClient.isConnected()) {
            try {
                ftpsClient.logout();
                ftpsClient.disconnect();
            } catch (IOException ex) {
                logger.error("断开连接失败:" + ex.getMessage(), ex);
            }
        }
    }
}

  1. FTP工具类实现
package net.evecom.iaplatform.common.utils;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.ftp.Ftp;
import cn.hutool.extra.ftp.FtpMode;
import net.evecom.iaplatform.api.model.dto.FtpLoginDTO;
import net.evecom.iaplatform.common.constants.FtpConstant;
import org.slf4j.Logger;

import java.io.*;
import java.nio.file.Files;

/**
 * <p>
 * <B>Description: FTP 工具</B>
 * </P>
 *
 * @author Ryan Huang
 * @version 1.0
 */
public class FtpUtil {

    private static final Logger logger = org.slf4j.LoggerFactory.getLogger(FtpUtil.class);
    private final String host;
    private final int port;
    private final String username;
    private final String password;

    /**
     * 连接方式 NONE:不加密 EXPLICIT:SSL/TLS显式加密 IMPLICIT:SSL/TLS隐式加密
     */
    private final String encryptionMode;

    /**
     * 传输模式 ACTIVE:主动 PASSIVE:被动
     */
    private final String transmissionMode;

    /**
     * 重连最大次数
     */
    private final int maxRetries;

    /**
     * FTP 客户端
     */
    private Ftp ftpClient;

    /**
     * FTPS工具
     */
    private FtpsUtil ftpsUtil;

    /**
     * 构造函数,用于初始化FTP
     *
     * @param info          登录信息
     */
    public FtpUtil(FtpLoginDTO info) {
        this.host = info.getHost();
        this.port = info.getPort();
        this.username = info.getUsername();
        this.password = info.getPassword();
        this.encryptionMode = info.getEncryptionMode();
        this.transmissionMode = info.getTransmissionMode();
        this.maxRetries = info.getMaxRetries();
        try {
            initializeFtpsClient();
        }catch (Exception e) {
            logger.error("FTP连接失败:" + e.getMessage(), e);
            throw new RuntimeException("FTP连接失败:" + e.getMessage());
        }
    }

    /**
     * 判断是否是FTPS
     */
    public boolean isFtps(){
        return ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_IMPLICIT) || ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_EXPLICIT);
    }

    /**
     * 初始化FTP客户端
     */
    private void initializeFtpsClient() {
        //SSL/TLS加密
        if(isFtps()){
            ftpsUtil = new FtpsUtil(host, port, username, password, transmissionMode, encryptionMode, maxRetries);
            ftpsUtil.connect();
        }
        //不加密
        else {
            ftpClient = new Ftp(host, port, username, password);
            if (ObjectUtil.equals(transmissionMode, FtpConstant.TRANSMISSION_MODE_ACTIVE)) {
                ftpClient.setMode(FtpMode.Active);
            } else if (ObjectUtil.equals(transmissionMode, FtpConstant.TRANSMISSION_MODE_PASSIVE)) {
                ftpClient.setMode(FtpMode.Passive);
            }
        }
    }

    /**
     * 从FTPS服务器下载文件并返回一个InputStream,并在下载失败时实现重试机制。
     *
     * @param remoteFilePath 服务器上的文件路径。
     * @return 下载文件的InputStream,如果下载失败返回null。
     */
    public InputStream downloadFile(String remoteFilePath) {
        String localFilePath = "/data/" + remoteFilePath;
        File localFile = new File(localFilePath);
        //目录不存在则创建
        File parentFile = localFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        if(isFtps()) {
            boolean result = ftpsUtil.downloadFile(remoteFilePath, localFilePath);
            if(!result){
                throw new RuntimeException("文件【" + remoteFilePath + "】下载失败。");
            }
        }else {
            ftpClient.download(remoteFilePath, localFile);
        }
        try {
            return Files.newInputStream(localFile.toPath());
        } catch (IOException e) {
            throw new RuntimeException("文件【" + remoteFilePath + "】下载失败:" + e.getMessage());
        }
    }

    /**
     * 从本地文件系统上传文件到FTPS服务器,并在上传失败时实现重试机制。
     *
     * @param localFilePath  本地文件的路径。
     * @param remoteFilePath 服务器上的目标路径。
     * @return 如果文件上传成功返回true,否则返回false。
     */
    public boolean uploadFile(String localFilePath, String remoteFilePath) {
        //根据文件路径获取目录和文件名
        String fileName = remoteFilePath.substring(remoteFilePath.lastIndexOf("/") + 1);
        String remotePath = remoteFilePath.substring(0, remoteFilePath.lastIndexOf("/"));
        if(isFtps()) {
            ftpsUtil.mkdirs(remotePath);
            return ftpsUtil.uploadFile(localFilePath, remoteFilePath);
        }else {
            //判断ftpClient目标目录是否存在不存在则创建
            if (!ftpClient.exist(remotePath)) {
                ftpClient.mkDirs(remotePath);
            }
            return ftpClient.upload(remotePath, fileName, new File(localFilePath));
        }
    }


    /**
     * 断开与FTPS服务器的连接。
     */
    public void disconnect() {
        try {
            if(isFtps()) {
                ftpsUtil.disconnect();
            }else {
                ftpClient.close();
            }
        }catch (Exception e){
            logger.error("断开连接失败:"+e.getMessage(),e);
        }
    }
}


其他

    另外写了一篇关于解决425 Unable to build data connection: TLS session of data connection not resumed.的随笔,有需要可以看看:https://www.cnblogs.com/IamHzc/p/18516621

posted @ 2024-10-28 09:42  IamHzc  阅读(52)  评论(0编辑  收藏  举报