目录
最近开发的一个项目需要将上传接口放开,做了很多安全性的工作,这个文件类型检测是其中的一环,趁着最近有空,就记录一下。
1.免登陆上传接口安全性风险和建议
免登录上传接口面临各种风险:恶意文件上传、DoS攻击、数据泄露、文件名注入攻击、隐藏文件上传…等。对应的防御策略如过滤和限制文件类型、限制文件大小、避免上传文件被执行、重命名文件、服务器端做入侵检测、上传文件病毒扫描…等,系统的安全需要多方面考虑,并不断优化和完善才行,今天主要介绍tika的使用,就不拓展更多了,有兴趣的朋友可以自行去了解,后面有空可能也会写一写。
2.Apache Tika 介绍
Apache Tika 是一个开源的内容检测和分析框架,由Apache软件基金会开发和维护的顶级项目。它可以从各种格式的文件中提取元数据和文本内容。Tika非常适合处理全文搜索、内容分析、翻译、内容提取等需要大量处理和分析文档内容的任务。
2.1 主要功能
Apache Tika的主要功能:
- 内容检测:通过检查文件内容或文件扩展名,Tika能够准确地判断文件的媒体类型(MIME类型)。
- 元数据提取:Tika能够从各种媒体类型的文件中提取元数据,比如标题、作者、时间戳等。
- 内容提取:Tika能够从文件中提取出文本、图片等内容。
- 语言检测:Tika可以检测文本内容的语言。
- 格式转换:Tika可以将各种格式的文件转换为XHTML内容。
2.2 主要项目
- tika-core: 这是Tika的核心库,提供了Tika API的主要接口和类,例如内容检测、元数据提取、内容提取等。此外,它还提供了一些工具类来处理MIME类型和字符集等。
- tika-parsers: 这个库包含了Tika可以使用的所有解析器。每一个解析器都是针对一种特定的文件格式(如PDF,Word,Excel,PowerPoint,HTML,XML,RTF,EPUB等)。这些解析器依赖于其他一些专门用于处理这些格式的库,如PDFBox,POI等。
- tika-app: 这是一个命令行应用程序,集成了tika-core和tika-parsers的功能,你可以使用它来在命令行中直接处理文件。
- tika-server: 这是一个RESTful服务,提供了Tika的所有功能。你可以在网络上任何地方使用它来处理文件。
- tika-bundle: 这是一个包含了tika-core和tika-parsers的大型jar包,方便你在项目中直接使用Tika的所有功能。
- tika-langdetect: 这个库提供了一种方法来检测文本内容的语言。
- tika-translate: 这个库提供了一种方法来翻译文本内容。
- **tika-dl: **这个库提供了一种方法来使用深度学习模型处理文件。
Tika的项目彼此之间有着紧密的关系。tika-core是所有其他项目的基础,tika-parsers为core提供了具体的解析功能,app、server和bundle等则是提供了不同形式的使用接口。其余的一些项目,如langdetect、translate和dl,则提供了更加特化的功能。
3.整合tika
3.1 pomxml引入依赖
<!-- https://mvnrepository.com/artifact/org.apache.tika/tika-core -->
<!-- 其他版本可自行去maven仓库查找 -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId>
<version>2.8.0</version>
</dependency>
3.2 tika的使用
3.2.1 第一版demo代码
public static void main(String[] args) {
File file = new File("E:\\software\\ab.png");
// 使用Tika检测文件类型
MediaType mediaType;
String mediaTypeExtension;
String extName = getFileExtension(file.getName());
try (TikaInputStream tis = TikaInputStream.get(file)) {
mediaType = TikaConfig.getDefaultConfig().getDetector().detect(tis, new Metadata());
mediaTypeExtension = MimeTypes.getDefaultMimeTypes().forName(mediaType.toString()).getExtension();
mediaTypeExtension = mediaTypeExtension.replace(".", "");
} catch (IOException | MimeTypeException e) {
throw new RuntimeException("Could not read file", e);
}
if (!mediaTypeExtension.equals(extName)) {
log.info("不匹配");
} else {
log.info("匹配");
}
log.info("MediaType>>>>{}",mediaType);
log.info("type>>>>{}",mediaType.getType());
log.info("mediaTypeExtension>>>>{}",mediaTypeExtension);
log.info("extName>>>>{}",extName);
}
private static String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex == -1) {
throw new CheckedException("文件名无扩展名,无法确定文件类型");
}
// 不包含点,所以使用dotIndex + 1
return fileName.substring(dotIndex + 1);
}
运行结果:这里我使用了lombok的注解@Slf4j打印日志,没有引入的直接是java自带的 打印即可,这里的判断是正确的,现在我们直接修改文件的后缀再测试,ab.png修改为ab.gif:
运行结果如下:
校验结果也是正确的,避免了不必要的类型上传到服务器。
但是这里要注意,tika会将魔数值相同的后缀类型标准化,比如jpeg,jif等会被标准化成jpg,修改代码测试:
运行结果:我们可以追溯一下代码:
第一个是框架自带的类型映射,第二个是自定义的类型映射,这个我还没自己动手弄过,因为官方提供的已经够使用了,然后我们打开tika-mimetypes.xml看一下:
而直接获取getExtension的方法如下
所以我们需要调整一下代码,我们看到这个类里还有一个getExtensions方法,获取这一个映射下的所有类型
修改代码:
运行结果:
3.2.2 最终版完整代码
@Slf4j
@UtilityClass
public class FileDetectUtils {
/**
* 文件大小
*/
private static final long MAX_FILE_SIZE = 10485760L;
private static final List<String> MEDIA_TYPE_LIST = Lists.newArrayList("image","video");
private static final Detector DETECTOR = TikaConfig.getDefaultConfig().getDetector();
/**
* 文件检测
* @param file
* @return
*/
public void detect(MultipartFile file) {
// 检查文件大小
if (file.getSize() > MAX_FILE_SIZE) {
throw new CheckedException("上传文件过大,请上传低于" + MAX_FILE_SIZE + "文件");
}
// 检查文件名 以.开头的文件不支持上传
isValidFileName(file.getOriginalFilename());
String extension = getFileExtension(file.getOriginalFilename());
// 使用Tika检测文件类型
MediaType mediaType;
List<String> mediaTypeExtensions;
try (TikaInputStream tis = TikaInputStream.get(file.getBytes())) {
mediaType = DETECTOR.detect(tis, new Metadata());
if (!MEDIA_TYPE_LIST.contains(mediaType.getType())) {
log.error("不支持的文件类型,type:{}", mediaType.getType());
throw new CheckedException("不支持的文件类型");
}
mediaTypeExtensions = MimeTypes.getDefaultMimeTypes().forName(mediaType.toString()).getExtensions();
} catch (IOException e) {
log.error("Could not read file", e);
throw new CheckedException("请上传正确的文件类型");
} catch (MimeTypeException e) {
log.error("Could not get extension for media type", e);
throw new CheckedException("请上传正确的文件类型");
}
// 验证文件类型
if (!mediaTypeExtensions.contains(extension.toLowerCase())) {
throw new CheckedException("文件类型已被修改,不支持上传");
}
// 如果需要,你可以在这里添加更深入的文件内容检查
// 例如,对于PDF文件,你可能希望检查是否包含JavaScript或宏
}
/**
* 校验文件名
* @param fileName
*/
private void isValidFileName(String fileName) {
if (null == fileName) {
throw new CheckedException("文件名不能为空");
}
// 简单的文件名检查,需要根据需求进行修改
if (fileName.startsWith(StrUtil.DOT)) {
throw new CheckedException("不支持以.开头的文件");
}
}
/**
* 因为tika解析出的文件后缀带了. 所以此处不去除
* @param fileName
* @return
*/
private static String getFileExtension(String fileName) {
int dotIndex = fileName.lastIndexOf(".");
if (dotIndex == -1) {
throw new CheckedException("文件名无扩展名,无法确定文件类型");
}
// 包含点,所以使用dotIndex
return fileName.substring(dotIndex);
}
}
a.我在其他地方做了文件重命名,如果你有需要,也可以使用UUID等工具重命名。
b.我这里限定两个主类型,image和video,你也可以使用子类型来限定,调整下代码即可