实现方案
ftpserver支持配置文件和db两种方式来保存账号信息和其它相关配置,如果我们的业务系统需要将用户信息和ftp的账号信息打通,并且还有相关的业务统计,比如统计系统中每个人上传文件的时间、个数等等,那么使用数据库来保存ftp账号信息还是比较方便灵活的。我这里就选择使用mysql了。
springboot开始整合
添加项目依赖
<dependency><groupId>org.apache.ftpserver</groupId><artifactId>ftpserver-core</artifactId><version>1.1.1</version></dependency><dependency><groupId>org.apache.ftpserver</groupId><artifactId>ftplet-api</artifactId><version>1.1.1</version></dependency><dependency><groupId>org.apache.mina</groupId><artifactId>mina-core</artifactId><version>2.0.16</version></dependency>
创建FTP_USER 数据库
CREATE TABLE FTP_USER (userid VARCHAR(64) NOT NULL PRIMARY KEY,userpassword VARCHAR(64),homedirectory VARCHAR(128) NOT NULL,enableflag BOOLEAN DEFAULT TRUE,writepermission BOOLEAN DEFAULT FALSE,idletime INT DEFAULT 0,uploadrate INT DEFAULT 0,downloadrate INT DEFAULT 0,maxloginnumber INT DEFAULT 0,maxloginperip INT DEFAULT 0);
字段说明:
- userid:登录账号
- userpassword:登录密码
- homedirectory:主目录,用户授权主目录
- enableflag:当前用户可用
- writepermission:具有上传权限
- idletime:空闲时间(为300秒)
- uploadrate:上传速率限制为480000字节每秒 0为不限制
- downloadrate:下载速率限制为480000字节每秒 0为不限制
- maxloginnumber:最大登陆用户数
- maxloginperip:同IP登陆用户数
创建Ftpserver
配置ftpserver,提供ftpserver的init()、start()、stop()方法
package com.qingfeng.ftpserver.mysqldatasource;import com.qingfeng.ftpserver.config.YmlProperties;import com.qingfeng.ftpserver.ftplet.MyFtpPlet;import org.apache.ftpserver.ConnectionConfigFactory;import org.apache.ftpserver.DataConnectionConfigurationFactory;import org.apache.ftpserver.FtpServer;import org.apache.ftpserver.FtpServerFactory;import org.apache.ftpserver.ftplet.FtpException;import org.apache.ftpserver.ftplet.Ftplet;import org.apache.ftpserver.listener.ListenerFactory;import org.apache.ftpserver.usermanager.ClearTextPasswordEncryptor;import org.apache.ftpserver.usermanager.DbUserManagerFactory;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Service;import javax.sql.DataSource;import java.io.IOException;import java.util.HashMap;import java.util.Map;/*** @ProjectName MyFtpServer* @author Administrator* @version 1.0.0* @Description* 注意:被@Configuration标记的类会被加入ioc容器中,而且类中所有带 @Bean注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例。* ftp服务访问地址:* ftp://localhost:2121/* @createTime 2022/4/21 0021 23:21*/@Configuration("MyFtp")@Servicepublic class MyFtpServer {private static final Logger logger = LoggerFactory.getLogger(MyFtpServer.class);// springboot配置好数据源直接注入即可private DataSource dataSource;private YmlProperties yml;protected FtpServer server;// 我们这里利用spring加载@Configuration的特性来完成ftp server的初始化public MyFtpServer(DataSource dataSource, YmlProperties yml) {this.dataSource = dataSource;this.yml = yml;initFtp();logger.info("Apache ftp server is already instantiation complete!");System.out.println("Apache ftp server is already instantiation complete!");}/*** ftp server init* @throws IOException*/public void initFtp() {FtpServerFactory serverFactory = new FtpServerFactory();ListenerFactory listenerFactory = new ListenerFactory();// 1、设置服务端口listenerFactory.setPort(new Integer(this.yml.getFtpport()));// 2、设置被动模式数据上传的接口范围,云服务器需要开放对应区间的端口给客户端DataConnectionConfigurationFactory dataConnectionConfFactory = new DataConnectionConfigurationFactory();dataConnectionConfFactory.setPassivePorts(this.yml.getPassiveports());listenerFactory.setDataConnectionConfiguration(dataConnectionConfFactory.createDataConnectionConfiguration());// 3、增加SSL安全配置/*SslConfigurationFactory ssl = new SslConfigurationFactory();ssl.setKeystoreFile(new File("src/main/resources/ftpserver.jks"));ssl.setKeystorePassword("password");ssl.setSslProtocol("SSL");//set the SSL configuration for the listenerlistenerFactory.setSslConfiguration(ssl.createSslConfiguration());listenerFactory.setImplicitSsl(true);*///替换默认的监听器serverFactory.addListener("default", listenerFactory.createListener());// 4、设置最大连接数ConnectionConfigFactory connectionConfigFactory = new ConnectionConfigFactory();connectionConfigFactory.setMaxLogins(new Integer(this.yml.getMaxlogins()));serverFactory.setConnectionConfig(connectionConfigFactory.createConnectionConfig());// 5、配置自定义用户事件Map<String, Ftplet> ftpLets = new HashMap<String, Ftplet>();ftpLets.put("ftpService", new MyFtpPlet());serverFactory.setFtplets(ftpLets);// 6、读取用户的配置信息// 注意:配置文件位于resources目录下,如果项目使用内置容器打成jar包发布,FTPServer无法直接直接读取Jar包中的配置文件。// 解决办法:将文件复制到指定目录(本文指定到根目录)下然后FTPServer才能读取到。/*PropertiesUserManagerFactory userManagerFactory = new PropertiesUserManagerFactory();String tempPath = System.getProperty("java.io.tmpdir") + System.currentTimeMillis() + ".properties";File tempConfig = new File(tempPath);ClassPathResource resource = new ClassPathResource("users.properties");IOUtils.copy(resource.getInputStream(), new FileOutputStream(tempConfig));userManagerFactory.setFile(tempConfig);userManagerFactory.setPasswordEncryptor(new ClearTextPasswordEncryptor()); //密码以明文的方式serverFactory.setUserManager(userManagerFactory.createUserManager());*/// 6.2、基于数据库来存储用户实例DbUserManagerFactory dbUserManagerFactory = new DbUserManagerFactory();// todo....System.out.println("======================================");System.out.println(this.yml.getAdminname());dbUserManagerFactory.setDataSource(dataSource);dbUserManagerFactory.setAdminName(this.yml.getAdminname());dbUserManagerFactory.setSqlUserAdmin(this.yml.getSqluseradmin());dbUserManagerFactory.setSqlUserInsert(this.yml.getSqluserinsert());dbUserManagerFactory.setSqlUserDelete(this.yml.getSqluserdelete());dbUserManagerFactory.setSqlUserUpdate(this.yml.getSqluserupdate());dbUserManagerFactory.setSqlUserSelect(this.yml.getSqluserselect());dbUserManagerFactory.setSqlUserSelectAll(this.yml.getSqluserselectall());dbUserManagerFactory.setSqlUserAuthenticate(this.yml.getSqluserauthenticate());dbUserManagerFactory.setPasswordEncryptor(new ClearTextPasswordEncryptor()); //密码以明文的方式serverFactory.setUserManager(dbUserManagerFactory.createUserManager());// 7、实例化FTP Serverserver = serverFactory.createServer();}/*** ftp server start*/public void start() {try {server.start();logger.info("Apache Ftp server is starting!");} catch (FtpException e) {e.printStackTrace();}}/*** ftp server stop*/public void stop() {server.stop();logger.info("Apache Ftp server is stoping!");}}
创建配置监听器Listener
package com.qingfeng.ftpserver.listener;import com.qingfeng.ftpserver.mysqldatasource.MyFtpServer;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.context.support.WebApplicationContextUtils;import javax.servlet.ServletContextEvent;import javax.servlet.ServletContextListener;import javax.servlet.annotation.WebListener;@WebListenerpublic class FtpServerListener implements ServletContextListener {private static final Logger logger = LoggerFactory.getLogger(MyFtpServer.class);private static final String SERVER_NAME="FTP-SERVER";@Autowiredprivate MyFtpServer server;//容器初始化调用方法start ftpServerpublic void contextInitialized(ServletContextEvent sce) {WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext()).getAutowireCapableBeanFactory().autowireBean(this);//必须添加的代码sce.getServletContext().setAttribute(SERVER_NAME,server);try {System.out.println("===================111===================");System.out.println(server);//项目启动时已经加载好了server.start();System.out.println("Apache Ftp server is started!");logger.info("Apache Ftp server is started!");} catch (Exception e){e.printStackTrace();throw new RuntimeException("Apache Ftp server start failed!", e);}}//容器关闭时调用方法stop ftpServerpublic void contextDestroyed(ServletContextEvent sce) {server.stop();sce.getServletContext().removeAttribute(SERVER_NAME);logger.info("Apache Ftp server is stoped!");System.out.println("Apache Ftp server is stoped!");}}
创建MyFtpPlet实现监听事件
package com.qingfeng.ftpserver.ftplet;import org.apache.ftpserver.ftplet.*;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.io.File;import java.io.IOException;public class MyFtpPlet extends DefaultFtplet {private static final Logger logger = LoggerFactory.getLogger(MyFtpPlet.class);@Overridepublic FtpletResult onLogin(FtpSession session, FtpRequest request) throws FtpException, IOException {// 获取上传文件的上传路径String path = session.getUser().getHomeDirectory();// 获取上传用户String name = session.getUser().getName();//校验文件夹路径是否存在viladateDir(path);logger.info("用户:'{}'登录成功, 目录地址: '{}'", name, path);System.out.println("用户:'" + name + "'登录成功, 目录地址:'" + path + "'");return super.onLogin(session, request);}@Overridepublic FtpletResult onUploadStart(FtpSession session, FtpRequest request) throws FtpException, IOException {// 获取上传文件的上传路径String path = session.getUser().getHomeDirectory();//校验文件夹路径是否存在viladateDir(path);// 获取上传用户String name = session.getUser().getName();// 获取上传文件名String filename = request.getArgument();logger.info("用户:'{}',上传文件到目录:'{}',文件名称为:'{},状态:开始上传~'", name, path, filename);System.out.println("用户:'" + name + "',上传文件到目录:'" + path + "',文件名称为:'" + filename + "',状态:开始上传~");return super.onUploadStart(session, request);}@Overridepublic FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException {// 获取上传文件的上传路径String path = session.getUser().getHomeDirectory();// 获取上传用户String name = session.getUser().getName();// 获取上传文件名String filename = request.getArgument();logger.info("用户:'{}',上传文件到目录:'{}',文件名称为:'{},状态:成功!'", name, path, filename);System.out.println("用户:'" + name + "',上传文件到目录:'" + path + "',文件名称为:'" + filename + "',状态:成功!'");return super.onUploadEnd(session, request);}@Overridepublic FtpletResult onDownloadStart(FtpSession session, FtpRequest request) throws FtpException, IOException {// todo servies...return super.onDownloadStart(session, request);}@Overridepublic FtpletResult onDownloadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException {// todo servies...return super.onDownloadEnd(session, request);}//判断文件夹是否存在,不存在则创建文件夹public void viladateDir(String path) {File file = new File(path);// 如果文件夹不存在则创建if (!file.exists() && !file.isDirectory()){System.out.println("文件夹不存在,创建新的文件夹");file.mkdir();}}}
创建FtpConfig配置类
package com.qingfeng.ftpserver.config;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configurationpublic class FtpConfig implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {//可以通过os来判断String os = System.getProperty("os.name");//linux设置//registry.addResourceHandler("/ftp/**").addResourceLocations("file:/home/pic/");//windows设置//第一个方法设置访问路径前缀,第二个方法设置资源路径,既可以指定项目classpath路径,也可以指定其它非项目路径registry.addResourceHandler("/ftp/**").addResourceLocations("file:D:\\FtpFileHome");registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");}}
创建参数Properties类
package com.qingfeng.ftpserver.config;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;@Component@ConfigurationProperties(prefix = "ftp")public class YmlProperties {private String homedirectory;private String ftpport;private String passiveports;private String maxlogins;private String adminname;private String sqluseradmin;private String sqluserinsert;private String sqluserdelete;private String sqluserupdate;private String sqluserselect;private String sqluserselectall;private String sqluserauthenticate;public String getHomedirectory() {return homedirectory;}public void setHomedirectory(String homedirectory) {this.homedirectory = homedirectory;}public String getFtpport() {return ftpport;}public void setFtpport(String ftpport) {this.ftpport = ftpport;}public String getPassiveports() {return passiveports;}public void setPassiveports(String passiveports) {this.passiveports = passiveports;}public String getMaxlogins() {return maxlogins;}public void setMaxlogins(String maxlogins) {this.maxlogins = maxlogins;}public String getAdminname() {return adminname;}public void setAdminname(String adminname) {this.adminname = adminname;}public String getSqluseradmin() {return sqluseradmin;}public void setSqluseradmin(String sqluseradmin) {this.sqluseradmin = sqluseradmin;}public String getSqluserinsert() {return sqluserinsert;}public void setSqluserinsert(String sqluserinsert) {this.sqluserinsert = sqluserinsert;}public String getSqluserdelete() {return sqluserdelete;}public void setSqluserdelete(String sqluserdelete) {this.sqluserdelete = sqluserdelete;}public String getSqluserupdate() {return sqluserupdate;}public void setSqluserupdate(String sqluserupdate) {this.sqluserupdate = sqluserupdate;}public String getSqluserselect() {return sqluserselect;}public void setSqluserselect(String sqluserselect) {this.sqluserselect = sqluserselect;}public String getSqluserselectall() {return sqluserselectall;}public void setSqluserselectall(String sqluserselectall) {this.sqluserselectall = sqluserselectall;}public String getSqluserauthenticate() {return sqluserauthenticate;}public void setSqluserauthenticate(String sqluserauthenticate) {this.sqluserauthenticate = sqluserauthenticate;}}
配置application.yml
ftp:homedirectory: D:/FtpFileHomeftpport: 2121passiveports: 10000-10500maxlogins: 20adminname: adminsqluseradmin: SELECT userid FROM FTP_USER WHERE userid='{userid}' AND userid='admin'sqluserinsert: INSERT INTO FTP_USER (userid, userpassword, homedirectory,enableflag, writepermission, idletime, uploadrate, downloadrate) VALUES ('{userid}', '{userpassword}', '{homedirectory}', {enableflag}, {writepermission}, {idletime}, uploadrate}, {downloadrate})sqluserdelete: DELETE FROM FTP_USER WHERE userid = '{userid}'sqluserupdate: UPDATE FTP_USER SET userpassword='{userpassword}',homedirectory='{homedirectory}',enableflag={enableflag},writepermission={writepermission},idletime={idletime},uploadrate={uploadrate},downloadrate={downloadrate},maxloginnumber={maxloginnumber}, maxloginperip={maxloginperip} WHERE userid='{userid}'sqluserselect: SELECT userid,userpassword,CONCAT('${ftp.homedirectory}',homedirectory) as homedirectory,enableflag,writepermission,idletime,uploadrate,downloadrate,maxloginnumber,maxloginperip FROM ftp_user WHERE userid = '{userid}'sqluserselectall: SELECT userid FROM FTP_USER ORDER BY useridsqluserauthenticate: SELECT userid, userpassword FROM FTP_USER WHERE userid='{userid}'
启动类配置@ServletComponentScan

拓展功能一(密码验证器)
用户密码验证
https://github.com/patrickfav/bcrypt
默认配置中,我们使用的用户密码是明文加密模式。
# 明文dbUserManagerFactory.setPasswordEncryptor(new ClearTextPasswordEncryptor());# Md5dbUserManagerFactory.setPasswordEncryptor(new Md5PasswordEncryptor());# 自定义HashdbUserManagerFactory.setPasswordEncryptor(new MyPasswordEncryptor());
自定义MyPasswordEncryptor加密器
增加bcrypt依赖
<dependency><groupId>at.favre.lib</groupId><artifactId>bcrypt</artifactId><version>0.9.0</version></dependency>
新建MyPasswordEncryptor
package com.qingfeng.ftpserver.config;/*** @author Administrator* @version 1.0.0* @ProjectName finder-module-ftpserver* @Description TODO* @createTime 2022年04月21日 09:34:00*/import at.favre.lib.crypto.bcrypt.BCrypt;import org.apache.ftpserver.usermanager.PasswordEncryptor;import java.security.NoSuchAlgorithmException;public class MyPasswordEncryptor implements PasswordEncryptor {public MyPasswordEncryptor() {}public String encrypt(String password) {String bcryptString = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(10, password.toCharArray());System.out.println(bcryptString);return bcryptString;}public boolean matches(String passwordToCheck, String storedPassword) {System.out.println("--------------matches-------------");System.out.println(passwordToCheck);System.out.println(storedPassword);if (storedPassword == null) {throw new NullPointerException("storedPassword can not be null");} else if (passwordToCheck == null) {throw new NullPointerException("passwordToCheck can not be null");} else {BCrypt.Result result = BCrypt.verifyer().verify(passwordToCheck.toCharArray(), storedPassword);// return this.encrypt(passwordToCheck).equalsIgnoreCase(storedPassword);return result.verified;}}public static void main(String[] args) throws NoSuchAlgorithmException {String bcryptString = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(10, "123456".toCharArray());System.out.println(bcryptString);}}
拓展功能二(文件类型验证)
@Overridepublic FtpletResult onUploadStart(FtpSession session, FtpRequest request) throws FtpException, IOException {// 获取上传文件的上传路径String path = session.getUser().getHomeDirectory();//校验文件夹路径是否存在viladateDir(path);// 获取上传用户String name = session.getUser().getName();// 获取上传文件名String filename = request.getArgument();logger.info("用户:'{}',上传文件到目录:'{}',文件名称为:'{},状态:开始上传~'", name, path, filename);System.out.println("用户:'" + name + "',上传文件到目录:'" + path + "',文件名称为:'" + filename + "',状态:开始上传~");session.write(new DefaultFtpReply(550, "附件格式不正确!"));return super.onUploadStart(session, request);}
项目启动测试
可能遇到问题
1、@Autowired自动注入对象为Null的问题解决

解决方案:
增加如下配置
WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext()).getAutowireCapableBeanFactory().autowireBean(this);//必须添加的代码



