为了安全,有时我们需要限制前端上传文件的类型,这个功能可以结合Spring的拦截器和Hutool的文件类型判断来完成。
我们实现如下功能:
- 整个项目默认仅允许一些常见文件类型的上传,比如xlsx等
- 如果某个接口有具体要求,还可以在接口级指定该接口额外允许上传的文件类型
首先,我们需要加载hutool包:
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<!-- 版本号去官网找最新的就行 -->
<version>5.8.27</version>
</dependency>
下面是代码,实现了上面的功能:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AdditionAllowUploadFileTypes {
String[] value() default "";
}
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Lists;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
/**
* 默认支持上传:
* 1. 文档类: "xlsx", "xls", "docx", "doc", "pdf", "txt"
* 2. 图片/视频类: "jpg", "jpeg", "png", "bmp", "mp4"
* 3. 压缩包:zip
*
* @author
*/
@ConfigurationProperties(prefix = "myservice.filetype")
@Data
public class FileTypeProperties {
private static final Logger LOGGER = LoggerFactory.getLogger(FileTypeProperties.class);
/**
* 默认允许的文件类型
*
* @see cn.hutool.core.io.FileTypeUtil
*/
private List<String> defaultAllowTypes = Lists.newArrayList(
"xlsx", "xls", "docx", "doc", "pdf", "txt",
"jpg", "jpeg", "png", "bmp", "mp4",
"zip");
/**
* 自定义支持的扩展类型
*
* @see cn.hutool.core.io.FileTypeUtil
*/
private List<String> additionalAllowTypes;
private Set<String> allowedTypes;
@PostConstruct
public void preSetAllowedTypes() {
allowedTypes = new HashSet<>();
if (CollectionUtil.isNotEmpty(defaultAllowTypes)) {
for (String type : defaultAllowTypes) {
if (StrUtil.isNotBlank(type)) {
allowedTypes.add(type.toLowerCase(Locale.ROOT));
}
}
}
if (CollectionUtil.isNotEmpty(additionalAllowTypes)) {
for (String type : additionalAllowTypes) {
if (StrUtil.isNotBlank(type)) {
allowedTypes.add(type.toLowerCase(Locale.ROOT));
}
}
}
LOGGER.info("File Allowed Types 初始化完成!");
}
}
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileTypeUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.http.HttpStatus;
import com.google.common.collect.Lists;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.NonNull;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
/**
* 限制上传文件类型
*
* @author
*/
public class FileTypeFilter implements HandlerInterceptor {
private final FileTypeProperties fileTypeProperties;
public FileTypeFilter(FileTypeProperties fileTypeProperties) {
this.fileTypeProperties = fileTypeProperties;
}
@Override
public boolean preHandle(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull Object handler) throws Exception {
if (!(request instanceof MultipartHttpServletRequest)) {
return true;
}
Map<String, MultipartFile> allFiles = ((MultipartHttpServletRequest) request).getFileMap();
List<String> notAllowedFiles = getNotAllowedFiles(handler, allFiles);
if (CollUtil.isNotEmpty(notAllowedFiles)) {
String message = CharSequenceUtil.format("不被允许的文件类型!不被允许的文件列表如下:{}", notAllowedFiles);
ServletUtil.write(response, message, MediaType.TEXT_PLAIN_VALUE));
return false;
}
return true;
}
private List<String> getNotAllowedFiles(Object handler, Map<String, MultipartFile> allFiles) throws IOException {
List<String> notAllowedFilenames = null;
for (MultipartFile file : allFiles.values()) {
if (!isAllowedFile(file, handler)) {
if (notAllowedFilenames == null) {
notAllowedFilenames = new ArrayList<>();
}
notAllowedFilenames.add(file.getOriginalFilename());
}
}
return notAllowedFilenames;
}
boolean isAllowedFile(MultipartFile file, Object handler) throws IOException {
String fileName = file.getOriginalFilename();
try (InputStream in = file.getInputStream()) {
String type = FileTypeUtil.getType(in, fileName);
if (StringUtils.isBlank(type)) {
// 无法通过文件头或者扩展名识别文件类型。
return false;
}
String extType = FileUtil.extName(fileName);
if (StringUtils.isBlank(extType)) {
// 后缀读不到类型,直接拒绝
return false;
}
// 文件头类型和后缀类型都要满足白名单
if (CollectionUtils.containsAll(fileTypeProperties.getAllowedTypes(),
Lists.newArrayList(type.toLowerCase(Locale.ROOT), extType.toLowerCase(Locale.ROOT)))) {
return true;
}
String[] additionAllowUploadFileTypes = getAdditionAllowUploadFileTypes(handler);
return Arrays.stream(additionAllowUploadFileTypes)
.anyMatch(type::equalsIgnoreCase) &&
Arrays.stream(additionAllowUploadFileTypes)
.anyMatch(extType::equalsIgnoreCase);
}
}
private String[] getAdditionAllowUploadFileTypes(Object handle) {
String[] res = new String[0];
if (!(handle instanceof HandlerMethod)) {
return res;
}
HandlerMethod handlerMethod = (HandlerMethod) handle;
if (handlerMethod.hasMethodAnnotation(AdditionAllowUploadFileTypes.class)) {
AdditionAllowUploadFileTypes types = handlerMethod.getMethodAnnotation(AdditionAllowUploadFileTypes.class);
if (types != null) {
String[] allowedTypes = types.value();
if (allowedTypes != null) {
res = allowedTypes;
}
}
}
return res;
}
}
使用方法:
项目中可以在application.yaml中指定项目级别额外允许的文件类型
- 默认允许的类型:
"xlsx", "xls", "docx", "doc", "pdf", "txt", "jpg", "jpeg", "png", "bmp", "mp4","zip"
,可以通过myservice.filetype.default-allow-types
变量来覆盖 - 如果觉得上面的类型没问题,只是想新增几个韦德类型,可以使用下面的配置:
myservice:
filetype:
additional-allow-types:
- xxx
- 接口级的可以在接口上使用注解
@AdditionAllowUploadFileTypes("xxx")
来额外允许指定类型的文件上传。