背景
10年前的老代码,需要升级springboot框架,在升级过程中,测试业务流程里,有FTP的下载业务,不管测试环境如何测试,都没有成功,最后只能自己搭建一个FTP服务器,写一个ftp-demo来测试。记录一下过程,防止后续使用的时候在来一次。
CentOS7安装FTP
步骤1:安装 vsftpd
TypeScript
# 更新系统软件包
sudo yum update -y
# 安装 vsftpd
sudo yum install vsftpd -y
步骤2:启动服务并设置开机自启
TypeScript
# 启动 vsftpd 服务
sudo systemctl start vsftpd
# 设置开机自启
sudo systemctl enable vsftpd
# 检查服务状态
sudo systemctl status vsftpd
# 停止服务
sudo systemctl stop vsftpd
# 重启服务
sudo systemctl restart vsftpd
步骤3:配置防火墙
TypeScript
# 开放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
TypeScript
cd /etc/vsftpd/
cp vsftpd.conf vsftpd.conf_default
修改下列参数的值
TypeScript
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
以上配置可以直接用下面命令进行替换修改(一句一句执行)
TypeScript
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用户
TypeScript
# 创建用户(例如用户名为 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服务
TypeScript
# 启动 vsftpd 服务
sudo systemctl restart vsftpd
步骤7:客户端登录测试
TypeScript
ftp localhost
问题1:ftp客户端未安装

解决办法:
TypeScript
sudo yum install ftp
问题2:登录失败
找的截图,不要纠结里边的命令,只看红框部分即可

解决办法:
TypeScript
vi /etc/pam.d/vsftpd
# 注释以下一行
#auth required pam_shells.so

重启vsftpd服务,再进行登录提示:

再手动在/etc/vsftpd/目录下创建一下chroot_list文件即可
TypeScript
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
扩展防火墙命令:
TypeScript
一、防火墙的开启、关闭、禁用命令
设置开机启用防火墙: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需要引用的包:
XML
......
<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>
......
配置文件:
TypeScript
ftp:
# 服务器地址
host: 192.168.1.56
# 端口号
port: 21
# 用户名
userName: douzi
# 密码
password: 123456
代码部分:
java
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;
}
}
java
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);
}
}
}
}
java
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 然后查看内容

浏览器执行
html
http://localhost:8080/download


windows下,代码在哪个盘执行,就会生成在哪个盘。

下载成功!其他上传,删除等功能自行试验。
打完收工。