栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

基于springboot实现一个文件服务器

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

基于springboot实现一个文件服务器

不积跬步,无以至千里;不积小流,无以成江海。

| @Author:TTODS

前言

本人因学习需要租了一台Liunx云服务器,为了充分利用这台云服务器,我在上面搭建了一个个人文件服务器。

它的主要用途:

  1. 可以将一些不常用但有用的文件丢到该服务器上吃灰,填出自己电脑上的磁盘空间
  2. 从个人的角度出发,由于自己电脑上有些环境没有配置,有些课程需要用到学校机房的电脑。这时就可以用该服务器来存储实验课的代码和数据,实验课开始时从服务器上取出上次上传的代码,结束后再上传新的代码上去。

系统主页截图:

功能模块简介

本系统的功能分为三个模块:文件管理、权限与安全和日志统计

文件管理
  1. 实现文件的上传、下载、预览功能。
  2. 为了避免用户误删文件导致文件丢出造成严重后果,还实现了回收站功能。
权限与安全
  1. 使用请求拦截器对用户请求进行权限验证。
  2. 使用基于TimedCache(定时缓存)和ip地址的登录保护,防止黑客恶意暴力破解密码。
日志统计
  1. 对用户的操作进行日志统计。
功能的具体实现 文件管理

在springboot的配置文件application.xml中除了常规的端口、数据源、日志配置外,还需配置文件服务器的根路径和回收站的根路径

file-server:
# root-path 请使用'/'作为路径分隔符
  root-path: D:/rootPath
  recycle-bin-path: D:/recycleBin

然后在Controller层中使用@Value注解获取配置的属性

    @Value("${file-server.root-path}")
    private String rootPath;
    @Value("${file-server.recycle-bin-path}")
    private String recycleBinPath;
上传功能

