实现方案

ftpserver支持配置文件和db两种方式来保存账号信息和其它相关配置,如果我们的业务系统需要将用户信息和ftp的账号信息打通,并且还有相关的业务统计,比如统计系统中每个人上传文件的时间、个数等等,那么使用数据库来保存ftp账号信息还是比较方便灵活的。我这里就选择使用mysql了。

springboot开始整合

添加项目依赖

  1. <dependency>
  2. <groupId>org.apache.ftpserver</groupId>
  3. <artifactId>ftpserver-core</artifactId>
  4. <version>1.1.1</version>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.ftpserver</groupId>
  8. <artifactId>ftplet-api</artifactId>
  9. <version>1.1.1</version>
  10. </dependency>
  11. <dependency>
  12. <groupId>org.apache.mina</groupId>
  13. <artifactId>mina-core</artifactId>
  14. <version>2.0.16</version>
  15. </dependency>

创建FTP_USER 数据库

  1. CREATE TABLE FTP_USER (
  2. userid VARCHAR(64) NOT NULL PRIMARY KEY,
  3. userpassword VARCHAR(64),
  4. homedirectory VARCHAR(128) NOT NULL,
  5. enableflag BOOLEAN DEFAULT TRUE,
  6. writepermission BOOLEAN DEFAULT FALSE,
  7. idletime INT DEFAULT 0,
  8. uploadrate INT DEFAULT 0,
  9. downloadrate INT DEFAULT 0,
  10. maxloginnumber INT DEFAULT 0,
  11. maxloginperip INT DEFAULT 0
  12. );

字段说明:

  • userid:登录账号
  • userpassword:登录密码
  • homedirectory:主目录,用户授权主目录
  • enableflag:当前用户可用
  • writepermission:具有上传权限
  • idletime:空闲时间(为300秒)
  • uploadrate:上传速率限制为480000字节每秒 0为不限制
  • downloadrate:下载速率限制为480000字节每秒 0为不限制
  • maxloginnumber:最大登陆用户数
  • maxloginperip:同IP登陆用户数

创建Ftpserver

