一、ftp服务的搭建与简单使用
1、windows下搭建
步骤一: 安装FTP服务器支持和IIS管理平台。
操作步骤: 电脑 => 控制面板 => 程序和功能 => 启用和关闭Windows功能 => Internet Infomation Services => 勾选【FTP服务器】和Web管理工具的【IIS管理控制台】=> 点击确定等待安装完成

步骤二: 打开IIS管理器
操作步骤: 电脑 => 控制面板 => 管理工具 => Internet Infomation Services(IIS)管理器

步骤三: 创建FTP服务器
操作1:
在某个盘符如D盘,创建一个FTP共享文件夹,用于FTP共享文件存放地址
操作2:
右键IIS管理器左边导航栏 => 添加FTP站点

操作3:
指定【站点名称】和【FTP共享的文件夹路径】

操作4:
配置FTP服务器相关信息

操作5:
配置FTP服务器验证和权限信息
【注意:如果想通过程序实现上传、下载功能,身份验证中的基本选项需要勾选上,后面程序需要通过这个方式使用账号和密码登录到FTP服务器】

操作6:
在同一网段的小伙伴可以通过:ftp://ftp配置的ip地址 格式访问到FTP服务器。

其他问题
问题: FTP按照流程搭建完成后,在同一网段的小伙伴却无法访问!
原因: 可能是开启了防火墙拦截,需要在防火墙放行FTP服务器。
解决: 电脑 => 控制面板 => Windows Defender 防火墙 => 允许应用通过Windows Defender 防火墙进行通信 => 勾选【FTP服务器】

2、linux下搭建(vsftpd)
其实在linux下的搭建很简单,分为三步,下载、安装、配置。
其中vsftpd的配置项比较多,所以本文重点会在vsftpd的配置里
2.1、安装启动
# 1. 安装
yum install vsftpd -y # 如果没有外网,那么可以下载离线包然后安装
# 2. 启动
systemctl start vsftpd.service
# 3.查看进程是否启动
ps -ef | grep vsftpd
# 4.查看端口号
netstat -anplut | grep vsftpd
# 5.修改配置文件
vim /etc/vsftpd/vsftpd.conf
2.2、 配置
(1)配置文件说明
| 选项 | 说明 |
|---|---|
| anonymous_enable | 设置是否允许匿名用户登录服务器 |
| local_enable | 设置是否允许本地用户登录服务器 |
| write_enable | 设置是否允许写操作 |
| local_umask | 设置本地用户创建文件的umask值 (umask 是一个权限掩码,用于控制新创建文件或目录的默认权限。) |
| local_umask | umask 077:表示文件权限为 600(即 rw-------),目录权限为 700(即 rwx------)。这意味着只有文件或目录的所有者可以读写或执行,其他用户没有任何权限。 |
| local_umask | umask 022:表示文件权限为 644(即 rw-r–r–),目录权限为 755(即 rwxr-xr-x)。这意味着所有者有读写权限,其他用户只有读权限。 |
| anon*_upload_enable* | 设置是否允许匿名用户上传文件 |
| anon*_mkdir_write_enable* | 设置是否允许匿名用户建立目录 |
| xferlog_enable | 是否激活日志功能 |
| chown_uploads | 修改匿名用户上传文件的所有者 |
| chown_username=whoever | 启用chown_uploads=YES时,指定为主用户账户,whoever表示你的ftp用户 |
| chroot*_local_user* | 设置是否将所有用户限制在其主目录 |
| chroot*_list_enable* | 设置是否启用限制用户的名单 |
| chroot*_list_file* | 设置是否限制/排除主目录下的用户名单,限制/排除有chrootlocaluser值决定 |
| allow*_writeable_choot* | 设置chroot目录的写权限 |
2.3 登录
vsftpd允许用户以3种认证模式登录到FTP服务器上。
- 匿名用户:任何人都可以直接登录服务器
- 本地用户:通过本地用户输入密码登录服务器( 在生产环境中,我们通常使用本地用户登录这种方式 )
- 虚拟用户:本身不存在、是一个虚拟出来的用户、就算黑客破解的用户信息也无法登录服务器
2.3.1 匿名登录
匿名用户登录的参数说明
| 参数 | 作用 |
|---|---|
| anonymous_enable=YES | 允许匿名用户访问 |
| anon_umask=022 | 匿名用户上传文件的umask值 |
| anon_upload_enable=YES | 允许匿名用户上传文件 |
| anon_mkdir_write_enable=YES | 允许匿名用户创建文件 |
| anon_other_write_enable=YES | 允许匿名用户修改目录名称或删除目录 |
(a) 修改配置文件:
vi /etc/vsftpd/vsftpd.conf
修改内容如下,前面是行号
12 anonymous_enable=YES
29 anon_upload_enable=YES # 允许上传文件 如果前面有# 删除最前面的 “#”
33 anon_mkdir_write_enable=YES # 允许创建文件
(b) 重启FTP服务
systemctl restart vsftpd
可能出现的问题
1 、连接ftp服务后,创建文件夹或者文件失败,可能是文件夹权限不够导致,使用如下命令更改权限
chmod 777 /var/ftp/pub #给目录所属设置为ftp
2 、查看 SELinux 的状态
sestatus

