FTP伺服器搭建
這邊不再介紹安裝方法,可以自己GPT下,主要記錄一些安裝過程中踩到的坑。
vsftpd(Linux)
如何配置SSL/TLS隱式加密連線
- 生成公鑰和金鑰
sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/vsftpd/vsftpd.pem -out /etc/vsftpd/vsftpd.pem
- 在配置檔案中新增如下配置
# 啟用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加密連線失敗可能出現的原因
- 公鑰和私鑰的路徑不要有中文,不然你安裝的版本可能出現識別不到,導致一直連線失敗!
工具類實現
為了實現FTP伺服器上上傳和下載,並且支援FTPS,編寫了如下工具類。
- 工具類所需的登入引數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;
}
- 所需用到的常量類
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";
}
- 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);
}
}
}
}
- 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);
}
}
}