配置ftpserver,提供ftpserver的init()、start()、stop()方法

  1. package com.qingfeng.ftpserver.mysqldatasource;
  2. import com.qingfeng.ftpserver.config.YmlProperties;
  3. import com.qingfeng.ftpserver.ftplet.MyFtpPlet;
  4. import org.apache.ftpserver.ConnectionConfigFactory;
  5. import org.apache.ftpserver.DataConnectionConfigurationFactory;
  6. import org.apache.ftpserver.FtpServer;
  7. import org.apache.ftpserver.FtpServerFactory;
  8. import org.apache.ftpserver.ftplet.FtpException;
  9. import org.apache.ftpserver.ftplet.Ftplet;
  10. import org.apache.ftpserver.listener.ListenerFactory;
  11. import org.apache.ftpserver.usermanager.ClearTextPasswordEncryptor;
  12. import org.apache.ftpserver.usermanager.DbUserManagerFactory;
  13. import org.slf4j.Logger;
  14. import org.slf4j.LoggerFactory;
  15. import org.springframework.context.annotation.Configuration;
  16. import org.springframework.stereotype.Service;
  17. import javax.sql.DataSource;
  18. import java.io.IOException;
  19. import java.util.HashMap;
  20. import java.util.Map;
  21. /**
  22. * @ProjectName MyFtpServer
  23. * @author Administrator
  24. * @version 1.0.0
  25. * @Description
  26. * 注意:被@Configuration标记的类会被加入ioc容器中,而且类中所有带 @Bean注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例。
  27. * ftp服务访问地址:
  28. * ftp://localhost:2121/
  29. * @createTime 2022/4/21 0021 23:21
  30. */
  31. @Configuration("MyFtp")
  32. @Service
  33. public class MyFtpServer {
  34. private static final Logger logger = LoggerFactory.getLogger(MyFtpServer.class);
  35. // springboot配置好数据源直接注入即可
  36. private DataSource dataSource;
  37. private YmlProperties yml;
  38. protected FtpServer server;
  39. // 我们这里利用spring加载@Configuration的特性来完成ftp server的初始化
  40. public MyFtpServer(DataSource dataSource, YmlProperties yml) {
  41. this.dataSource = dataSource;
  42. this.yml = yml;
  43. initFtp();
  44. logger.info("Apache ftp server is already instantiation complete!");
  45. System.out.println("Apache ftp server is already instantiation complete!");
  46. }
  47. /**
  48. * ftp server init
  49. * @throws IOException
  50. */
  51. public void initFtp() {
  52. FtpServerFactory serverFactory = new FtpServerFactory();
  53. ListenerFactory listenerFactory = new ListenerFactory();
  54. // 1、设置服务端口
  55. listenerFactory.setPort(new Integer(this.yml.getFtpport()));
  56. // 2、设置被动模式数据上传的接口范围,云服务器需要开放对应区间的端口给客户端
  57. DataConnectionConfigurationFactory dataConnectionConfFactory = new DataConnectionConfigurationFactory();
  58. dataConnectionConfFactory.setPassivePorts(this.yml.getPassiveports());
  59. listenerFactory.setDataConnectionConfiguration(dataConnectionConfFactory.createDataConnectionConfiguration());
  60. // 3、增加SSL安全配置
  61. /*
  62. SslConfigurationFactory ssl = new SslConfigurationFactory();
  63. ssl.setKeystoreFile(new File("src/main/resources/ftpserver.jks"));
  64. ssl.setKeystorePassword("password");
  65. ssl.setSslProtocol("SSL");
  66. //set the SSL configuration for the listener
  67. listenerFactory.setSslConfiguration(ssl.createSslConfiguration());
  68. listenerFactory.setImplicitSsl(true);
  69. */
  70. //替换默认的监听器
  71. serverFactory.addListener("default", listenerFactory.createListener());
  72. // 4、设置最大连接数
  73. ConnectionConfigFactory connectionConfigFactory = new ConnectionConfigFactory();
  74. connectionConfigFactory.setMaxLogins(new Integer(this.yml.getMaxlogins()));
  75. serverFactory.setConnectionConfig(connectionConfigFactory.createConnectionConfig());
  76. // 5、配置自定义用户事件
  77. Map<String, Ftplet> ftpLets = new HashMap<String, Ftplet>();
  78. ftpLets.put("ftpService", new MyFtpPlet());
  79. serverFactory.setFtplets(ftpLets);
  80. // 6、读取用户的配置信息
  81. // 注意:配置文件位于resources目录下,如果项目使用内置容器打成jar包发布,FTPServer无法直接直接读取Jar包中的配置文件。
  82. // 解决办法:将文件复制到指定目录(本文指定到根目录)下然后FTPServer才能读取到。
  83. /*
  84. PropertiesUserManagerFactory userManagerFactory = new PropertiesUserManagerFactory();
  85. String tempPath = System.getProperty("java.io.tmpdir") + System.currentTimeMillis() + ".properties";
  86. File tempConfig = new File(tempPath);
  87. ClassPathResource resource = new ClassPathResource("users.properties");
  88. IOUtils.copy(resource.getInputStream(), new FileOutputStream(tempConfig));
  89. userManagerFactory.setFile(tempConfig);
  90. userManagerFactory.setPasswordEncryptor(new ClearTextPasswordEncryptor()); //密码以明文的方式
  91. serverFactory.setUserManager(userManagerFactory.createUserManager());
  92. */
  93. // 6.2、基于数据库来存储用户实例
  94. DbUserManagerFactory dbUserManagerFactory = new DbUserManagerFactory();
  95. // todo....
  96. System.out.println("======================================");
  97. System.out.println(this.yml.getAdminname());
  98. dbUserManagerFactory.setDataSource(dataSource);
  99. dbUserManagerFactory.setAdminName(this.yml.getAdminname());
  100. dbUserManagerFactory.setSqlUserAdmin(this.yml.getSqluseradmin());
  101. dbUserManagerFactory.setSqlUserInsert(this.yml.getSqluserinsert());
  102. dbUserManagerFactory.setSqlUserDelete(this.yml.getSqluserdelete());
  103. dbUserManagerFactory.setSqlUserUpdate(this.yml.getSqluserupdate());
  104. dbUserManagerFactory.setSqlUserSelect(this.yml.getSqluserselect());
  105. dbUserManagerFactory.setSqlUserSelectAll(this.yml.getSqluserselectall());
  106. dbUserManagerFactory.setSqlUserAuthenticate(this.yml.getSqluserauthenticate());
  107. dbUserManagerFactory.setPasswordEncryptor(new ClearTextPasswordEncryptor()); //密码以明文的方式
  108. serverFactory.setUserManager(dbUserManagerFactory.createUserManager());
  109. // 7、实例化FTP Server
  110. server = serverFactory.createServer();
  111. }
  112. /**
  113. * ftp server start
  114. */
  115. public void start() {
  116. try {
  117. server.start();
  118. logger.info("Apache Ftp server is starting!");
  119. } catch (FtpException e) {
  120. e.printStackTrace();
  121. }
  122. }
  123. /**
  124. * ftp server stop
  125. */
  126. public void stop() {
  127. server.stop();
  128. logger.info("Apache Ftp server is stoping!");
  129. }
  130. }

