Commit 03ce4fad5766bd85d318b9d6fb4cf8e43dad92d1

Authored by YunaiV
1 parent 223a4d81

feat(infra): 增加文件上传类型白名单校验

yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/AppFileController.java
@@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.Parameters; @@ -12,7 +12,6 @@ import io.swagger.v3.oas.annotations.Parameters;
12 import io.swagger.v3.oas.annotations.media.Schema; 12 import io.swagger.v3.oas.annotations.media.Schema;
13 import io.swagger.v3.oas.annotations.tags.Tag; 13 import io.swagger.v3.oas.annotations.tags.Tag;
14 import jakarta.annotation.Resource; 14 import jakarta.annotation.Resource;
15 -import jakarta.annotation.security.PermitAll;  
16 import jakarta.validation.Valid; 15 import jakarta.validation.Valid;
17 import lombok.extern.slf4j.Slf4j; 16 import lombok.extern.slf4j.Slf4j;
18 import org.springframework.validation.annotation.Validated; 17 import org.springframework.validation.annotation.Validated;
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
@@ -34,6 +34,7 @@ public interface ErrorCodeConstants { @@ -34,6 +34,7 @@ public interface ErrorCodeConstants {
34 ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在"); 34 ErrorCode FILE_NOT_EXISTS = new ErrorCode(1_001_003_001, "文件不存在");
35 ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空"); 35 ErrorCode FILE_IS_EMPTY = new ErrorCode(1_001_003_002, "文件为空");
36 ErrorCode FILE_PATH_INVALID = new ErrorCode(1_001_003_003, "文件路径不正确"); 36 ErrorCode FILE_PATH_INVALID = new ErrorCode(1_001_003_003, "文件路径不正确");
  37 + ErrorCode FILE_TYPE_NOT_ALLOWED = new ErrorCode(1_001_003_004, "不支持的文件类型");
37 38
38 // ========== 代码生成器 1-001-004-000 ========== 39 // ========== 代码生成器 1-001-004-000 ==========
39 ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在"); 40 ErrorCode CODEGEN_TABLE_EXISTS = new ErrorCode(1_001_004_002, "表定义已经存在");
yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java
@@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.infra.service.file; @@ -3,6 +3,8 @@ package cn.iocoder.yudao.module.infra.service.file;
3 import cn.hutool.core.date.LocalDateTimeUtil; 3 import cn.hutool.core.date.LocalDateTimeUtil;
4 import cn.hutool.core.io.FileUtil; 4 import cn.hutool.core.io.FileUtil;
5 import cn.hutool.core.lang.Assert; 5 import cn.hutool.core.lang.Assert;
  6 +import cn.hutool.core.util.ArrayUtil;
  7 +import cn.hutool.core.util.ObjectUtil;
6 import cn.hutool.core.util.RandomUtil; 8 import cn.hutool.core.util.RandomUtil;
7 import cn.hutool.core.util.StrUtil; 9 import cn.hutool.core.util.StrUtil;
8 import cn.hutool.crypto.digest.DigestUtil; 10 import cn.hutool.crypto.digest.DigestUtil;
@@ -23,10 +25,11 @@ import lombok.SneakyThrows; @@ -23,10 +25,11 @@ import lombok.SneakyThrows;
23 import org.springframework.stereotype.Service; 25 import org.springframework.stereotype.Service;
24 26
25 import java.util.List; 27 import java.util.List;
  28 +import java.util.Set;
26 29
27 import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN; 30 import static cn.hutool.core.date.DatePattern.PURE_DATE_PATTERN;
28 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 31 import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
29 -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS; 32 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
30 33
31 /** 34 /**
32 * 文件 Service 实现类 35 * 文件 Service 实现类
@@ -37,6 +40,29 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX @@ -37,6 +40,29 @@ import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EX
37 public class FileServiceImpl implements FileService { 40 public class FileServiceImpl implements FileService {
38 41
39 /** 42 /**
  43 + * 允许上传的文件类型,可按业务需要调整
  44 + */
  45 + private static final Set<String> ALLOWED_FILE_TYPES = Set.of(
  46 + // 图片
  47 + "image/jpeg", "image/png", "image/gif", "image/webp", "image/bmp",
  48 + "image/vnd.microsoft.icon", "image/tiff", "image/avif", "image/heic", "image/heif", "image/svg+xml",
  49 + // 文档
  50 + "application/pdf", "text/plain", "text/csv", "text/markdown", "text/html", "application/xhtml+xml",
  51 + "application/xml", "application/json", "application/epub+zip", "message/rfc822", "application/vnd.ms-outlook",
  52 + "application/msword", "application/vnd.ms-excel", "application/vnd.ms-powerpoint",
  53 + "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
  54 + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
  55 + "application/vnd.openxmlformats-officedocument.presentationml.presentation",
  56 + // 压缩包
  57 + "application/zip", "application/x-rar-compressed", "application/x-7z-compressed", "application/gzip",
  58 + // 音视频
  59 + "audio/mpeg", "audio/vnd.wave", "audio/vorbis", "audio/ogg", "audio/opus", "audio/mp4", "audio/webm",
  60 + "audio/x-aac", "audio/x-flac", "audio/amr", "audio/x-ms-wma",
  61 + "video/mp4", "video/quicktime", "video/x-msvideo", "video/webm", "video/x-ms-wmv", "video/x-flv",
  62 + "application/x-matroska", "video/x-matroska"
  63 + );
  64 +
  65 + /**
40 * 上传文件的前缀,是否包含日期(yyyyMMdd) 66 * 上传文件的前缀,是否包含日期(yyyyMMdd)
41 * 67 *
42 * 目的:按照日期,进行分目录 68 * 目的:按照日期,进行分目录
@@ -74,9 +100,15 @@ public class FileServiceImpl implements FileService { @@ -74,9 +100,15 @@ public class FileServiceImpl implements FileService {
74 // 1.1 处理 name 的合法性,禁止携带目录路径 100 // 1.1 处理 name 的合法性,禁止携带目录路径
75 name = FilePathUtils.validateFileName(name); 101 name = FilePathUtils.validateFileName(name);
76 102
77 - // 1.2.1 处理 type 为空的情况  
78 - if (StrUtil.isEmpty(type)) {  
79 - type = FileTypeUtils.getMineType(content, name); 103 + // 1.2.1 校验文件内容和类型,不信任客户端传入的 type
  104 + if (ArrayUtil.isEmpty(content)) {
  105 + throw exception(FILE_IS_EMPTY);
  106 + }
  107 + type = FileTypeUtils.getMineType(content, name);
  108 + String nameType = StrUtil.isNotEmpty(FileUtil.extName(name)) ? validateFileType(name) : null;
  109 + if (!ALLOWED_FILE_TYPES.contains(type)
  110 + || (FileTypeUtils.isImage(nameType) && ObjectUtil.notEqual(type, nameType))) {
  111 + throw exception(FILE_TYPE_NOT_ALLOWED);
80 } 112 }
81 // 1.2.2 处理 name 为空的情况 113 // 1.2.2 处理 name 为空的情况
82 if (StrUtil.isEmpty(name)) { 114 if (StrUtil.isEmpty(name)) {
@@ -148,8 +180,10 @@ public class FileServiceImpl implements FileService { @@ -148,8 +180,10 @@ public class FileServiceImpl implements FileService {
148 @Override 180 @Override
149 @SneakyThrows 181 @SneakyThrows
150 public FilePresignedUrlRespVO presignPutUrl(String name, String directory) { 182 public FilePresignedUrlRespVO presignPutUrl(String name, String directory) {
151 - // 1. 生成上传的 path,需要保证唯一 183 + // 1.1 生成上传的 path,需要保证唯一
152 String path = generateUploadPath(name, directory); 184 String path = generateUploadPath(name, directory);
  185 + // 1.2 前端直传无法读取文件内容,此处仅校验文件名类型
  186 + validateFileType(name);
153 187
154 // 2. 获取文件预签名地址 188 // 2. 获取文件预签名地址
155 FileClient fileClient = fileConfigService.getMasterFileClient(); 189 FileClient fileClient = fileConfigService.getMasterFileClient();
@@ -170,7 +204,13 @@ public class FileServiceImpl implements FileService { @@ -170,7 +204,13 @@ public class FileServiceImpl implements FileService {
170 // 1.1 校验参数的合法性 204 // 1.1 校验参数的合法性
171 FilePathUtils.validatePath(createReqVO.getPath()); 205 FilePathUtils.validatePath(createReqVO.getPath());
172 createReqVO.setName(FilePathUtils.validateFileName(createReqVO.getName())); 206 createReqVO.setName(FilePathUtils.validateFileName(createReqVO.getName()));
173 - // 1.2 处理 URL 的合法性,移除 URL 中的查询参数(例如签名参数),保证 URL 的唯一性 207 + // 1.2 校验原文件名和存储路径的类型,前端直传不校验文件内容
  208 + String type = validateFileType(createReqVO.getName());
  209 + if (ObjectUtil.notEqual(type, validateFileType(FileUtil.getName(createReqVO.getPath())))) {
  210 + throw exception(FILE_TYPE_NOT_ALLOWED);
  211 + }
  212 + createReqVO.setType(type);
  213 + // 1.3 处理 URL 的合法性,移除 URL 中的查询参数(例如签名参数),保证 URL 的唯一性
174 createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数 214 createReqVO.setUrl(HttpUtils.removeUrlQuery(createReqVO.getUrl())); // 目的:移除私有桶情况下,URL 的签名参数
175 215
176 // 2. 保存到数据库 216 // 2. 保存到数据库
@@ -179,6 +219,14 @@ public class FileServiceImpl implements FileService { @@ -179,6 +219,14 @@ public class FileServiceImpl implements FileService {
179 return file.getId(); 219 return file.getId();
180 } 220 }
181 221
  222 + private String validateFileType(String name) {
  223 + String type = FileTypeUtils.getMineType(name);
  224 + if (!ALLOWED_FILE_TYPES.contains(type)) {
  225 + throw exception(FILE_TYPE_NOT_ALLOWED);
  226 + }
  227 + return type;
  228 + }
  229 +
182 @Override 230 @Override
183 public FileDO getFile(Long id) { 231 public FileDO getFile(Long id) {
184 return validateFileExists(id); 232 return validateFileExists(id);
yudao-module-infra/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java
@@ -7,23 +7,31 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; @@ -7,23 +7,31 @@ import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
7 import cn.iocoder.yudao.framework.test.core.util.AssertUtils; 7 import cn.iocoder.yudao.framework.test.core.util.AssertUtils;
8 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO; 8 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
9 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO; 9 import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePageReqVO;
  10 +import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
10 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO; 11 import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
11 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper; 12 import cn.iocoder.yudao.module.infra.dal.mysql.file.FileMapper;
12 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient; 13 import cn.iocoder.yudao.module.infra.framework.file.core.client.FileClient;
13 import jakarta.annotation.Resource; 14 import jakarta.annotation.Resource;
  15 +import org.apache.poi.hssf.usermodel.HSSFWorkbook;
  16 +import org.apache.poi.ss.usermodel.Workbook;
  17 +import org.apache.poi.xssf.usermodel.XSSFWorkbook;
14 import org.junit.jupiter.api.BeforeEach; 18 import org.junit.jupiter.api.BeforeEach;
15 import org.junit.jupiter.api.Test; 19 import org.junit.jupiter.api.Test;
16 import org.springframework.context.annotation.Import; 20 import org.springframework.context.annotation.Import;
17 import org.springframework.test.context.bean.override.mockito.MockitoBean; 21 import org.springframework.test.context.bean.override.mockito.MockitoBean;
18 22
  23 +import javax.imageio.ImageIO;
  24 +import java.awt.image.BufferedImage;
  25 +import java.io.ByteArrayOutputStream;
  26 +import java.nio.charset.StandardCharsets;
19 import java.time.LocalDateTime; 27 import java.time.LocalDateTime;
  28 +import java.util.Locale;
20 import java.util.concurrent.atomic.AtomicReference; 29 import java.util.concurrent.atomic.AtomicReference;
21 30
22 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; 31 import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
23 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException; 32 import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
24 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*; 33 import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
25 -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_NOT_EXISTS;  
26 -import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.FILE_PATH_INVALID; 34 +import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
27 import static org.junit.jupiter.api.Assertions.*; 35 import static org.junit.jupiter.api.Assertions.*;
28 import static org.mockito.ArgumentMatchers.same; 36 import static org.mockito.ArgumentMatchers.same;
29 import static org.mockito.Mockito.*; 37 import static org.mockito.Mockito.*;
@@ -147,6 +155,198 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -147,6 +155,198 @@ public class FileServiceImplTest extends BaseDbUnitTest {
147 } 155 }
148 156
149 @Test 157 @Test
  158 + public void testCreateFile_success_image() throws Exception {
  159 + for (String extension : new String[]{"jpg", "png", "gif", "bmp"}) {
  160 + // 准备参数
  161 + String type = "jpg".equals(extension) ? "image/jpeg" : "image/" + extension;
  162 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
  163 + assertTrue(ImageIO.write(new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB), extension, outputStream));
  164 + byte[] content = outputStream.toByteArray();
  165 + String name = "头像." + extension.toUpperCase(Locale.ROOT);
  166 + // mock 方法
  167 + FileClient client = mock(FileClient.class);
  168 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  169 + String url = randomString();
  170 + when(client.upload(same(content), anyString(), eq(type))).thenReturn(url);
  171 +
  172 + // 调用
  173 + String result = fileService.createFile(content, name, null, "application/octet-stream");
  174 + // 断言
  175 + assertEquals(url, result);
  176 + FileDO file = fileMapper.selectOne(FileDO::getUrl, url);
  177 + assertEquals(name, file.getName());
  178 + assertEquals(type, file.getType());
  179 + }
  180 + }
  181 +
  182 + @Test
  183 + public void testCreateFile_success_excel() throws Exception {
  184 + for (String extension : new String[]{"xls", "xlsx"}) {
  185 + // 准备参数
  186 + String type = "xls".equals(extension) ? "application/vnd.ms-excel"
  187 + : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
  188 + byte[] content;
  189 + try (Workbook workbook = "xls".equals(extension) ? new HSSFWorkbook() : new XSSFWorkbook();
  190 + ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
  191 + workbook.createSheet("用户").createRow(0).createCell(0).setCellValue("芋道源码");
  192 + workbook.write(outputStream);
  193 + content = outputStream.toByteArray();
  194 + }
  195 + // mock 方法
  196 + FileClient client = mock(FileClient.class);
  197 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  198 + String url = randomString();
  199 + when(client.upload(same(content), anyString(), eq(type))).thenReturn(url);
  200 +
  201 + // 调用
  202 + String result = fileService.createFile(content, "用户." + extension, null, null);
  203 + // 断言
  204 + assertEquals(url, result);
  205 + assertEquals(type, fileMapper.selectOne(FileDO::getUrl, url).getType());
  206 + }
  207 + }
  208 +
  209 + @Test
  210 + public void testCreateFile_success_detectType() throws Exception {
  211 + for (String declaredType : new String[]{null, "", "image/jpg", "text/html", "application/octet-stream"}) {
  212 + // 准备参数
  213 + byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
  214 + // mock 方法
  215 + FileClient client = mock(FileClient.class);
  216 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  217 + String url = randomString();
  218 + when(client.upload(same(content), anyString(), eq("image/jpeg"))).thenReturn(url);
  219 +
  220 + // 调用
  221 + String result = fileService.createFile(content, "头像.jpg", null, declaredType);
  222 + // 断言
  223 + assertEquals(url, result);
  224 + assertEquals("image/jpeg", fileMapper.selectOne(FileDO::getUrl, url).getType());
  225 + }
  226 + }
  227 +
  228 + @Test
  229 + public void testCreateFile_success_document() throws Exception {
  230 + for (String[] document : new String[][]{
  231 + {"test.svg", "<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>", "image/svg+xml"},
  232 + {"test.html", "<html><script>alert(1)</script></html>", "text/html"},
  233 + {"test.xml", "<?xml version=\"1.0\"?><root>test</root>", "application/xml"},
  234 + {"test.markdown", "# test\n\nhello", "text/markdown"},
  235 + {"test.eml", "From: a@example.com\r\nTo: b@example.com\r\nSubject: test\r\n\r\nhello", "message/rfc822"}}) {
  236 + // 准备参数
  237 + String name = document[0];
  238 + byte[] content = document[1].getBytes(StandardCharsets.UTF_8);
  239 + String type = document[2];
  240 + // mock 方法
  241 + FileClient client = mock(FileClient.class);
  242 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  243 + String url = randomString();
  244 + when(client.upload(same(content), anyString(), eq(type))).thenReturn(url);
  245 +
  246 + // 调用
  247 + String result = fileService.createFile(content, name, null, "application/octet-stream");
  248 + // 断言
  249 + assertEquals(url, result);
  250 + assertEquals(type, fileMapper.selectOne(FileDO::getUrl, url).getType());
  251 + }
  252 + }
  253 +
  254 + @Test
  255 + public void testCreateFile_success_opus() throws Exception {
  256 + // 准备参数
  257 + byte[] content = new byte[64];
  258 + System.arraycopy("OggS".getBytes(StandardCharsets.US_ASCII), 0, content, 0, 4);
  259 + System.arraycopy("OpusHead".getBytes(StandardCharsets.US_ASCII), 0, content, 28, 8);
  260 + // mock 方法
  261 + FileClient client = mock(FileClient.class);
  262 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  263 + String url = randomString();
  264 + when(client.upload(same(content), anyString(), eq("audio/opus"))).thenReturn(url);
  265 +
  266 + // 调用
  267 + String result = fileService.createFile(content, "voice.ogg", null, "audio/ogg;codecs=opus");
  268 + // 断言
  269 + assertEquals(url, result);
  270 + assertEquals("audio/opus", fileMapper.selectOne(FileDO::getUrl, url).getType());
  271 + }
  272 +
  273 + @Test
  274 + public void testCreateFile_typeNotAllowed_fakeImage() {
  275 + for (String text : new String[]{"<html><script>alert(1)</script></html>",
  276 + "<svg xmlns=\"http://www.w3.org/2000/svg\"><script>alert(1)</script></svg>", "普通文本"}) {
  277 + // 准备参数
  278 + byte[] content = text.getBytes(StandardCharsets.UTF_8);
  279 +
  280 + // 调用,并断言异常
  281 + assertServiceException(() -> fileService.createFile(content, "头像.jpg", null, "image/jpeg"),
  282 + FILE_TYPE_NOT_ALLOWED);
  283 + // 断言未上传文件、未保存记录
  284 + verifyNoInteractions(fileConfigService);
  285 + assertEquals(0L, fileMapper.selectCount());
  286 + }
  287 + }
  288 +
  289 + @Test
  290 + public void testCreateFile_typeNotAllowed_extension() {
  291 + for (String name : new String[]{"头像.svg", "头像.js", "头像.exe", "头像.unknown", "头像.png"}) {
  292 + // 准备参数
  293 + byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
  294 +
  295 + // 调用,并断言异常
  296 + assertServiceException(() -> fileService.createFile(content, name, null, "image/jpeg"), FILE_TYPE_NOT_ALLOWED);
  297 + // 校验调用
  298 + verifyNoInteractions(fileConfigService);
  299 + }
  300 + }
  301 +
  302 + @Test
  303 + public void testCreateFile_fileIsEmpty() {
  304 + // 准备参数
  305 + byte[] content = new byte[0];
  306 +
  307 + // 调用,并断言异常
  308 + assertServiceException(() -> fileService.createFile(content, "头像.jpg", null, null), FILE_IS_EMPTY);
  309 + // 校验调用
  310 + verifyNoInteractions(fileConfigService);
  311 + }
  312 +
  313 + @Test
  314 + public void testPresignPutUrl_success() {
  315 + for (String extension : new String[]{"jpg", "JPEG", "jfif", "png", "gif", "webp", "bmp", "ico", "tiff",
  316 + "avif", "heic", "heif", "pdf", "doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "csv", "md",
  317 + "zip", "rar", "mp3", "mp4", "svg", "html", "htm", "xml", "markdown", "eml", "msg", "epub",
  318 + "json", "opus", "ogg", "m4a", "webm", "amr", "wma", "wmv", "flv", "mkv"}) {
  319 + // 准备参数
  320 + String name = "测试文件." + extension;
  321 + // mock 方法
  322 + FileClient client = mock(FileClient.class);
  323 + when(fileConfigService.getMasterFileClient()).thenReturn(client);
  324 + when(client.getId()).thenReturn(10L);
  325 + when(client.presignPutUrl(anyString())).thenReturn("upload-url");
  326 + when(client.presignGetUrl(anyString(), isNull())).thenReturn("visit-url");
  327 +
  328 + // 调用
  329 + FilePresignedUrlRespVO result = fileService.presignPutUrl(name, "avatar");
  330 + // 断言
  331 + assertEquals(10L, result.getConfigId());
  332 + assertTrue(result.getPath().endsWith("/" + name));
  333 + assertEquals("upload-url", result.getUploadUrl());
  334 + assertEquals("visit-url", result.getUrl());
  335 + }
  336 + }
  337 +
  338 + @Test
  339 + public void testPresignPutUrl_typeNotAllowed() {
  340 + // 准备参数
  341 + for (String name : new String[]{"test.php", "test.jsp", "test.js", "test.exe", "test.unknown", "test"}) {
  342 + // 调用,并断言异常
  343 + assertServiceException(() -> fileService.presignPutUrl(name, null), FILE_TYPE_NOT_ALLOWED);
  344 + // 校验调用
  345 + verifyNoInteractions(fileConfigService);
  346 + }
  347 + }
  348 +
  349 + @Test
150 public void testDeleteFile_success() throws Exception { 350 public void testDeleteFile_success() throws Exception {
151 // mock 数据 351 // mock 数据
152 FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg")); 352 FileDO dbFile = randomPojo(FileDO.class, o -> o.setConfigId(10L).setPath("tudou.jpg"));
@@ -245,6 +445,27 @@ public class FileServiceImplTest extends BaseDbUnitTest { @@ -245,6 +445,27 @@ public class FileServiceImplTest extends BaseDbUnitTest {
245 assertEquals("avatar/test.jpg", file.getPath()); 445 assertEquals("avatar/test.jpg", file.getPath());
246 assertEquals("test.jpg", file.getName()); 446 assertEquals("test.jpg", file.getName());
247 assertEquals("https://www.iocoder.cn/test.jpg", file.getUrl()); 447 assertEquals("https://www.iocoder.cn/test.jpg", file.getUrl());
  448 + assertEquals("image/jpeg", file.getType());
  449 + }
  450 +
  451 + @Test
  452 + public void testCreateFileByPresignedPath_typeNotAllowed() {
  453 + for (String[] file : new String[][]{{"test.js", "avatar/test.js"}, {"test.jpg", "avatar/test.html"},
  454 + {"test.html", "avatar/test.jpg"}, {"test.jpg", "avatar/test.png"}}) {
  455 + // 准备参数
  456 + String name = file[0];
  457 + String path = file[1];
  458 + FileCreateReqVO reqVO = randomPojo(FileCreateReqVO.class, o -> {
  459 + o.setName(name);
  460 + o.setPath(path);
  461 + o.setType("image/jpeg");
  462 + });
  463 +
  464 + // 调用,并断言异常
  465 + assertServiceException(() -> fileService.createFile(reqVO), FILE_TYPE_NOT_ALLOWED);
  466 + // 断言未保存记录
  467 + assertEquals(0L, fileMapper.selectCount());
  468 + }
248 } 469 }
249 470
250 @Test 471 @Test
yudao-module-infra/src/test/resources/sql/create_tables.sql
@@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS &quot;infra_file&quot; ( @@ -37,7 +37,7 @@ CREATE TABLE IF NOT EXISTS &quot;infra_file&quot; (
37 "name" varchar(256), 37 "name" varchar(256),
38 "path" varchar(512), 38 "path" varchar(512),
39 "url" varchar(1024), 39 "url" varchar(1024),
40 - "type" varchar(63) DEFAULT NULL, 40 + "type" varchar(128) DEFAULT NULL,
41 "size" bigint NOT NULL, 41 "size" bigint NOT NULL,
42 "creator" varchar(64) DEFAULT '', 42 "creator" varchar(64) DEFAULT '',
43 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, 43 "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,