FTP伺服器搭建踩坑和工具類實現

IamHzc發表於2024-10-28

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,編寫了如下工具類。

  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());
        }
    }

    /**
     * 初始化FTP客戶端
     */
    private void initializeFtpsClient() {
        //SSL/TLS隱式加密
        if(ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_IMPLICIT) || ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_EXPLICIT)){
            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/local/ + remoteFilePath;
        File localFile = new File(localFilePath);
        //目錄不存在則建立
        File parentFile = localFile.getParentFile();
        if (!parentFile.exists()) {
            parentFile.mkdirs();
        }
        if(ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_IMPLICIT)) {
            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(ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_IMPLICIT)) {
            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(ObjectUtil.equals(encryptionMode, FtpConstant.ENCRYPTION_MODE_IMPLICIT)) {
                ftpsUtil.disconnect();
            }else {
                ftpClient.close();
            }
        }catch (Exception e){
            logger.error("斷開連線失敗:"+e.getMessage(),e);
        }
    }
}

相關文章