创建配置监听器Listener

  1. package com.qingfeng.ftpserver.listener;
  2. import com.qingfeng.ftpserver.mysqldatasource.MyFtpServer;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.web.context.support.WebApplicationContextUtils;
  7. import javax.servlet.ServletContextEvent;
  8. import javax.servlet.ServletContextListener;
  9. import javax.servlet.annotation.WebListener;
  10. @WebListener
  11. public class FtpServerListener implements ServletContextListener {
  12. private static final Logger logger = LoggerFactory.getLogger(MyFtpServer.class);
  13. private static final String SERVER_NAME="FTP-SERVER";
  14. @Autowired
  15. private MyFtpServer server;
  16. //容器初始化调用方法start ftpServer
  17. public void contextInitialized(ServletContextEvent sce) {
  18. WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext())
  19. .getAutowireCapableBeanFactory().autowireBean(this);//必须添加的代码
  20. sce.getServletContext().setAttribute(SERVER_NAME,server);
  21. try {
  22. System.out.println("===================111===================");
  23. System.out.println(server);
  24. //项目启动时已经加载好了
  25. server.start();
  26. System.out.println("Apache Ftp server is started!");
  27. logger.info("Apache Ftp server is started!");
  28. } catch (Exception e){
  29. e.printStackTrace();
  30. throw new RuntimeException("Apache Ftp server start failed!", e);
  31. }
  32. }
  33. //容器关闭时调用方法stop ftpServer
  34. public void contextDestroyed(ServletContextEvent sce) {
  35. server.stop();
  36. sce.getServletContext().removeAttribute(SERVER_NAME);
  37. logger.info("Apache Ftp server is stoped!");
  38. System.out.println("Apache Ftp server is stoped!");
  39. }
  40. }

