背景
?????? 10年前的老代碼,需要升級springboot框架,在升級過程中,測試業務流程里,有FTP的下載業務,不管測試環境如何測試,都沒有成功,最后只能自己搭建一個FTP服務器,寫一個ftp-demo來測試。記錄一下過程,防止后續使用的時候在來一次。
CentOS7安裝FTP
步驟1:安裝 vsftpd
# 更新系統軟件包
sudo yum update -y# 安裝 vsftpd
sudo yum install vsftpd -y
步驟2:啟動服務并設置開機自啟
# 啟動 vsftpd 服務
sudo systemctl start vsftpd# 設置開機自啟
sudo systemctl enable vsftpd# 檢查服務狀態
sudo systemctl status vsftpd# 停止服務
sudo systemctl stop vsftpd# 重啟服務
sudo systemctl restart vsftpd
步驟3:配置防火墻
# 開放FTP端口(21和被動模式端口范圍)
sudo firewall-cmd --zone=public --add-port=21/tcp --permanent
sudo firewall-cmd --zone=public --add-port=30000-31000/tcp --permanent
sudo firewall-cmd --zone=public --add-service=ftp --permanent# 重新加載防火墻規則
sudo firewall-cmd --reload
步驟4:配置 vsftpd
cd /etc/vsftpd/cp vsftpd.conf vsftpd.conf_default
修改下列參數的值
anonymous_enable=NO #禁止匿名登錄FTP服務器
local_enable=YES #允許本地用戶登錄FTP服務器
listen=YES #監聽IPv4 sockets
#listen_ipv6=YES #關閉監聽IPv6 sockets或者改為NO
chroot_local_user=YES #全部用戶被限制在主目錄
chroot_list_enable=YES #啟用例外用戶名單
chroot_list_file=/etc/vsftpd/chroot_list #指定例外用戶列表文件,列表中用戶不被鎖定在主目錄
allow_writeable_chroot=YES
pasv_enable=YES
pasv_min_port=30000
pasv_max_port=31000
以上配置可以直接用下面命令進行替換修改(一句一句執行)
sed -i 's/anonymous_enable=YES/anonymous_enable=NO/' /etc/vsftpd/vsftpd.conf
sed -i 's/listen=NO/listen=YES/' /etc/vsftpd/vsftpd.conf
sed -i 's/listen_ipv6=YES/listen_ipv6=NO/' /etc/vsftpd/vsftpd.conf
sed -i 's/#chroot_local_user=YES/chroot_local_user=YES/' /etc/vsftpd/vsftpd.conf
sed -i 's/#chroot_list_enable=YES/chroot_list_enable=YES/' /etc/vsftpd/vsftpd.conf
sed -i 's/#chroot_list_file=/chroot_list_file=/' /etc/vsftpd/vsftpd.conf
echo "allow_writeable_chroot=YES" >> /etc/vsftpd/vsftpd.conf
# 被動模式
echo "pasv_enable=YES">> /etc/vsftpd/vsftpd.conf
echo "pasv_min_port=30001">> /etc/vsftpd/vsftpd.conf
echo "pasv_max_port=30010">> /etc/vsftpd/vsftpd.conf
?步驟5:創建FTP用戶
# 創建用戶(例如用戶名為 ftpuser,目錄為 /home/ftpuser)
sudo useradd -m -d /home/ftpuser -s /sbin/nologin ftpuser# 設置用戶密碼
sudo passwd ftpuser# 確保用戶目錄權限正確
sudo chmod -R 750 /home/ftpuser
sudo chown -R ftpuser: /home/ftpuser例如:
sudo useradd -m -d /home/douzi -s /sbin/nologin douzi
sudo passwd 123456
sudo chmod -R 750 /home/douzi
sudo chown -R douzi: /home/douzi
步驟6:重啟vsftpd服務
# 啟動 vsftpd 服務
sudo systemctl restart vsftpd
步驟7:客戶端登錄測試
ftp localhost
問題1:ftp客戶端未安裝
解決辦法:
sudo yum install ftp
問題2:登錄失敗
找的截圖,不要糾結里邊的命令,只看紅框部分即可
?解決辦法:
vi /etc/pam.d/vsftpd# 注釋以下一行
#auth required pam_shells.so
重啟vsftpd服務,再進行登錄提示:
再手動在/etc/vsftpd/目錄下創建一下chroot_list文件即可
cd /etc/vsftpd/touch /etc/vsftpd/chroot_list
?然后重啟vsftpd服務,登錄即可正常:
登陸后默認為二進制傳輸模式
擴展FTP客戶端命令:
dir ls cd pwd?lcd(切換工作目錄)? mkdir rmdir?get?mget(下載多個文件) put?mput(上傳多個文件) rename?delete?mdelete(刪除多個ftp文件) rmdir?ascii,bin(切換傳輸模式) close(關閉鏈接) open(重連ftp) quit
擴展防火墻命令:
一、防火墻的開啟、關閉、禁用命令
設置開機啟用防火墻:systemctl enable firewalld
設置開機禁用防火墻:systemctl disable firewalld
啟動防火墻: systemctl start firewalld
關閉防火墻: systemctl stop firewalld 或 systemctl stop firewalld.service
檢查防火墻狀態 systemctl status firewalld二、使用firewall-cmd配置端口
查看防火墻狀態: firewall-cmd --state
重新加載配置: firewall-cmd --reload
查看開放的端口: firewall-cmd --list-ports
開啟防火墻端口: firewall-cmd --zone=public --add-port=9200/tcp --permanent
擴展FTP配置項說明:
1. 基礎訪問控制
配置項 | 默認值 | 說明 |
---|---|---|
anonymous_enable | NO | 是否允許匿名登錄(YES /NO ) |
local_enable | YES | 是否允許本地用戶登錄(YES /NO ) |
write_enable | YES | 是否允許寫入操作(上傳/刪除/重命名) |
2. 權限與安全
配置項 | 默認值 | 說明 |
---|---|---|
local_umask | 022 | 本地用戶創建文件的權限掩碼(022 表示文件權限為644 ,目錄為755 ) |
chroot_local_user | NO | 是否將本地用戶限制在其主目錄(需配合allow_writeable_chroot=YES 使用) |
allow_writeable_chroot | NO | 允許被chroot 的用戶目錄可寫(需chroot_local_user=YES ) |
3. 連接與日志
配置項 | 默認值 | 說明 |
---|---|---|
dirmessage_enable | YES | 顯示目錄歡迎消息(消息文件默認為.message ) |
xferlog_enable | YES | 啟用傳輸日志(記錄上傳/下載) |
xferlog_file | /var/log/vsftpd.log | 指定日志文件路徑 |
xferlog_std_format | YES | 使用標準FTP日志格式(兼容wu-ftp 格式) |
connect_from_port_20 | YES | 主動模式時,強制數據連接從端口20發起 |
4. 超時設置
配置項 | 默認值 | 說明 |
---|---|---|
idle_session_timeout | 600 | 空閑會話超時時間(秒) |
data_connection_timeout | 120 | 數據連接超時時間(秒) |
5. 被動模式(PASV)配置
配置項 | 默認值 | 說明 |
---|---|---|
pasv_enable | YES | 啟用被動模式 |
pasv_min_port | - | 被動模式端口范圍下限(如30000 ) |
pasv_max_port | - | 被動模式端口范圍上限(如31000 ) |
pasv_address | - | 服務器公網IP(NAT環境下需指定) |
6. 高級選項
配置項 | 默認值 | 說明 |
---|---|---|
listen | NO | 以獨立模式運行(YES =IPv4,NO =通過xinetd啟動) |
listen_ipv6 | NO | 啟用IPv6監聽 |
tcp_wrappers | YES | 使用TCP Wrappers進行主機訪問控制 |
userlist_enable | NO | 啟用用戶列表控制(userlist_file 指定文件) |
userlist_deny | YES | 用戶列表中的用戶是否被拒絕(YES =黑名單,NO =白名單) |
FTP-DEMO Springboot3代碼
maven需要引用的包:
......<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.5</version><relativePath /> <!-- lookup parent from repository --></parent> ......<properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.35</version></dependency><dependency><groupId>commons-net</groupId><artifactId>commons-net</artifactId><version>3.11.1</version></dependency><dependency><groupId>com.jcraft</groupId><artifactId>jsch</artifactId><version>0.1.55</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>ch.qos.logback</groupId><artifactId>logback-core</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>......
配置文件:
ftp:# 服務器地址host: 192.168.1.56# 端口號port: 21# 用戶名userName: douzi# 密碼password: 123456
代碼部分:
package com.wd.ftp.config;import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;/*** ftp配置*/
@Configuration
public class FtpConfig {/*** 服務器地址*/private static String host;/*** 端口*/private static Integer port;/*** 用戶名*/private static String userName;/*** 密碼*/private static String password;@Value("${ftp.host}")public void setHost(String host) {FtpConfig.host = host;}public static String getHost() {return host;}@Value("${ftp.port}")public void setPort(Integer port) {FtpConfig.port = port;}public static Integer getPort() {return port;}@Value("${ftp.userName}")public void setUserName(String userName) {FtpConfig.userName = userName;}public static String getUserName() {return userName;}@Value("${ftp.password}")public void setPassword(String password) {FtpConfig.password = password;}public static String getPassword() {return password;}
}
package com.wd.ftp.util;import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;import org.apache.commons.net.ftp.FTPFile;import com.wd.ftp.config.FtpConfig;import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.ftp.Ftp;
import cn.hutool.extra.ftp.FtpMode;
import lombok.extern.slf4j.Slf4j;/*** FTP服務工具類*/
@Slf4j
public class FtpUtil {/*** 獲取 FTPClient對象*/private static Ftp getFTPClient() {try {if(StrUtil.isBlank(FtpConfig.getHost()) || FtpConfig.getPort() == null|| StrUtil.isBlank(FtpConfig.getUserName()) || StrUtil.isBlank(FtpConfig.getPassword())) {throw new RuntimeException("ftp配置信息不能為空");}Ftp ftp = new Ftp(FtpConfig.getHost(),FtpConfig.getPort(),FtpConfig.getUserName(),FtpConfig.getPassword());//設置為被動模式,防止防火墻攔截ftp.setMode(FtpMode.Passive);return ftp;} catch (Exception e) {e.printStackTrace();log.error("獲取ftp客戶端異常",e);throw new RuntimeException("獲取ftp客戶端異常:"+e.getMessage());}}/*** 下載ftp服務器上的文件到本地* @param remoteFile ftp上的文件路徑* @param localFile 輸出的目錄,使用服務端的文件名*/public static void download(String remoteFile, String localPath) {if(StrUtil.isBlank(remoteFile) || StrUtil.isBlank(localPath)) {return;}Ftp ftp = getFTPClient();try {if(!FileUtil.exist(localPath)){FileUtil.mkdir(localPath);} File lFile = FileUtil.file(localPath);ftp.download(remoteFile, lFile);} catch (Exception e) {e.printStackTrace();log.error("FTP文件下載異常",e);} finally {//關閉連接try {if(ftp != null) ftp.close();} catch (IOException e) {throw new RuntimeException(e);}}}/*** 本地文件上傳到ftp服務器上* @param remoteDir 上傳的ftp目錄* @param remoteFileName 保存到ftp服務器上的名稱* @param localFile 本地文件全名稱*/public static boolean upload(String remoteDir, String remoteFileName, String localFile) {if(StrUtil.isBlank(remoteDir) || StrUtil.isBlank(remoteFileName) || StrUtil.isBlank(localFile)) {return false;}Ftp ftp = getFTPClient();try {File lFile = FileUtil.file(localFile);if(!lFile.exists()) {log.error("本地文件不存在");return false;}if(StrUtil.isBlank(remoteFileName)) {return ftp.upload(remoteDir, lFile);} else {return ftp.upload(remoteDir, remoteFileName, lFile);}} catch (Exception e) {e.printStackTrace();log.error("文件上傳FTP異常",e);return false;} finally {//關閉連接try {if(ftp != null) ftp.close();} catch (IOException e) {throw new RuntimeException(e);}}}/*** 刪除FTP服務器中的文件* @param remoteFile ftp上的文件路徑*/public static boolean delFile(String remoteFile) {if(StrUtil.isBlank(remoteFile)) {return false;}Ftp ftp = getFTPClient();try {return ftp.delFile(remoteFile);} catch (Exception e) {e.printStackTrace();log.error("刪除FTP服務器中的文件異常",e);return false;} finally {//關閉連接try {if(ftp != null) ftp.close();} catch (IOException e) {throw new RuntimeException(e);}}}/*** 遍歷某個目錄下所有文件,不會遞歸遍歷* @param path 目錄*/public static List<String> listFile(String path) {List<String> listFile = new ArrayList<>();Ftp ftp = getFTPClient();try {FTPFile[] ftpFiles = ftp.lsFiles(path);for (int i = 0; i < ftpFiles.length; i++) {FTPFile ftpFile = ftpFiles[i];if(ftpFile.isFile()){listFile.add(ftpFile.getName());}}return listFile;} catch (Exception e) {e.printStackTrace();log.error("遍歷某個目錄下所有文件異常",e);return null;} finally {//關閉連接try {if(ftp != null) ftp.close();} catch (IOException e) {throw new RuntimeException(e);}}}
}
package com.wd.ftp.controller;import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;import com.wd.ftp.util.FtpUtil;
import com.wd.ftp.util.SftpUtil;import jakarta.servlet.http.HttpServletRequest;@RestController
public class FtpController {/*** 下載ftp服務器上的文件到本地* @param rf ftp上的文件路徑* @param lp 輸出的目錄,使用服務端的文件名*/@GetMapping("/download")public String download(@RequestParam(required = false, defaultValue = "1.xml") String rf, @RequestParam(required = false, defaultValue = "/ftp/download/") String lp) {FtpUtil.download(rf, lp);return "success";}}
可以根據FtpUtil擴展SftpUtil的代碼,從而支持Sftp模式。
執行效果:
隨便造一個1.xml上傳ftp
cd /home/douzi 然后查看內容
?
瀏覽器執行
http://localhost:8080/download
windows下,代碼在哪個盤執行,就會生成在哪個盤。
下載成功!其他上傳,刪除等功能自行試驗。
打完收工。