根据请求提交的directory参数,拼接出目标文件夹的路径,然后将上传的文件保存到该目录下。这里有一个小细节就是如果该目录下已存在同名文件,为了防止该文件被覆盖,可以在上传的文件名后拼接当前的时间戳。

   @PostMapping("/upload")
   @ResponseBody
   public R upload(@RequestParam("file") MultipartFile file, @RequestParam("directory") String directory,
                           HttpServletRequest request) {
       String filename = file.getOriginalFilename();
       log.info("文件名:[{}]", filename);
       log.info("上传路径:[{}]", directory);
       directory = directory.replace("\", "/");
       StrUtil.prependIfMissing(directory, "/");
       log.info("文件上传开始,文件名:[{}],上传路径:[{}]", filename, directory);
       String absolutePath = rootPath + directory;
       File newFile = new File(absolutePath, filename);
       // 避免同名文件被覆盖
       if(newFile.exists()){
           // 文件拓展名
           String extName = FileUtil.extName(filename);
           if(StrUtil.isNotBlank(extName)){
               filename = filename.substring(0,filename.lastIndexOf('.'))+'('+new Date().getTime()+")."+extName;
           }
           newFile = new File(absolutePath, filename);
       }
       try {
           FileUtil.touch(newFile);
           FileUtil.writeFromStream(file.getInputStream(), newFile);
           logSystem.log(Action.UPLOAD, filename,request);
       } catch (IOException|RuntimeException e) {
           log.info("文件传输出错:{}", e.getMessage());
           newFile.delete();
           return R .failed(e.getMessage());
       }
       log.info("文件保存成功...");
       return R.ok(null);
   }
 

上传文件的前端部分,我直接使用了layui的多文件上传功能,实现起来也比较简单。

文件预览与下载

文件预览与文件下载的功能其实差不多,直接夹文件写入response的输出流中,浏览器会根据自己能否解析该文件类型来决定是预览还是开启下载。如果是要下载支持预览的类型的文件,需要在回应头中添加一个content-disposition属性,具体见代码。

文件预览代码

@GetMapping("/preview/**")
public void preview(HttpServletRequest request, HttpServletResponse response) {
    String requestURI = request.getRequestURI();
    String relativePath = URLUtil.decode(StrUtil.removePrefix(requestURI, "/preview"));
    String absolutePath =rootPath + relativePath;
    File file = new File(absolutePath);
    if (!file.exists()) {
        throw new FileNotExistError(absolutePath);
    }
    //        if (file.getName().endsWith(".html")) {
    //            // 启用下载
    //            response.setHeader("content-disposition", "attachment;filename=" + URLUtil.encode(file.getName()));
    //        }
    
    // 对给浏览器自行判断,若为pdf等浏览器可以解析的文件格式,则预览。否则浏览器会自动下载。
    try {
        ServletOutputStream outputStream = response.getOutputStream();
        FileUtil.writeToStream(file, outputStream);
        logSystem.log(Action.DOWNLOAD,relativePath,request);

    } catch (IOException e) {
        e.printStackTrace();
    }
}

文件下载代码

@GetMapping("/download/**")
public void download(HttpServletRequest request, HttpServletResponse response) {
    // 从请求路径中获取文件的相对路径
    String requestURI = request.getRequestURI();
    // 相对路径
    String relativePath = StrUtil.removePrefix(requestURI, "/download");
    // 绝对路径
    String absolutePath = URLUtil.decode(rootPath + relativePath);
    File file = new File(absolutePath);

    if (!file.exists()||file.isDirectory()) {
        throw new FileNotExistError(absolutePath);
    }
    response.setHeader("content-disposition", "attachment;filename=" + URLUtil.encode(file.getName()));
    try {
        ServletOutputStream outputStream = response.getOutputStream();
        FileUtil.writeToStream(file, outputStream);
        logSystem.log(Action.DOWNLOAD,URLUtil.decode(relativePath),request);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
回收站功能的实现

在文件管理中删除文件,实际上是调用了/remove接口将文件移动到回收站路径下.而在回收站中删除文件是调用了/delete接口,这才是真正意义上的删除文件.在回收站中还可以恢复文件,恢复文件调用的是/recovery接口。

/remove接口代码

// 将文件(夹)移入回收站
@ResponseBody
@DeleteMapping("/remove")
public R remove(@RequestParam("file") String file,HttpServletRequest request){
    File  f = new File(rootPath,file);
    if(!f.exists()){
        return R.failed("文件不存在");
    }
    // 将文件移入回收站
    // 如果回收站存在同名文件则重命名文件
    if(FileUtil.exist(new File(recycleBinPath,f.getName()))){
        String filename = f.getName();
        String extName = FileUtil.extName(filename);
        String newFileName;
        // 有拓展名
        if(!StrUtil.isBlank(extName)){
            newFileName = filename.substring(0,filename.lastIndexOf('.'))+'('+new Date().getTime()+")."+extName;
        }else{
            // 没有拓展名
            newFileName = filename +'('+new Date().getTime()+')';
        }

        File renamedFile = new File(f.getParentFile(),newFileName);
        f.renameTo(renamedFile);
        f = renamedFile;
    }
    log.info(f.getName());
    FileUtil.move(f,new File(recycleBinPath),true);
    logSystem.log(Action.REMOVE,file,request);

    return R.ok("文件已移入回收站");
}
 

/delete接口代码

@DeleteMapping("/delete")
@ResponseBody
public R delete(@RequestParam("file") String file,HttpServletRequest request){
    File f = new File(recycleBinPath, file);
    boolean success = FileUtil.del(f);
    if(success) {
        logSystem.log(Action.DELETE,file,request);
        return R.ok(null);
    }
    return R.failed("操作失败");
}
 

/recovery接口代码

@PostMapping("/recovery")
@ResponseBody
public R recovery(@RequestParam("file") String file, HttpServletRequest request){
    File f = new File(recycleBinPath, file);
    try {
        FileUtil.move(f, new File(rootPath), false);
    }catch (IORuntimeException e){
        log.warn("文件恢复失败,原因:"+e.getMessage());
        return R.failed(e.getMessage());
    }
    logSystem.log(Action.RECOVERY,file,request);
    return R.ok("文件恢复成功");
}
 
权限与安全 
用户登录与基于ip地址和定时缓存的登录保护 

登录流程:根据用户名和密码查询数据库,若查询到了用户信息,则说明用户名和密码正确,将用户名存入session中,登录成功。

定时缓存,我这里使用了hutool提供的TimedCache,存入其中的键值对过期将会消失。在用户尝试登录失败后,我们就以该请求的IP地址为键,在缓存中记录该ip的用户尝试登录失败的次数,当登录失败超过五次后则不再处理该ip的登录请求,直到缓存中的数据过期后才能重新尝试,这里设置的是30分钟。

登录代码:

// 缓存 刷新时间 30分钟
private  TimedCache cache = CacheUtil.newTimedCache(3*60*1000);   

@PostMapping("/login")
public String login(@RequestParam String username, @RequestParam String password,
                    @RequestParam(defaultValue = "false") Boolean rememberPassword,
                    HttpServletRequest request,
                    HttpSession session,
                    HttpServletResponse response, Model model) {
    String remoteAddr = request.getRemoteAddr();
    Integer count = (Integer) cache.get(remoteAddr, false);
    if(count !=null&&count>=5){
        model.addAttribute("error","密码尝试次数过多,请半小时后再试");
        return "/login";
    }
    LambdaQueryWrapper wrapper = Wrappers.lambdaQuery().eq(User::getUsername, username)
        .eq(User::getPassword, password);
    User user = userService.getOne(wrapper);
    if(user!=null){
        session.setAttribute("username",username);
    }else{
        if(count==null) count=0;
        if(++count>=5){
            model.addAttribute("error","密码尝试次数过多,请半小时后再试");
        }else{
            model.addAttribute("error","账号或密码错误");
        }
        cache.put(remoteAddr,count);
        return "/login";
    }

    // 如果用户勾选了记住密码选项,则将用户名密码保存在缓存中
    if(true == rememberPassword){
        cookie usernamecookie = new cookie("username", username);
        usernamecookie.setMaxAge(ONE_DAY);
        cookie passwordcookie = new cookie("password", password);
        passwordcookie.setMaxAge(ONE_DAY);
        response.addcookie(usernamecookie);
        response.addcookie(passwordcookie);
    }
    logSystem.log(Action.LOGIN,null,request);
    return "redirect:/";
}
使用拦截器检测用户是否登录

编写一个登录拦截器实现springboot提供的HandlerInterceptor,并实现其preHandle方法。

若session中存在username属性则说明用户已登录,按照正常逻辑处理请求,否则重定向到登录页面。

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
        Object username = request.getSession().getAttribute("username");

        boolean login = null != username;
        if(!login){
            response.sendRedirect("/login");
        }
        return login;
    }
}

编写WebConfig类实现springboot提供的WebMvcConfigurer接口,并实现addInterceptors方法,将我们的拦截器添加到Spring MVC的请求处理流程中。

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 配置拦截器 拦截除
        // 登录、登出、静态资源、错误界面
        // 外所有请求路径
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**")
                .excludePathPatterns("/","/login","/logout","/webjars/**",
                        "/js/**","/css/**","/img/**","/favicon.ico","/error");
    }

}
日志统计