创建MyFtpPlet实现监听事件

  1. package com.qingfeng.ftpserver.ftplet;
  2. import org.apache.ftpserver.ftplet.*;
  3. import org.slf4j.Logger;
  4. import org.slf4j.LoggerFactory;
  5. import java.io.File;
  6. import java.io.IOException;
  7. public class MyFtpPlet extends DefaultFtplet {
  8. private static final Logger logger = LoggerFactory.getLogger(MyFtpPlet.class);
  9. @Override
  10. public FtpletResult onLogin(FtpSession session, FtpRequest request) throws FtpException, IOException {
  11. // 获取上传文件的上传路径
  12. String path = session.getUser().getHomeDirectory();
  13. // 获取上传用户
  14. String name = session.getUser().getName();
  15. //校验文件夹路径是否存在
  16. viladateDir(path);
  17. logger.info("用户:'{}'登录成功, 目录地址: '{}'", name, path);
  18. System.out.println("用户:'" + name + "'登录成功, 目录地址:'" + path + "'");
  19. return super.onLogin(session, request);
  20. }
  21. @Override
  22. public FtpletResult onUploadStart(FtpSession session, FtpRequest request) throws FtpException, IOException {
  23. // 获取上传文件的上传路径
  24. String path = session.getUser().getHomeDirectory();
  25. //校验文件夹路径是否存在
  26. viladateDir(path);
  27. // 获取上传用户
  28. String name = session.getUser().getName();
  29. // 获取上传文件名
  30. String filename = request.getArgument();
  31. logger.info("用户:'{}',上传文件到目录:'{}',文件名称为:'{},状态:开始上传~'", name, path, filename);
  32. System.out.println("用户:'" + name + "',上传文件到目录:'" + path + "',文件名称为:'" + filename + "',状态:开始上传~");
  33. return super.onUploadStart(session, request);
  34. }
  35. @Override
  36. public FtpletResult onUploadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException {
  37. // 获取上传文件的上传路径
  38. String path = session.getUser().getHomeDirectory();
  39. // 获取上传用户
  40. String name = session.getUser().getName();
  41. // 获取上传文件名
  42. String filename = request.getArgument();
  43. logger.info("用户:'{}',上传文件到目录:'{}',文件名称为:'{},状态:成功!'", name, path, filename);
  44. System.out.println("用户:'" + name + "',上传文件到目录:'" + path + "',文件名称为:'" + filename + "',状态:成功!'");
  45. return super.onUploadEnd(session, request);
  46. }
  47. @Override
  48. public FtpletResult onDownloadStart(FtpSession session, FtpRequest request) throws FtpException, IOException {
  49. // todo servies...
  50. return super.onDownloadStart(session, request);
  51. }
  52. @Override
  53. public FtpletResult onDownloadEnd(FtpSession session, FtpRequest request) throws FtpException, IOException {
  54. // todo servies...
  55. return super.onDownloadEnd(session, request);
  56. }
  57. //判断文件夹是否存在,不存在则创建文件夹
  58. public void viladateDir(String path) {
  59. File file = new File(path);
  60. // 如果文件夹不存在则创建
  61. if (!file.exists() && !file.isDirectory()){
  62. System.out.println("文件夹不存在,创建新的文件夹");
  63. file.mkdir();
  64. }
  65. }
  66. }

创建FtpConfig配置类

  1. package com.qingfeng.ftpserver.config;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
  4. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  5. @Configuration
  6. public class FtpConfig implements WebMvcConfigurer {
  7. @Override
  8. public void addResourceHandlers(ResourceHandlerRegistry registry) {
  9. //可以通过os来判断
  10. String os = System.getProperty("os.name");
  11. //linux设置
  12. //registry.addResourceHandler("/ftp/**").addResourceLocations("file:/home/pic/");
  13. //windows设置
  14. //第一个方法设置访问路径前缀,第二个方法设置资源路径,既可以指定项目classpath路径,也可以指定其它非项目路径
  15. registry.addResourceHandler("/ftp/**").addResourceLocations("file:D:\\FtpFileHome");
  16. registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
  17. }
  18. }

