系统:Linux Centos 7
yum install -y vsftpd
添加 test 用户,并禁止ssh登录
useradd -d /home/vsftpd/test -g ftp -s /sbin/nologin test
更改密码
passwd test
修改 /etc/vsftpd/vsftpd.conf
chroot_local_user=YES
#chroot_list_enable=YES
# (default follows)
#chroot_list_file=/etc/vsftpd/chroot_list
#
# You may activate the "-R" option to the builtin ls. This is disabled by
# default to avoid remote users being able to cause excessive I/O on large
# sites. However, some broken FTP clients such as "ncftp" and "mirror" assume
# the presence of the "-R" option, so there is a strong case for enabling it.
#ls_recurse_enable=YES
#
# When "listen" directive is enabled, vsftpd runs in standalone mode and
# listens on IPv4 sockets. This directive cannot be used in conjunction
# with the listen_ipv6 directive.
listen=NO
#
# This directive enables listening on IPv6 sockets. By default, listening
# on the IPv6 "any" address (::) will accept connections from both IPv6
# and IPv4 clients. It is not necessary to listen on *both* IPv4 and IPv6
# sockets. If you want that (perhaps because you want to listen on specific
# addresses) then you must run two copies of vsftpd with two configuration
# files.
# Make sure, that one of the listen options is commented !!
listen_ipv6=YES
pam_service_name=vsftpd
userlist_enable=YES
tcp_wrappers=YES
chroot_list_enable=YES
chroot_list_file=/etc/vsftpd/chroot_list
创建文件 /etc/vsftpd/chroot_list, 内容为test
修改 /etc/pam.d/vsftpd 文件
#%PAM-1.0
session optional pam_keyinit.so force revoke
auth required pam_listfile.so item=user sense=deny file=/etc/vsftpd/ftpusers onerr=succeed
auth required pam_nologin.so
auth include password-auth
account include password-auth
session required pam_loginuid.so
session include password-auth
将 auth required pam_shells.so
修改为->auth required pam_nologin.so
或者将 auth required pam_shells.so注释
然后重启
文章地址:https://blog.csdn.net/wqh0830/article/details/87795709
不管哪种模式,都必须通过21这个端口建立起FTP的管道连接,通过这个通道发送命令
发起连接的 是客户端,所以就是 客户端的21端口可以是关闭状态的,但是服务端的21端口一定要开着。
通过21端口可以创建文件夹,但不能创建文件
遇到的问题是 可以创建文件,但是不能写入文件
FTP:文件传输协议,20 、21端口
两个TCP连接,一个控制层面的连接,一个数据层面的连接
被动模式(第二信道的TCP连接时由客户端主动发起的)
客户端主动找服务器建立第一信道,目的端口21
服务器给客户端发一个数据包(PASV命令),IP地址,x,y ,意思就是告诉客户端,这是的IP地址,我开放的端口为 x * 256 + y
客户端要主动发起第二信道的TCP连接,通过上一步得到的IP地址,和 计算出来的端口号
主动模式
第一个TCP连接 【第一信道】
客户端主动发起,和FTP服务器建立通信,目的端口 21
发送PORT命令, 客户端告诉服务器,我在 IP地址, x, y, 端口为 x * 256 + y
建立第二信道,服务端主动找客户端
1、通过tcp的21端口建立通道
2、客户端通过此通道发起PORT命令,$\color{red} {并产生一个随机非特殊的端口号N ( 1023 < N < 65536) 给到FTP服务器}$
3、此时客户端监听 N + 1 端口 (N + 1 >= 1025, 不一定是N端口 + 1),同时通过21的通道发送命令通知FTP服务器,客户端通过此端口接收数据传输。
4、FTP服务器接收到上一步的响应后,通过自己的数据源端口20,去连接远程的客户端的 N + 1 端口(此时是FTP服务端主动发起的一个端口连接)
5、如果此时客户端的防火墙开着,并且只开放了21端口,则会造成连接成功, 而无法上传文件,会导致上一步的出现数据端口连接失败
6、如果端口开放正常, FTP客户端会接收到服务端响应并返回信息,则建立起了数据连接通道。
总结:主动模式,是FTP服务器主动连接客户端的数据端口,服务端可以只开放端口21,但是客户端还是保证端口要开放指定范围,或者客户端关闭防火墙
服务端需要开启 20、21 端口
1、通过tcp的21端口建立起通道
2、与主动方式的FTP不同,客户端不会提交PORT命令并允许服务器回连它的数据端口,而是提交PASV命令,会产生两个随机非特殊的端口 N ( 1023 < N < 65536 ) 和 N + 1 给到FTP服务器。其中N端口跟主动模式一样,$\color {red} {会把N端口给到服务端的远程21端口,相当于FTP服务端被动接受数据端口号而不是之前PORT模式的主动发起连接}$
3、FTP服务端会打开 N + 1 端口号
4、客户端发起 对FTP服务器的 N + 1 端口建立数据连接。
总结:所以要保证FTP服务器开放对应范围端口,或者 FTP服务端 把防火墙关闭
服务端需要开启通信端口、一定范围内的数据端口
依赖
<!-- 集成ftp-->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.3</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
<version>1.3.2</version>
</dependency>
工具类
package com.example.springbootftpdemo.utils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPReply;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.io.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* <h2></h2>
* @author gitsilence
* date 2021/6/18
*/
@Component
public class FtpUtils {
private final Logger logger = LoggerFactory.getLogger(FtpUtils.class);
@Value("${custom.config.file-server.ip}")
private String FTP_ADDRESS;
@Value("${custom.config.file-server.port}")
private Integer FTP_PORT;
@Value("${custom.config.file-ftp-user}")
private String FTP_USERNAME;
@Value("${custom.config.file-ftp-password}")
private String FTP_PASSWORD;
@Value("${custom.config.file-savepath}")
private String FTP_BASEPATH;
@Value("${custom.config.enterLocalPassiveMode}")
private String isEnablePassiveMode;
//根据当前文件生成 文件夹
private static String getTimePath() {
Date now = new Date();
DateFormat format = new SimpleDateFormat("yyyy/MM/dd/");
return format.format(now);
}
public String upload (String filePath) {
File file = new File(filePath);
String originName = file.getName();
logger.info("originName : {}", originName);
StringBuilder url = new StringBuilder();
FTPClient ftp = new FTPClient();
ftp.setControlEncoding("utf-8");
try {
int reply;
ftp.enterLocalPassiveMode();
ftp.setConnectTimeout(120000);
ftp.setDataTimeout(120000);
// 连接FTP服务器
ftp.connect(FTP_ADDRESS, FTP_PORT);
// 登录
ftp.login(FTP_USERNAME, FTP_PASSWORD);
reply = ftp.getReplyCode();
// FTPReply.isPositiveCompletion()
logger.info("reply: {}", reply);
if (!FTPReply.isPositiveCompletion(reply)) {
logger.warn("connect failed .... ftp server");
}
String timePath = getTimePath();
String saveDir = FTP_BASEPATH + timePath;
url.append(saveDir);
ftp.setFileType(FTPClient.BINARY_FILE_TYPE);
createDir(ftp, saveDir);
// originName = System.currentTimeMillis() + originName.substring(originName.lastIndexOf('.'));
url.append(originName);
FileInputStream inputStream = FileUtils.openInputStream(file);
logger.info("parth is : {}", url.toString());
if ("true".equals(isEnablePassiveMode)) {
// open passive mode
ftp.enterLocalPassiveMode();
}
boolean b = ftp.storeFile(originName, inputStream);
logger.info("upload result: {}", (b ? "upload success" : "upload failed "));
// logout
ftp.logout();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (ftp.isConnected()) {
try {
ftp.disconnect();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
return url.toString();
}
// 创建文件夹,并切换到该文件夹
// 比如: hello/test
//最终会切换到test 文件夹返回
private void createDir(FTPClient client, String path) throws IOException {
String[] dirs = path.split("/");
for (String dir : dirs) {
if (StringUtils.isEmpty(dir)) {
continue;
}
// if not exist, mkdirs
if (!client.changeWorkingDirectory(dir)) {
client.makeDirectory(dir);
}
client.changeWorkingDirectory(dir);
}
}
}
完整代码地址:https://github.com/MrNiebit/springboot-ftp-demo
对于主动模式,可以尝试关闭 客户端的防火墙
对于被动模式,可以尝试关闭 FTP服务端的防火墙