如果是启用的Enforcing状态,则需要调整相关策略
setsebool -P ftpd_full_access on
setsebool -P ftpd_use_passive_mode on
2.3.2 本地用户登录
(a) 首先在linux上创建ftp用户
# 第一步、创建ftp用户的所属目录
mkdir /usr/local/ftp
# 第二步、创建用户
useradd ftpUser -d /usr/local/ftp
# 第三步、给用户设置密码
echo "123" | passwd --stdin ftpUser
(b) 修改配置文件
vim /etc/vsftpd/vsftpd.conf
# 不允许匿名用户访问
12 anonymous_enable=NO
# 对应(a)中第一步,“创建ftp用户的所属目录”
13 local_root=/web/www/html # 添加
102 chroot_list_enable=YES # 取消注释 激活chroot功能
104 chroot_list_file=/etc/vsftpd/chroot_list # 取消注释 锁定用户在根目录中的列表文件
# 启用chroot,允许chroot限制
105 allow_writeable_chroot=YES # 添加
106 write_enable=yes # 添加
(c ) 建立/etc/vsftpd/chroot_list文件 [此处目录对应的是(b)中第“104”行配置]
vim /etc/vsftpd/chroot_list
#加入下面的用户名
ftpUser
(d) 重启FTP服务
systemctl restart vsftpd
二、Springboot整合ftp服务,实现文件上传下载功能
1、配置ftp连接设置
(a)application.yml文件配置如下
global-config:
ftp:
client:
# ftp客户端文件使用的字符集
charset: UTF-8
server:
# ftp服务器绑定ip或者域名
hostname: 192.168.3.210
# 端口
port: 21
# 连接ftp服务器的用户名
username: ftpuser2
# 密码
password: 123
# ftp的共享文件路径
workingPath: /usr/local/ftp/
# ftp服务器文件使用的字符集(用于上传包含中文名的文件和下载包含中文名的文件 - 很重要)
charset: ISO-8859-1
(b) 编写config配置文件,读取application.yml中的配置
@Data
@Component
@ConfigurationProperties("global-config")
public class GlobalConfig {
@Value("${global-config.ftp.server.hostname}")
private String ftpHost;
@Value("${global-config.ftp.server.port}")
private String ftpPort;
@Value("${global-config.ftp.server.username}")
private String ftpUsername;
@Value("${global-config.ftp.server.password}")
private String ftpPassword;
@Value("${global-config.ftp.server.workingPath}")
private String ftpWorkingPath;
@Value("${global-config.ftp.server.charset}")
private String ftpCharset;
}
2、拿到连接信息,实现文件的上传与下载
FTPUtil.java代码
package com.ztjz.share.ops.file.util;
import com.ztjz.share.ops.config.GlobalConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.net.ftp.FTPClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;
/**
* @author c先生
* @describe:
* @date 2025年02月10日 下午 05:52
*/
@Data
@Slf4j
@Component
public class FTPUtil {
@Autowired
private GlobalConfig globalConfig;
private FTPClient ftpClient;
private boolean connectServer(String ip, String port, String user, String pwd) {
ftpClient = new FTPClient();
Boolean isSuccess = false;
try {
ftpClient.setDefaultPort(Integer.parseInt(port));
ftpClient.connect(ip);
isSuccess = ftpClient.login(user, pwd);
} catch (IOException e) {
log.error("连接ftp服务器失败", e);
}
return isSuccess;
}
public boolean uploadFile(String remotePath, List<File> fileList) throws IOException {
boolean upload = true;
FileInputStream fileInputStream = null;
//connect to ftpServer
if (connectServer(globalConfig.getFtpHost(), globalConfig.getFtpPort(), globalConfig.getFtpUsername(), globalConfig.getFtpPassword())) {
try {
ftpClient.changeWorkingDirectory(remotePath);
ftpClient.setBufferSize(1024);
ftpClient.setControlEncoding("UTF-8");
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
for (File fileItem : fileList
) {
fileInputStream = new FileInputStream(fileItem);
ftpClient.storeFile(fileItem.getName(), fileInputStream);
}
} catch (IOException e) {
log.error("上传文件异常", e);
upload = false;
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
ftpClient.disconnect();
}
}
return upload;
}
public boolean uploadToFtp(String remotePath, String fileName, File file, String ftpHost, String ftpPort, String ftpUserName, String ftpPassword) throws IOException {
boolean upload = true;
FileInputStream fileInputStream = null;
//connect to ftpServer
if (connectServer(ftpHost, ftpPort, ftpUserName, ftpPassword)) {
try {
remotePath = new String(remotePath.getBytes(), globalConfig.getFtpCharset());
boolean flag = createDirectories(ftpClient, remotePath);
// int mkd = ftpClient.mkd(remotePath);
if(!flag){
throw new IOException("创建文件夹失败");
}
flag = ftpClient.changeWorkingDirectory(remotePath);
if(!flag){
throw new IOException("切换工作目录失败");
}
ftpClient.setBufferSize(1024);
ftpClient.setControlEncoding("UTF-8");
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
fileName = new String(fileName.getBytes(), globalConfig.getFtpCharset());
//上传文件 参数:上传后的文件名,输入流
upload = ftpClient.storeFile(fileName, new FileInputStream(file));
} catch (IOException e) {
log.error("上传文件异常", e);
upload = false;
} finally {
if (fileInputStream != null) {
fileInputStream.close();
}
ftpClient.disconnect();
}
}
return upload;
}
//下载
public boolean download(String remotePath, String fileName, String ftpHost, String ftpPort, String ftpUserName, String ftpPassword) throws Exception {
if (connectServer(ftpHost, ftpPort, ftpUserName, ftpPassword)) {
remotePath = new String(remotePath.getBytes(), globalConfig.getFtpCharset());
ftpClient.changeWorkingDirectory(remotePath);
ftpClient.setBufferSize(1024);
ftpClient.setControlEncoding("UTF-8");
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
String newFileName = new String(fileName.getBytes(), globalConfig.getFtpCharset());
FileOutputStream fos = new FileOutputStream(System.getProperty("java.io.tmpdir") + "/" + fileName);
;
boolean b = ftpClient.retrieveFile(newFileName, fos);
fos.close();
return b;
} else {
throw new Exception("ftp服务器连接失败");
}
}
/**
* 创建文件夹
* @param ftpClient
* @param path
* @return
* @throws IOException
*/
public boolean createDirectories(FTPClient ftpClient, String path) throws IOException {
String[] directories = path.split("/");
StringBuilder currentPath = new StringBuilder();
for (String dir : directories) {
if (dir.isEmpty()) {
continue; // 忽略空路径(如开头的 "/")
}
currentPath.append("/").append(dir);
// 检查目录是否存在
if (!ftpClient.changeWorkingDirectory(currentPath.toString())) {
// 目录不存在,尝试创建
int returnCode = ftpClient.mkd(currentPath.toString());
if (!FTPReply.isPositiveCompletion(returnCode)) {
System.err.println("创建目录失败: " + currentPath);
return false;
}
}
}
return true;
}
}
特别注意
1、在对ftp文件服务器上的文件进行操作前,需要先指定工作目录,也就是下面这几行代码不可以省略
//将FTP服务器上的当前工作目录更改为remotePath指定的路径。
ftpClient.changeWorkingDirectory(remotePath);
//设置数据传输的缓冲区大小为1024字节。影响传输效率,服务器允许的话尽可能大
ftpClient.setBufferSize(1024);
//设置控制通道的编码为UTF-8,确保服务器和客户端之间的命令和响应使用UTF-8编码,避免乱码。
ftpClient.setControlEncoding(“UTF-8”);
//设置文件传输模式为二进制模式,适用于传输图片、压缩文件等二进制文件,防止数据损坏。
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
//启用本地被动模式(PASV),客户端连接服务器的数据端口,适用于客户端位于防火墙后的情况。
ftpClient.enterLocalPassiveMode();
2、ftp对字符串的编码格式要求为ISO-8859-1格式,所以需要进行格式转换,否则会出现中文乱码的问题
String newFileName = new String(fileName.getBytes(),globalConfig.getFtpCharset());
Controller层
package com.ztjz.share.ops.file.controller;
import com.forest.support.core.result.Result;
import com.forest.support.extend.file.constant.NameFormat;
import com.forest.support.utils.UUIDGenerator;
import com.ztjz.share.ops.file.service.FtpFileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URLEncoder;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
* @author c先生
* @describe: ftp文件服务接口
* @date 2025年02月10日 下午 03:14
*/
@Tag(name = "ftp文件上传")
@RestController
@RequestMapping("/ftp/file")
@Slf4j
public class FtpFileController {
private final static SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMddHHmmss");
@Autowired
private FtpFileService ftpFileService;
@Operation(summary = "文件上传")
@PostMapping("/upload")
public Result<?> add(@RequestParam("file") MultipartFile file , @RequestParam(value = "format", required = false, defaultValue = "02") String format , @RequestParam(value = "folder", required = false) String folder) {
try {
String fileName = getFileName(file, NameFormat.fromValue(format));
List<MultipartFile> files = new ArrayList<>();
files.add(file);
ftpFileService.upload(files,folder,fileName);
return Result.ok(fileName);
} catch (IOException e) {
log.error("文件上传失败,{}",e.getMessage());
return Result.error("文件上传失败");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
@Operation(summary = "文件下载")
@GetMapping("/download")
public void download(@RequestParam String fileName, HttpServletResponse response) throws Exception {
String folder = "";
if(fileName.contains("/")){
folder = fileName.substring(0,fileName.lastIndexOf('/'));
fileName = fileName.substring(fileName.lastIndexOf('/')+1);
}
boolean flag = ftpFileService.download(folder,fileName);
if(flag){
wapperResponse(fileName,response);
}
}
private void wapperResponse(String fileName,HttpServletResponse response){
FileInputStream in = null;
try {
response.setContentType("application/octet-stream;charset=utf-8");
response.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.addHeader("Content-type", "application/octet-stream");
response.setCharacterEncoding("UTF-8");
//将字节从InputStream复制到OutputStream 。
in = new FileInputStream(System.getProperty("java.io.tmpdir")+"/"+fileName);
IOUtils.copy(in, response.getOutputStream());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 为文件创建名称
* @param multipartFile 上传文件
* @return 返回文件名
*/
private String getFileName(MultipartFile multipartFile, NameFormat nameFormat ){
String newFileName = null;
String originalFilename = multipartFile.getOriginalFilename();
String fileName;
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
switch (nameFormat){
case UUID:
fileName = UUIDGenerator.randomUUID();
newFileName = fileName + extension;
break;
case ORIGINAL:
newFileName = originalFilename;
break;
case TIMESTAMP:
fileName = originalFilename.substring(0, originalFilename.lastIndexOf("."));
newFileName = fileName + "_" + DATE_FORMAT.format(new Date()) + extension;
break;
}
return newFileName;
}
}
service层
package com.ztjz.share.ops.file.service.impl;
import com.ztjz.share.ops.config.GlobalConfig;
import com.ztjz.share.ops.file.service.FtpFileService;
import com.ztjz.share.ops.file.util.FTPUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
/**
* @author c先生
* @describe:
* @date 2025年02月10日 下午 03:14
*/
@Service
@Slf4j
public class FtpFileServiceImpl implements FtpFileService {
@Autowired
private FTPUtil ftpUtil;
@Autowired
private GlobalConfig globalConfig;
@Override
public String upload(List<MultipartFile> fileList,String folder,String fileName) throws Exception {
if(ObjectUtils.isNotEmpty(fileList)){
MultipartFile multipartFile = fileList.get(0);
//3、将MultipartFile转化为File
File file = this.multipartFileToFile(multipartFile);
String remotePath = "/";
if(StringUtils.isNotBlank(folder)){
remotePath = folder;
if(!folder.startsWith("/")){
remotePath = "/"+folder;
}
if(!folder.endsWith("/")){
remotePath = folder + "/";
}
if(!folder.endsWith("/")&&!folder.startsWith("/")){
remotePath = "/" + folder + "/";
}
}
//4、上传至ftp服务器
if (ftpUtil.uploadToFtp(remotePath, fileName, file)) {
System.out.println("上传至ftp服务器!");
} else {
System.out.println("上传至ftp服务器失败!");
}
return fileName;
}else {
throw new Exception("文件为空,上传失败");
}
}
@Override
public boolean download(String folder,String objectName) throws Exception {
String remotePath = "/";
if(StringUtils.isNotBlank(folder)){
remotePath = folder;
if(!folder.startsWith("/")){
remotePath = "/"+folder;
}
if(!folder.endsWith("/")){
remotePath = folder + "/";
}
if(!folder.endsWith("/")&&!folder.startsWith("/")){
remotePath = "/" + folder + "/";
}
}
return ftpUtil.download(remotePath,objectName);
}
public File multipartFileToFile(MultipartFile file) throws Exception {
File toFile = null;
if (file.equals("") || file.getSize() <= 0) {
file = null;
} else {
InputStream ins = null;
ins = file.getInputStream();
toFile = new File(file.getOriginalFilename());
inputStreamToFile(ins, toFile);
ins.close();
}
return toFile;
}
//获取流文件
private static void inputStreamToFile(InputStream ins, File file) {
try {
OutputStream os = new FileOutputStream(file);
int bytesRead = 0;
byte[] buffer = new byte[8192];
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
ins.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
1884

被折叠的 条评论
为什么被折叠?