创建参数Properties类

  1. package com.qingfeng.ftpserver.config;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import org.springframework.stereotype.Component;
  4. @Component
  5. @ConfigurationProperties(prefix = "ftp")
  6. public class YmlProperties {
  7. private String homedirectory;
  8. private String ftpport;
  9. private String passiveports;
  10. private String maxlogins;
  11. private String adminname;
  12. private String sqluseradmin;
  13. private String sqluserinsert;
  14. private String sqluserdelete;
  15. private String sqluserupdate;
  16. private String sqluserselect;
  17. private String sqluserselectall;
  18. private String sqluserauthenticate;
  19. public String getHomedirectory() {
  20. return homedirectory;
  21. }
  22. public void setHomedirectory(String homedirectory) {
  23. this.homedirectory = homedirectory;
  24. }
  25. public String getFtpport() {
  26. return ftpport;
  27. }
  28. public void setFtpport(String ftpport) {
  29. this.ftpport = ftpport;
  30. }
  31. public String getPassiveports() {
  32. return passiveports;
  33. }
  34. public void setPassiveports(String passiveports) {
  35. this.passiveports = passiveports;
  36. }
  37. public String getMaxlogins() {
  38. return maxlogins;
  39. }
  40. public void setMaxlogins(String maxlogins) {
  41. this.maxlogins = maxlogins;
  42. }
  43. public String getAdminname() {
  44. return adminname;
  45. }
  46. public void setAdminname(String adminname) {
  47. this.adminname = adminname;
  48. }
  49. public String getSqluseradmin() {
  50. return sqluseradmin;
  51. }
  52. public void setSqluseradmin(String sqluseradmin) {
  53. this.sqluseradmin = sqluseradmin;
  54. }
  55. public String getSqluserinsert() {
  56. return sqluserinsert;
  57. }
  58. public void setSqluserinsert(String sqluserinsert) {
  59. this.sqluserinsert = sqluserinsert;
  60. }
  61. public String getSqluserdelete() {
  62. return sqluserdelete;
  63. }
  64. public void setSqluserdelete(String sqluserdelete) {
  65. this.sqluserdelete = sqluserdelete;
  66. }
  67. public String getSqluserupdate() {
  68. return sqluserupdate;
  69. }
  70. public void setSqluserupdate(String sqluserupdate) {
  71. this.sqluserupdate = sqluserupdate;
  72. }
  73. public String getSqluserselect() {
  74. return sqluserselect;
  75. }
  76. public void setSqluserselect(String sqluserselect) {
  77. this.sqluserselect = sqluserselect;
  78. }
  79. public String getSqluserselectall() {
  80. return sqluserselectall;
  81. }
  82. public void setSqluserselectall(String sqluserselectall) {
  83. this.sqluserselectall = sqluserselectall;
  84. }
  85. public String getSqluserauthenticate() {
  86. return sqluserauthenticate;
  87. }
  88. public void setSqluserauthenticate(String sqluserauthenticate) {
  89. this.sqluserauthenticate = sqluserauthenticate;
  90. }
  91. }

配置application.yml

  1. ftp:
  2. homedirectory: D:/FtpFileHome
  3. ftpport: 2121
  4. passiveports: 10000-10500
  5. maxlogins: 20
  6. adminname: admin
  7. sqluseradmin: SELECT userid FROM FTP_USER WHERE userid='{userid}' AND userid='admin'
  8. sqluserinsert: INSERT INTO FTP_USER (userid, userpassword, homedirectory,enableflag, writepermission, idletime, uploadrate, downloadrate) VALUES ('{userid}', '{userpassword}', '{homedirectory}', {enableflag}, {writepermission}, {idletime}, uploadrate}, {downloadrate})
  9. sqluserdelete: DELETE FROM FTP_USER WHERE userid = '{userid}'
  10. 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}'
  11. sqluserselect: SELECT userid,userpassword,CONCAT('${ftp.homedirectory}',homedirectory) as homedirectory,enableflag,writepermission,idletime,uploadrate,downloadrate,maxloginnumber,maxloginperip FROM ftp_user WHERE userid = '{userid}'
  12. sqluserselectall: SELECT userid FROM FTP_USER ORDER BY userid
  13. sqluserauthenticate: SELECT userid, userpassword FROM FTP_USER WHERE userid='{userid}'