实现简单的日志记录,记录用户的操作。

每条日志信息包括:编号,操作,文件路径(登录操作除外),用户名,IP地址,日期

日志信息实体类

@TableName("log")
@Data
public class Log {
    @TableId(type = IdType.AUTO)
    private Integer id;
    private String action;
    private String path;
    private String username;
    private String ip;
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime datetime;

}

操作枚举类

public enum Action {
    MKDIRS("创建文件夹"),UPLOAD("上传文件"),DOWNLOAD("下载文件"),REMOVE("移除文件"),RECOVERY("恢复文件"),DELETE("删除文件"),LOGIN("登录系统");
    private String action;
    Action(String action){
        this.action = action;
    }
    @Override
    public String toString() {
        return action;
    }
}

简单的日志系统

@Component
public class LogSystem {
    @Autowired
    private LogService logService;

    public void log(Action action, String path, HttpServletRequest request) {
        String username = (String) request.getSession().getAttribute("username");
        String remoteAddr = request.getRemoteAddr();
        logService.log(action.toString(), path, username, remoteAddr);
    }
    
}
总结

本系统实现了一个简单的个人文件服务器,功能方面还是基本满足了一个文件服务器的需求。但是由于本人水平和开发时间有限,很多地方实现的还是比较粗糙。还有一些是基于实际出发,我做这个系统的目的是想给个人使用,所以也只实现了核心的功能,向用户注册等次要功能都没有实现。写下这篇博客一是记录一下这个项目中使用到的技术,二是分享一下实现这个系统的思路,希望对有需要的人有所帮助。

项目仓库:https://gitee.com/TTODS/my-file-server

- THE END -
转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号