最近做项目需要文件上传下载的功能模块,但是考虑客户的因素没有选择阿里云OSS文件服务
如果条件允许的情况下选择云存储的OSS是比较合适的,下面介绍如何自己搭建一个FTP文件服务器:
根据当时项目环境:docker部署spring cloud项目,Linux系统环境
安装vsftpd
Linux执行命令:yum -y install vsftpd
安装完后,有/etc/vsftpd/vsftpd.conf 文件,是vsftp的配置文件;
查看是否安装成功
执行命令:rpm -qa| grep vsftpd
默认配置文件
文件路径:/etc/vsftpd/vsftpd.conf
创建虚拟用户
# 在根目录或者用户目录下创建ftp文件夹,这里选择在根目录(目录为Nginx目录)
mkdir /home/aeccspringcloud/website/nginx/html/ftpfile
# 添加用户
useradd ftpuser -d /home/aeccspringcloud/website/nginx/html/ftpfile -s /sbin/nologin
# 修改ftpfile文件夹权限
chown -R ftpuser.ftpuser /home/aeccspringcloud/website/nginx/html/ftpfile
# 重设ftpuser密码
passwd ftpuser
密码为:#设置你的密码
配置
执行命令:cd /etc/vsftpd
# 创建文件chroot_list
执行命令:vim chroot_list
# 添加内容:ftpuser,保存退出执行:wq
执行命令:vim /etc/selinux/config
# 修改SELINUX=disabled
执行setenforce 0表示关闭selinux防火墙
setenforce 0
修改主配置
修改主配置文件:vim /etc/vsftpd/vsftpd.conf
1. 查找文本信息找到如下注释:
取消ftpd_banner注释,新增加三行配置
2. 继续查找choose_list,取消如下两行配置的注释
chroot_list_enable=YES
# (default follows)
chroot_list_file=/etc/vsftpd/chroot_list
3. 查找anon,将如下的配置项值改为NO
4. 在最底端添加被动传输的端口,最大和最小端口值,在ftp上传文件传输时需要使用的,虽然采用默认的端口范围
也可以,但是防火墙的设置就不能太严格,所以线上环境为了安全考虑建议加上端口配置,方便防火墙配置;
防火墙配置
vim /etc/sysconfig/iptables
# 添加vsftpd的端口配置
-A INPUT -p TCP --dport 61001:62000 -j ACCEPT
-A OUTPUT -p TCP --sport 61001:62000 -j ACCEPT
-A INPUT -p TCP --dport 20 -j ACCEPT
-A OUTPUT -p TCP --sport 20 -j ACCEPT
-A INPUT -p TCP --dport 21 -j ACCEPT
-A OUTPUT -p TCP --sport 21 -j ACCEPT
启动服务
重启防火墙:service iptables restart
启动vsftpd:systemctl start vsftpd.service
在/home/aeccspringcloud/ftpfile目录上传一些测试文件及目录,方便验证查看;
查看服务状态
执行命令:systemctl status vsftpd
测试客户端连接状态(只能访问本目录)
配置nginx服务
注:当前的alias标红路径为docker映射路径,在非docker环境下为当前系统物理路径
# 文件服务器代理配置
location /aeccfile {
alias /usr/share/nginx/html/ftpfile;
autoindex on;
}
解决nginx访问404问题
使用Nginx做图片服务器时候,配置之后图片访问一直是 404问题解决
总结:
root响应的路径:配置的路径(root指向的路径)+ 完整访问路径(location的路径)+ 静态文件
alias响应的路径:配置路径+静态文件(去除location中配置的路径)
一般情况下,在location /中配置root,在location /other中配置alias
Java项目中通过FTP连接文件服务器实现上传下载
该项目使用spring boot,所以ftp的相关配置连接写在了配置文件中
#静态资源对外暴露的访问路径
file:
relativeUrl: /aeccfile/
ftp:
ipAddress: # ftp服务器地址
port: 21 # 默认端口
username: ftpuser # 用户名
password: # 文件服务设置的密码
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author: Dbw
* @date: 2019年8月12日 下午6:36:46
*/
@Component
public class ConfigBeanValue {
@Value("${file.relativeUrl}")
public String relativeUrl;
@Value("${ftp.ipAddress}")
public String ipAddress;
@Value("${ftp.port}")
public Integer port;
@Value("${ftp.username}")
public String username;
@Value("${ftp.password}")
public String password;
}
文件上传
/**
* @author: Dbw
* @date: 2019年8月20日 下午6:16:53
* @description: ftp文件上传下载工具类
*/
public class FtpUploadUtils {
public static FTPClient getFTPClient(ConfigBeanValue configBeanValue) throws SocketException, IOException {
FTPClient ftpClient = new FTPClient();
ftpClient.setControlEncoding("utf-8");
//设置传输超时时间为60秒
ftpClient.setDataTimeout(60000);
//连接超时为60秒
ftpClient.setConnectTimeout(60000);
try {
String ftpHost = configBeanValue.ipAddress;
Integer ftpPort = configBeanValue.port;
String ftpUserName = configBeanValue.username;
String ftpPassword = configBeanValue.password;
ftpClient.connect(ftpHost, ftpPort);// 连接FTP服务器
ftpClient.login(ftpUserName, ftpPassword);// 登陆FTP服务器
if (!FTPReply.isPositiveCompletion(ftpClient.getReplyCode())) {
ftpClient.disconnect();
}
} catch (SocketException e) {
e.printStackTrace();
throw new SocketException("FTP的IP地址可能错误,请正确配置。");
} catch (IOException e) {
e.printStackTrace();
throw new SocketException("FTP的端口错误,请正确配置。");
}
return ftpClient;
}
/**
* 上传文件
* @param realPathName ftp服务保存地址
* @param uuidName 上传到ftp的文件名
* @param inputStream 输入文件流
* @return
*/
public static boolean uploadFile(FTPClient ftpClient,String realPathName, String uuidName, InputStream inputStream) throws Exception{
boolean flag = false;
try{
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.setFileTransferMode(FTPClient.BINARY_FILE_TYPE);
flag = createDirecroty(ftpClient, realPathName);
flag = ftpClient.makeDirectory(realPathName);
flag = ftpClient.changeWorkingDirectory(realPathName);
flag = ftpClient.storeFile(uuidName, inputStream);
inputStream.close();
}catch (Exception e) {
e.printStackTrace();
}
return flag;
}
//改变目录路径
public static boolean changeWorkingDirectory(FTPClient ftpClient, String directory) throws Exception {
boolean flag = true;
try {
flag = ftpClient.changeWorkingDirectory(directory);
} catch (IOException ioe) {
ioe.printStackTrace();
throw new Exception("改变目录路径失败");
}
return flag;
}
//创建多层目录文件,如果有ftp服务器已存在该文件,则不创建,如果无,则创建
public static boolean createDirecroty(FTPClient ftpClient, String remote) throws Exception {
boolean success = true;
String directory = remote + "/";
// 如果远程目录不存在,则递归创建远程服务器目录
if (!directory.equalsIgnoreCase("/") && !changeWorkingDirectory(ftpClient, new String(directory))) {
int start = 0;
int end = 0;
if (directory.startsWith("/")) {
start = 1;
} else {
start = 0;
}
end = directory.indexOf("/", start);
String path = "";
String paths = "";
while (true) {
String subDirectory = new String(remote.substring(start, end).getBytes("GBK"), "iso-8859-1");
path = path + "/" + subDirectory;
//false表示当前文件夹下没有文件
if (!existFile(ftpClient, path)) {
if (makeDirectory(ftpClient, subDirectory)) {
changeWorkingDirectory(ftpClient, subDirectory);
} else {
changeWorkingDirectory(ftpClient, subDirectory);
}
} else {
changeWorkingDirectory(ftpClient, subDirectory);
}
paths = paths + "/" + subDirectory;
start = end + 1;
end = directory.indexOf("/", start);
// 检查所有目录是否创建完毕
if (end <= start) {
break;
}
}
}
return success;
}
//判断ftp服务器文件是否存在
public static boolean existFile(FTPClient ftpClient, String path) throws IOException {
boolean flag = false;
FTPFile[] ftpFileArr = ftpClient.listFiles(path);
if (ftpFileArr.length > 0) {
flag = true;
}
return flag;
}
//创建目录
public static boolean makeDirectory(FTPClient ftpClient, String dir) {
boolean flag = true;
try {
flag = ftpClient.makeDirectory(dir);
} catch (Exception e) {
e.printStackTrace();
}
return flag;
}
/**
* 文件下载
* @param ftpClient
* @param originname
* @param path
* @return
*/
public static byte[] download(FTPClient ftpClient, String originname, String path) {
InputStream inputStream = null;
byte[] bytes = null;
try {
ftpClient.changeWorkingDirectory(path);
FTPFile[] ftpFiles = ftpClient.listFiles();
for (FTPFile ftpFile : ftpFiles) {
//判断文件是否存在
if(ftpFile.getName().equals(originname)) {
//返回文件对象
String filePath = path + originname;
// 每次数据连接之前,ftp client告诉ftp server开通一个端口来传输数据,ftp server可能每次开启不同的端口来传输数据,
// 但是在Linux上,由于安全限制,可能某些端口没有开启,所以就出现阻塞。
ftpClient.enterLocalPassiveMode();
inputStream = ftpClient.retrieveFileStream(filePath);
bytes = IOUtils.toByteArray(inputStream);
}
}
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
return bytes;
}
}
然后通过文件上传下载的接口调用,实现文件上传下载即可,以下记录下载接口调用:
/**
* 文件下载
*/
public R download(XmWd xmWd, HttpServletRequest request, HttpServletResponse response) throws IOException {
String path = xmWd.getPath();
String originname = xmWd.getOriginname();
String fileName = xmWd.getName();
//获取下载的文件对象
FTPClient ftpClient = FtpUploadUtils.getFTPClient(configBeanValue);
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.setFileTransferMode(FTPClient.BINARY_FILE_TYPE);
byte[] buffer = FtpUploadUtils.download(ftpClient, originname, path);
if(buffer != null && buffer.length > 0) {
response.setContentType("application/octet-stream");// 设置强制下载不打开
response.setHeader("Content-Disposition", "attachment;fileName=" + URLEncoder.encode(fileName, "utf-8"));
BufferedInputStream bis = null;
InputStream inputStream = null;
try {
inputStream = new ByteArrayInputStream(buffer);
bis = new BufferedInputStream(inputStream);
OutputStream os = response.getOutputStream();
int i = bis.read(buffer);
while (i != -1) {
os.write(buffer, 0, i);
i = bis.read(buffer);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (bis != null) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return R.ok("下载成功");
} else {
return R.ok("下载失败").put("code", 500);
}
}
通过浏览器可下载;
问题整理:
1. 实际应用中会出现文件上传到ftp服务器中文件size=0的情况,造成的原因可能时设置主动模式/被动模式的原因
ftpClient.setFileType(FTPClient.BINARY_FILE_TYPE);
ftpClient.setFileTransferMode(FTPClient.BINARY_FILE_TYPE);
ftpClient.enterLocalPassiveMode();
flag = createDirecroty(ftpClient, realPathName);
flag = ftpClient.makeDirectory(realPathName);
flag = ftpClient.changeWorkingDirectory(realPathName);
flag = ftpClient.storeFile(uuidName, inputStream);
添加ftpClient.enterLocalPassiveMode();//被动模式配置
2. 解决用户名密码正确,530 login incorrect问题
首先根据以上配置启动完ftp服务后,测试发现输入正确的用户名密码仍然连接不上
经过一番寻找问题解决办法,发现与etc/pam.d/vsftpd的pam验证有关
可能导致530错误的有
auth required pam_listfile.so item=user sense=deny file=/etc/ftpusers onerr=succeed
和
auth required pam_shells.so
/etc/ftpusers
auth required pam_listfile.so item=user sense=deny file=/etc/ftpusers onerr=succeed
该配置项的含义是 /etc/ftpusers 中的用户禁止登陆,如果文件不存在在默认所有用户均允许登录. 所以确保用户没在这个文件内。
pam_shells.so
auth required pam_shells.so 配置项的含义为仅允许用户的shell为 /etc/shells
文件内的shell命令时,才能够成功
cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/dash
/bin/bash
/bin/rbash
而创建ftp用户时,为了禁止ssh登录,一般多为/bin/false 、/usr/sbin/nologin 等,显然不是一个有效的bash,也就无法登录了
解决方案:
1、查看/etc/ftpusers ,确保账号没有在这个文件内。
2、修改/etc/pam.d/vsftpd
将auth required pam_shells.so修改为->auth required pam_nologin.so 即可
#我这里将该行注释掉了
3、重启vsftpd