启动类配置@ServletComponentScan

image.png

拓展功能一(密码验证器)

用户密码验证

https://github.com/patrickfav/bcrypt
默认配置中,我们使用的用户密码是明文加密模式。

  1. # 明文
  2. dbUserManagerFactory.setPasswordEncryptor(new ClearTextPasswordEncryptor());
  3. # Md5
  4. dbUserManagerFactory.setPasswordEncryptor(new Md5PasswordEncryptor());
  5. # 自定义Hash
  6. dbUserManagerFactory.setPasswordEncryptor(new MyPasswordEncryptor());

image.png

自定义MyPasswordEncryptor加密器

增加bcrypt依赖

  1. <dependency>
  2. <groupId>at.favre.lib</groupId>
  3. <artifactId>bcrypt</artifactId>
  4. <version>0.9.0</version>
  5. </dependency>

新建MyPasswordEncryptor

  1. package com.qingfeng.ftpserver.config;
  2. /**
  3. * @author Administrator
  4. * @version 1.0.0
  5. * @ProjectName finder-module-ftpserver
  6. * @Description TODO
  7. * @createTime 2022年04月21日 09:34:00
  8. */
  9. import at.favre.lib.crypto.bcrypt.BCrypt;
  10. import org.apache.ftpserver.usermanager.PasswordEncryptor;
  11. import java.security.NoSuchAlgorithmException;
  12. public class MyPasswordEncryptor implements PasswordEncryptor {
  13. public MyPasswordEncryptor() {
  14. }
  15. public String encrypt(String password) {
  16. String bcryptString = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(10, password.toCharArray());
  17. System.out.println(bcryptString);
  18. return bcryptString;
  19. }
  20. public boolean matches(String passwordToCheck, String storedPassword) {
  21. System.out.println("--------------matches-------------");
  22. System.out.println(passwordToCheck);
  23. System.out.println(storedPassword);
  24. if (storedPassword == null) {
  25. throw new NullPointerException("storedPassword can not be null");
  26. } else if (passwordToCheck == null) {
  27. throw new NullPointerException("passwordToCheck can not be null");
  28. } else {
  29. BCrypt.Result result = BCrypt.verifyer().verify(passwordToCheck.toCharArray(), storedPassword);
  30. // return this.encrypt(passwordToCheck).equalsIgnoreCase(storedPassword);
  31. return result.verified;
  32. }
  33. }
  34. public static void main(String[] args) throws NoSuchAlgorithmException {
  35. String bcryptString = BCrypt.with(BCrypt.Version.VERSION_2Y).hashToString(10, "123456".toCharArray());
  36. System.out.println(bcryptString);
  37. }
  38. }

拓展功能二(文件类型验证)

  1. @Override
  2. public FtpletResult onUploadStart(FtpSession session, FtpRequest request) throws FtpException, IOException {
  3. // 获取上传文件的上传路径
  4. String path = session.getUser().getHomeDirectory();
  5. //校验文件夹路径是否存在
  6. viladateDir(path);
  7. // 获取上传用户
  8. String name = session.getUser().getName();
  9. // 获取上传文件名
  10. String filename = request.getArgument();
  11. logger.info("用户:'{}',上传文件到目录:'{}',文件名称为:'{},状态:开始上传~'", name, path, filename);
  12. System.out.println("用户:'" + name + "',上传文件到目录:'" + path + "',文件名称为:'" + filename + "',状态:开始上传~");
  13. session.write(new DefaultFtpReply(550, "附件格式不正确!"));
  14. return super.onUploadStart(session, request);
  15. }

image.png

项目启动测试

image.png
image.png
image.png

可能遇到问题

1、@Autowired自动注入对象为Null的问题解决

image.png
解决方案:
增加如下配置

  1. WebApplicationContextUtils.getRequiredWebApplicationContext(sce.getServletContext())
  2. .getAutowireCapableBeanFactory().autowireBean(this);//必须添加的代码

image.png