介绍

项目中常会遇到需要文件上传和下载的地方,其中文件上传的安全问题不可忽视,下面是我考虑到的文件上传安全问题和用编码实现的过程

  • 对文件大小的判断,防止恶意上传大文件挤占服务器资源

  • 对文件类型的判断,这里实现的是对图片的判断

  • 对图片进行resize处理,防止图片嵌入恶意可执行的代码,通过压缩可以实现对嵌入可代码的破坏

  • 文件保存的地址有两种,一个就是借助第三方服务器进行保存(七牛云),或者是放在自己的服务器,在成功读取文件后,进行保存的时候,可以对文件名进行修改,采用随机数,一定程度上提高了安全性

  • 文件服务器和应用服务器的分开,避免对应用程序的直接破坏

  • 文件夹权限的设置,对用户上传的文件夹设置只读权限,可以有效防止远端直接启动木马程序

  • 当然,假如这个功能不需要,直接关闭文件上传功能是最安全的


编码

此Demo实现文件上传的读取保存,并在数据库中插入数据

项目目录

6.png

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dev</groupId>
<artifactId>springboot-file-upload-download</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-file-upload-download</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!-- springboot整合thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<!--mysql连接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.2</version>
</dependency>
<!--druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.1</version>
</dependency>
<!--fastjson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.57</version>
</dependency>
<!--thumbnailator-->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

单文件上传的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* 单文件上传
* @param file 前端传文件的参数
* @return
*/
@RequestMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file,HttpServletRequest request) {
FileDoc fileDoc = new FileDoc();
try {
if (file.isEmpty()) {
return "文件为空";
}
//获取大小
long size = file.getSize();
log.info("文件大小:" + size);
//判断文件上传大小
if (!FileUtils.checkFileSize(file, 20, "M")) {
log.info("上传文件规定小于20MB");
return "上传文件规定小于20MB";
}
//获取文件名
String filename = file.getOriginalFilename();
log.info("上传的文件名为:" + filename);
//获取文件后缀名
String suffixName = filename.substring(filename.lastIndexOf("."));
log.info("文件的后后缀名:" + suffixName);

//若要判断文件上传的类型,
//生成新文件名 6位随机数+文件后缀名
fileDoc.setFile_name(CodeGenerateUtil.generateVerCode(6).toString()+suffixName);

//拿到ip地址
fileDoc.setIp_addr(request.getRemoteAddr());

File dest = new File(FILEPATH+fileDoc.getFile_name());
//检测目录是否存在
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs(); //新建文件夹
}
file.transferTo(dest); //文件写入
int i = fileDocService.uploadFile(fileDoc);

if (i > 0){
return "文件上传成功";
}else {
return "文件上传失败";
}
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

return "上传失败";
}

FileUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.dev.util;

import org.springframework.web.multipart.MultipartFile;

/**
* @author 路飞
* @create 2020/11/1
*/

public class FileUtils {
/**
*检查文件大小
* @param file
* @param size
* @param unit
* @return
*/
public static boolean checkFileSize(MultipartFile file,int size,String unit){
long len = file.getSize();
double fileSize = 0;
if ("B".equals(unit.toUpperCase())){
fileSize = len;
}else if ("k".equals(unit.toUpperCase())){
fileSize = (double)len / 1024;
}else if ("M".equals(unit.toUpperCase())){
fileSize = (double)len / 1048576;
}else if ("G".equals(unit.toUpperCase())){
fileSize = (double)len / 1073741824;
}
if (fileSize > size){
return false;
}
return true;
}

}

效果图:

7.png

8.png

测试超过上传要求的文件

9.png

测试多文件上传

10.png

在进行多文件上传的时候,会对每个文件进行校验,不符合上传要求的会直接返回

11.png

测试文件下载就不测试了,这里提供两种方法,第一种就是直接利用IO直接读取文件,进行数据传输,第二种就是直接利用springboot的资源映射配置类,把服务器的文件资源映射到互联网上,直接请求路径即可下载文件

WebMvcConfigurer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//和页面有关的静态目录都放在项目的static目录下
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/static/");
//上传的图片在C盘下的image目录下,访问路径如:https://localhost:443/image/d3cf0281-bb7f-40e0-ab77-406db95ccf2c.jpg
//其中OTA表示访问的前缀。"file:D:/OTA/"是文件真实的存储路径
registry.addResourceHandler("/image/**").addResourceLocations("file:C:/image/");
registry.addResourceHandler("/lost/**").addResourceLocations("file:C:/lost/");
}
}

最后,是对图片上传的测试,这里引入谷歌的Thumbnailator,对图片进行压缩处理,破坏掉嵌入可执行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@RequestMapping("/uploadImage")
public String upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
FileDoc fileDoc = new FileDoc();
try {
if (file.isEmpty()) {
return "文件为空";
}
//获取大小
long size = file.getSize();
log.info("文件大小:" + size);
//判断文件上传大小
if (!FileUtils.checkFileSize(file, 10, "M")) {
log.info("上传文件规定小于10MB");
return "上传文件规定小于10MB";
}
//获取文件名
String filename = file.getOriginalFilename();
log.info("上传的文件名为:" + filename);
//获取文件后缀名
String suffixName = filename.substring(filename.lastIndexOf("."));
log.info("文件的后后缀名:" + suffixName);

//判断文件是否为图片
if (!ImageTypeUtils.checkImageUtils(suffixName)){
return "请上传图片格式为jpg,png,gif的图片";
}

//生成新文件名 6位随机数+文件后缀名
fileDoc.setFile_name(CodeGenerateUtil.generateVerCode(6).toString()+suffixName);
//拿到ip地址
fileDoc.setIp_addr(request.getRemoteAddr());

File dest = new File(FILEPATH+fileDoc.getFile_name());
//检测目录是否存在
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs(); //新建文件夹
}
file.transferTo(dest); //文件写入

//利用Thumbnails对图片进行resize
Thumbnails.of(FILEPATH+fileDoc.getFile_name())
.scale(1f)
.outputQuality(0.7F)
.toFile(FILEPATH+fileDoc.getFile_name());
log.info("图片压缩成功");

int i = fileDocService.uploadFile(fileDoc);
if (i > 0){
return "文件上传成功";
}else {
return "文件上传失败";
}
} catch (IllegalStateException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

return "上传失败";
}

Thumbnails的使用方法

scale: 按照比例进行缩放。范围:0.0~N。
scale(0.5) 宽高比例都是50%缩放 , scale(1,0.5) 宽不变,高为50%缩放

1
2
3
4
5
Thumbnails.of("原图文件的路径")
.scale(1F) //1F不缩放
.outputQuality(0.7F) //图片品质 0-1 靠近1质量越高
.watermark(Positions.BOTTOM_RIGHT, ImageIO.read(水印), 0.5f)
.toFile("转换后文件的路径");

格式的校验

12.png

成功上传后会对图片进行压缩

13.png

自己写的一个简单的Demo,未考虑的安全问题还有很多,对Java初学者应该会很有帮助。

详细代码见Github:

Github项目地址