|
|
@@ -0,0 +1,93 @@
|
|
|
+package com.qqflow.engine.common.controller;
|
|
|
+
|
|
|
+import com.qqflow.engine.domain.flow.mapper.AttachmentMapper;
|
|
|
+import com.qqflow.engine.domain.flow.po.Attachment;
|
|
|
+import com.qqflow.engine.domain.flow.service.ProcessInstanceService;
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.PathVariable;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.nio.file.Paths;
|
|
|
+
|
|
|
+@Slf4j
|
|
|
+@RestController
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class FileDownloadController {
|
|
|
+
|
|
|
+ private static final String UPLOAD_DIR = "uploads";
|
|
|
+
|
|
|
+ private final AttachmentMapper attachmentMapper;
|
|
|
+ private final ProcessInstanceService processInstanceService;
|
|
|
+
|
|
|
+ @GetMapping("/uploads/{date}/{filename}")
|
|
|
+ public void download(@PathVariable String date,
|
|
|
+ @PathVariable String filename,
|
|
|
+ HttpServletRequest request,
|
|
|
+ HttpServletResponse response) throws IOException {
|
|
|
+ // 基本路径校验,防止目录遍历
|
|
|
+ if (!isValidDate(date) || !isValidFilename(filename)) {
|
|
|
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "非法文件路径");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String fileUrl = "/uploads/" + date + "/" + filename;
|
|
|
+ Attachment attachment = this.attachmentMapper.selectByFileUrl(fileUrl);
|
|
|
+ if (attachment == null) {
|
|
|
+ // 不暴露文件是否存在,统一返回 403
|
|
|
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问该文件");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验当前登录用户对所属流程实例的可见性
|
|
|
+ try {
|
|
|
+ this.processInstanceService.checkInstanceVisibility(attachment.getInstanceId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问该文件");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Path filePath = Paths.get(UPLOAD_DIR, date, filename);
|
|
|
+ if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
|
|
+ response.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校验请求路径与真实文件路径一致,防止通过编码绕过
|
|
|
+ Path normalizedPath = filePath.toAbsolutePath().normalize();
|
|
|
+ Path basePath = Paths.get(UPLOAD_DIR).toAbsolutePath().normalize();
|
|
|
+ if (!normalizedPath.startsWith(basePath)) {
|
|
|
+ response.sendError(HttpServletResponse.SC_FORBIDDEN, "非法文件路径");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String contentType = request.getServletContext().getMimeType(filename);
|
|
|
+ if (!StringUtils.hasText(contentType)) {
|
|
|
+ contentType = "application/octet-stream";
|
|
|
+ }
|
|
|
+ response.setContentType(contentType);
|
|
|
+ response.setHeader("Content-Disposition", "inline; filename=\"" + filename + "\"");
|
|
|
+ Files.copy(filePath, response.getOutputStream());
|
|
|
+ response.getOutputStream().flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isValidDate(String date) {
|
|
|
+ return date != null && date.matches("\\d{6}");
|
|
|
+ }
|
|
|
+
|
|
|
+ private boolean isValidFilename(String filename) {
|
|
|
+ return filename != null
|
|
|
+ && !filename.isBlank()
|
|
|
+ && !filename.contains("..")
|
|
|
+ && !filename.contains("/")
|
|
|
+ && !filename.contains("\\")
|
|
|
+ && filename.matches("^[a-f0-9\\-]+\\.[a-zA-Z0-9]+$");
|
|
|
+ }
|
|
|
+}
|