本次搭建文件服务器选用的是Linux系统,CentOS版本
这里也简单介绍下本次安装会用到的Linux操作命令。
rpm -qa :rpm是redhat package manager的简写,是Red Hat Linux发行版专门用来管理Linux各项套件的程序,由于它遵循 GPL 规则且功能强大方便,因而广受欢迎。逐渐受到其他发行版的采用。-q命令指使用询问模式,当遇到任何问题时,rpm指令会先询问用户。-a命令指查询所有套件。
grep :用于查找文件里符合条件的字符串,查找内容包含指定范本样式的文件,如果发现某文件的内容符合所指定的范本样式,预设grep指令会把含有范本样式的那一列显示出来。
| :该符号代表管道流,用法是:command1 | command2,表示将第一个命令的标准输出作为第二个命令的标准输入
ps :ps命令用于显示当前进程 (process) 的状态。-aux命令指显示所有包含其他使用者的行程。
yum : Yellow dog Updater, Modified,是一个在Fedora和RedHat以及SUSE中的Shell前端软件包管理器。基于rmp包管理器,能从指定的服务器下载rpm包,处理好对应的依赖关系,不用我们自己查找对应的依赖,一一下载。-y命令指当安装过程中有提示全部选择为"yes"
rpm -qa | grep vsftpd
输入上述命令,查询我们的Linux系统是否已经安装vsftpd,如果什么也没有,则说明我们还未安装vsftpd,已经安装了,会显示对应的vsftpd版本信息
yum -y install vsftpd
输入上述命令,安装vsftpd,安装成功会提示以下信息
service vsftpd start
输入上述命令,启动vsftpd服务
vsftpd对应的停止命令
service vsftpd stop
vsftpd对应的重启命令
service vsftpd restart
ps aux|grep vsftpd
输入上述命令,查询我们的系统进程中是否存在vsftpd服务
# 创建一个ftp文件夹,用于文件的上传和下载,位置自行定义
cd /usr/local
mkdir ftpFile
cd ftpFile
mkdir risk
# 在Linux上创建一个用户,专门用于ftp文件上传和下载
useradd spirit
# 限制用户spirit只能通过FTP访问服务器,而不能直接登录服务器
usermod -s /sbin/nologin spirit
# 将上面创建的文件夹授权给spirit用户
usermod -d /usr/local/ftpFile/risk spirit
# 再将risk文件夹的用户组更改为spirit
chown spirit:spirit risk
# 并且将risk文件夹的权限修改为用户组内可读可写可执行
chmod 775 risk
# 最后重置spirit的密码,重复两次输入后完成
passwd spirit
这里可能会遇到的问题:
我们创建用户的时候,使用了nologin的shell,但是ftp用户无法登录,这里把PAM模块对vsftp登录的过度验证注释掉就可以了,截图中auth required pam_shells.so这一行
risk文件夹的用户组更改成功后,是下面的状态
vsftpd的主配置文件 vsftpd.conf 默认在 /etc/vsftpd 文件夹下,下面的配置文件是限定上述创建的spirit用户采用被动模式访问的
# 关闭匿名访问,不允许未授权用户访问ftp服务
anonymous_enable=NO
# 开启上传下载文件日志
xferlog_enable=YES
# 上传下载文件日志所在位置
xferlog_file=/var/log/xferlog
...
pam_service_name=vsftpd
# 是否允许ftpusers文件中的用户登录FTP服务器,默认为NO
# 若此项设为YES,则user_list文件中的用户允许登录FTP服务器
# 而如果同时设置了userlist_deny=YES,则user_list文件中的用户将不允许登录FTP服务器,甚至连输入密码提示信息都没有
userlist_enable=YES
# 设置是否阻扯user_list文件中的用户登录FTP服务器,默认为YES
userlist_deny=NO
# user_list文件所在位置
userlist_file=/etc/vsftpd/user_list
tcp_wrappers=YES
allow_writeable_chroot=YES
# ftp默认文件路径
local_root=/usr/local/ftpFile/risk
# 是否将所有用户限制在主目录,YES为启用,NO禁用.(该项默认值是NO,即在安装vsftpd后不做配置的话,ftp用户是可以向上切换到目录之外的)
chroot_local_user=YES
# 是否启动限制用户的名单,YES为启用,NO禁用(包括注释掉也为禁用)
chroot_list_enable=YES
# chroot_list文件所在位置
chroot_list_file=/etc/vsftpd/chroot_list
# 是否开启被动模式
pasv_enable=YES
# 被动模式端口范围
pasv_min_port=40000
pasv_max_port=41000
# 是否开启ftp操作日志
dual_log_enable=YES
# ftp操作日志所在位置
vsftpd_log_file=/var/log/vsftpd.log
/etc/vsftpd文件夹下默认没有chroot_list文件的,我们手动创建一个
cd /etc/vsftpd
# 创建文件chroot_list
vim chroot_list
# 添加用户:spirit,保存退出,再在user_list文件中也加入spirt用户,一个用户一行
vim /etc/selinux/config
# 修改SELINUX=disabled,也可以用下面的命令执行
setenforce 0
保存完配置文件后,重启vsftpd服务
service vsftpd restart
可能会遇到的问题:
1、chroot_local_user与chroot_list_enable的区别,可以参考下面这篇博客
https://blog.csdn.net/bluishglc/article/details/42398811
2、VSFTPD一直提示“用户身份验证失败”:搭建好之后,当日都正常,自从第二天服务器被重启之后,无论是xftp还是java客户端登录就一直提示“用户身份验证失败”,无论是匿名还是使用root账户或者ftpadmin账户均如此
这个问题发生在最新的这是由于下面的更新造成的:
- Add stronger checks for the configuration error of running with a writeable root directory inside a chroot().
This may bite people who carelessly turned on chroot_local_user but such is life.
从2.3.5之后,vsftpd增强了安全检查,如果用户被限定在了其主目录下,则该用户的主目录不能再具有写权限了!如果检查发现还有写权限,就会报该错误。
要修复这个错误,可以用命令chmod a-w /home/user去除用户主目录的写权限,注意把目录替换成你自己的。或者你可以在vsftpd的配置文件中增加下列两项中的一项:
allow_writeable_chroot=YES
如果启用了防火墙,需要开放上述配置的端口范围
vim /etc/sysconfig/iptables
# 添加vsftpd的端口配置
-A INPUT -p TCP --dport 40000:41000 -j ACCEPT
-A OUTPUT -p TCP --sport 40000:41000 -j ACCEPT
配置完成后,重启防火墙
firewall-cmd --reload
在浏览器输入ftp:// + ip就能访问到我们的ftp服务器,输入前面创建的帐号密码,就能查看内部文件
package com.mine.risk.util;
import org.apache.commons.net.ftp.FTP;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPReply;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.UnknownHostException;
/**
* 文件传输辅助工具类
* @author spirit
* @version 1.0
* @date 2019-06-26 17:05
*/
public class FtpUtil {
private FTPClient ftp;
public FtpUtil() {
ftp = new FTPClient();
//解决上传文件时文件名乱码
ftp.setControlEncoding("UTF-8");
}
public FtpUtil(String controlEncoding) {
ftp = new FTPClient();
// 解决上传文件时文件名乱码
ftp.setControlEncoding(controlEncoding);
}
/**
* Connect to FTP server.
* @param host FTP server address or name
* @param port FTP server port
* @param user user name
* @param password user password
* @throws IOException on I/O errors
*/
public void connect(String host, int port, String user, String password) throws IOException {
// Connect to server.
try {
ftp.connect(host, port);
} catch (UnknownHostException ex) {
throw new IOException("Can't find FTP server '" + host + "'");
}
// Check response after connection attempt.
int reply = ftp.getReplyCode();
if (!FTPReply.isPositiveCompletion(reply)) {
disconnect();
throw new IOException("Can't connect to server '" + host + "'");
}
if ("".equals(user)) {
user = "anonymous";
}
// Login.
if (!ftp.login(user, password)) {
disconnect();
throw new IOException("Can't login to server '" + host + "'");
}
// Set data transfer mode.
ftp.setFileType(FTP.BINARY_FILE_TYPE);
// Use passive mode to pass firewalls.
ftp.enterLocalPassiveMode();
}
/**
* Disconnect from the FTP server
*/
public void disconnect(){
if (ftp.isConnected()) {
try {
ftp.logout();
ftp.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 创建文件夹并切换到改目录下
* @param basePath 基础目录路径
* @param filePath 待创建文件目录路径
* @throws Exception on errors
*/
public void makeDir(String basePath, String filePath) throws Exception{
try {
//切换到上传目录
if (!ftp.changeWorkingDirectory(basePath+filePath)) {
//如果目录不存在创建目录
String[] dirs = filePath.split("/");
String tempPath = basePath;
for (String dir : dirs) {
if (null == dir || "".equals(dir)) {
continue;
}
tempPath += "/" + dir;
if (!ftp.changeWorkingDirectory(tempPath)) {
if (!ftp.makeDirectory(tempPath)) {
throw new Exception();
} else {
ftp.changeWorkingDirectory(tempPath);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* Upload the file to the FTP server.
* @param ftpFileName server file name (with absolute path)
* @param input local file to upload
*/
public void upload(String ftpFileName, InputStream input) throws Exception{
try {
if (!ftp.storeFile(ftpFileName, input)) {
throw new Exception();
}
} finally {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* Get file from ftp server into given output stream
* @param ftpFileName file name on ftp server
* @param out OutputStream
* @throws IOException on I/O errors
*/
public void download(String ftpFileName, OutputStream out) throws IOException {
try {
// Get file info.
FTPFile[] fileInfoArray = ftp.listFiles(ftpFileName);
if (fileInfoArray == null || fileInfoArray.length == 0) {
throw new FileNotFoundException("File '" + ftpFileName + "' was not found on FTP server.");
}
// Check file size.
FTPFile fileInfo = fileInfoArray[0];
long size = fileInfo.getSize();
if (size > Integer.MAX_VALUE) {
throw new IOException("File '" + ftpFileName + "' is too large.");
}
// Download file.
if (!ftp.retrieveFile(ftpFileName, out)) {
throw new IOException("Error loading file '" + ftpFileName + "' from FTP server. Check FTP permissions and path.");
}
out.flush();
} finally {
if (out != null) {
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* Delete the file from the FTP server.
* @param ftpFileName server file name (with absolute path)
* @throws IOException on I/O errors
*/
public void deleteFile(String ftpFileName) throws IOException {
if (!ftp.deleteFile(ftpFileName)) {
throw new IOException("Can't remove file '" + ftpFileName + "' from FTP server.");
}
}
}
这里只提供一下大致思路,具体的代码就得自己根据业务来写了
package com.mine.risk.controller;
import com.mine.risk.bean.entity.SysFileEntity;
import com.mine.risk.bean.enumeration.ResponseEnum;
import com.mine.risk.bean.pojo.Message;
import com.mine.risk.bean.vo.SysFileVo;
import com.mine.risk.service.SysFileService;
import com.mine.risk.util.FtpUtil;
import com.mine.risk.util.ResponseUtil;
import com.mine.risk.util.SnowFlakeIdWorker;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* 文件上传控制器
* @author spirit
* @version 1.0
* @date 2019-06-26 11:07
*/
@Controller
@RequestMapping("file")
public class FileController {
/** 主机 */
@Value("${ftp.address}")
private String host;
/** 端口 */
@Value("${ftp.port}")
private int port;
/** ftp用户名 */
@Value("${ftp.username}")
private String userName;
/** ftp用户密码 */
@Value("${ftp.password}")
private String passWord;
/** 文件在服务器端保存的主目录 */
@Value("${ftp.basePath}")
private String basePath;
/** 访问图片时的基础url */
@Value("${file.base.url}")
private String baseUrl;
@Resource
private SnowFlakeIdWorker snowFlakeIdWorker;
@Resource
private SysFileService sysFileService;
@RequestMapping(value = "upload", method = RequestMethod.POST)
@ResponseBody
public Message fileUpload(@RequestParam(value="file") MultipartFile file){
Message message;
try {
// 1、获取原始文件名
String oldName = file.getOriginalFilename();
// 2、获取文件后缀名
String extraFileName = oldName.substring(oldName.lastIndexOf("."));
// 3、生成新的文件名
String newName = snowFlakeIdWorker.getNextId() + extraFileName;
// 4、生成文件在服务器端存储的子目录
String filePath = new SimpleDateFormat("yyyy/MM/dd/").format(new Date());
StringBuilder tempPath = new StringBuilder();
String[] dirs = filePath.split("/");
for (String string : dirs) {
tempPath.append(string);
}
// 6、获取上传的io流
InputStream input = file.getInputStream();
// 7、调用FtpUtil工具类进行上传,获取返回结果
FtpUtil ftpUtil = new FtpUtil("UTF-8");
try {
ftpUtil.connect(host, port, userName, passWord);
ftpUtil.makeDir(basePath, tempPath.toString());
ftpUtil.upload(newName, input);
// 上传成功后返回数据
SysFileVo sysFileVo = new SysFileVo();
sysFileVo.setOriginalName(oldName);
sysFileVo.setNewName(newName);
sysFileVo.setFileType(extraFileName.substring(1));
sysFileVo.setFileSize(file.getSize());
sysFileVo.setRootDirectory(tempPath.toString());
sysFileService.saveFile(sysFileVo);
return ResponseUtil.success(sysFileVo);
} catch (Exception e) {
e.printStackTrace();
return ResponseUtil.info(ResponseEnum.BUSINESS_ERROR);
} finally {
// 用完了之后关闭连接
ftpUtil.disconnect();
}
} catch (IOException e) {
e.printStackTrace();
message = ResponseUtil.info(ResponseEnum.SERVICE_ERROR);
}
return message;
}
@RequestMapping(value = "download")
@ResponseBody
public void downloadFile(String fileName, HttpServletResponse response){
if(Objects.nonNull(fileName) && !StringUtils.isEmpty(fileName)){
SysFileEntity fileEntity = sysFileService.findByNewName(fileName);
if(Objects.nonNull(fileEntity)){
FtpUtil ftpUtil = new FtpUtil("UTF-8");
try {
// 清空response
response.reset();
// 设置response的Header
response.addHeader("Content-Disposition", "attachment;filename="+new String( fileEntity.getOriginalName().getBytes("gb2312"), "ISO8859-1" ) );
response.setContentType("application/octet-stream");
response.setContentLengthLong(fileEntity.getFileSize());
// 开始下载
ftpUtil.connect(host, port, userName, passWord);
BufferedOutputStream bus = new BufferedOutputStream(response.getOutputStream());
ftpUtil.download(basePath+"/"+fileEntity.getRootDirectory()+"/"+fileEntity.getNewName(), bus);
} catch (Exception e) {
e.printStackTrace();
} finally {
// 用完了之后关闭连接
ftpUtil.disconnect();
}
}
}
}
}
110 Restart marker reply. 重新启动标记答复。
120 Service ready in nnn minutes. 服务已就绪,在nnn分钟后开始。
125 Data connection already open; transfer starting. 数据连接已打开,正在开始传输。
150 File status okay; about to open data connection. 文件状态正常,准备打开数据连接。
200 Command okay. 命令确定。
202 Command not implemented, superfluous at this site. 未执行命令,站点上的命令过多
211 System status, or system help reply. 系统状态,或系统帮助答复。
212 Directory status. 目录状态。
213 File status. 文件状态。
214 Help message. 帮助消息。
215 NAME system type. NAME系统类型,其中,NAME是Assigned Numbers文档中所列的正式系统名称。
220 Service ready for new user. 服务就绪,可以执行新用户的请求。
221 Service closing control connection. 服务关闭控制连接。如果适当,请注销。
225 Data connection open; no transfer in progress. 数据连接打开,没有进行中的传输。
226 Closing data connection. 关闭数据连接。请求的文件操作已成功(例如,传输文件或放弃文件)。
227 Entering Passive Mode. 进入被动模式(h1,h2,h3,h4,p1,p2)。
228 Entering Long Passive Mode.
229 Extended Passive Mode Entered.
230 User logged in, proceed. 用户已登录,继续进行。
250 Requested file action okay, completed. 请求的文件操作正确,已完成。
257 "PATHNAME" created. 已创建“PATHNAME”。
331 User name okay, need password. 用户名正确,需要密码。
332 Need account for login. 需要登录帐户。
350 Requested file action pending further information. 请求的文件操作正在等待进一步的信息。
421 Service not available, closing control connection.
服务不可用,正在关闭控制连接。如果服务确定它必须关闭,将向任何命令发送这一应答。
425 Can't open data connection. 无法打开数据连接。
426 Connection closed; transfer aborted. 连接被关闭,数据传输中断。
450 Requested file action not taken. 未执行请求的文件操作。文件不可用。
451 Requested action aborted. Local error in processing.请求的操作异常终止:正在处理本地错误。
452 Requested action not taken. 未执行请求的操作。系统存储空间不够。
500 Syntax error, command unrecognized. 语法错误,命令无法识别。这可能包括诸如命令行太长之类的错误。
501 Syntax error in parameters or arguments. 在参数中有语法错误。
502 Command not implemented. 未执行命令。
503 Bad sequence of commands. 错误的命令序列。
504 Command not implemented for that parameter. 未执行该参数的命令。
521 Supported address families are
522 Protocol not supported.
530 Not logged in. 未登录。
532 Need account for storing files. 存储文件需要帐户。
550 Requested action not taken. 未执行请求的操作。文件不可用。
551 Requested action aborted. Page type unknown. 请求的操作异常终止:未知的页面类型。
552 Requested file action aborted. 请求的文件操作异常终止:超出存储分配(对于当前目录或数据集)。
553 Requested action not taken. 未执行请求的操作。不允许的文件名。
554 Requested action not taken: invalid REST parameter.
555 Requested action not taken: type or struct mismatch.