不积跬步,无以至千里;不积小流,无以成江海。
| @Author:TTODS
前言本人因学习需要租了一台Liunx云服务器,为了充分利用这台云服务器,我在上面搭建了一个个人文件服务器。
它的主要用途:
- 可以将一些不常用但有用的文件丢到该服务器上吃灰,填出自己电脑上的磁盘空间
- 从个人的角度出发,由于自己电脑上有些环境没有配置,有些课程需要用到学校机房的电脑。这时就可以用该服务器来存储实验课的代码和数据,实验课开始时从服务器上取出上次上传的代码,结束后再上传新的代码上去。
系统主页截图:
本系统的功能分为三个模块:文件管理、权限与安全和日志统计
文件管理- 实现文件的上传、下载、预览功能。
- 为了避免用户误删文件导致文件丢出造成严重后果,还实现了回收站功能。
- 使用请求拦截器对用户请求进行权限验证。
- 使用基于TimedCache(定时缓存)和ip地址的登录保护,防止黑客恶意暴力破解密码。
- 对用户的操作进行日志统计。
在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
上传文件的前端部分,我直接使用了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
/delete接口代码
@DeleteMapping("/delete")
@ResponseBody
public R
/recovery接口代码
@PostMapping("/recovery")
@ResponseBody
public R
权限与安全
用户登录与基于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



