在 Java Web 應用中實現圖片上傳的多種方式

🌏 Read the English version

為什麼需要理解不同的圖片上傳方式

1. 專案需求與技術棧匹配

選擇適合的圖片上傳方式直接影響開發效率與維護成本。不同的 Java Web 框架提供不同的檔案處理機制:

  • Spring Boot 專案:直接使用 MultipartFile 可節省 50% 以上的開發時間
  • 傳統 Servlet 專案:使用原生 API 避免引入不必要的依賴
  • RESTful API 服務:JAX-RS 提供標準化的處理方式,便於團隊協作
  • 微服務架構:需考慮分散式檔案儲存與同步問題

2. 效能與擴展性考量

不同實作方式的效能差異可達 3-5 倍:

  • 記憶體使用:傳統方式可能將整個檔案載入記憶體,大檔案會造成 OutOfMemoryError
  • 並發處理:Vert.x 的事件驅動模型可支援 10,000+ 並發連線
  • 檔案大小限制:Spring Boot 預設限制 1MB,需根據需求調整
  • 磁碟 I/O 優化:使用串流方式可減少 70% 的記憶體消耗

3. 安全性與合規要求

圖片上傳是常見的安全漏洞來源,選擇正確的實作方式可降低風險:

  • 檔案類型驗證:防止上傳惡意腳本偽裝成圖片
  • 檔案大小限制:防止 DoS 攻擊(惡意上傳大檔案耗盡磁碟空間)
  • 路徑遍歷攻擊:驗證檔名,防止 ../../etc/passwd 類型的攻擊
  • 病毒掃描:整合防毒軟體 API 檢查上傳檔案

在 Java Web 應用中實現圖片上傳的多種方式

在 Java Web 應用程式中,實現圖片上傳是常見的需求。本文將介紹五種常見且有效的圖片上傳方法,並按照推薦順序排列,確保每種方法不重複。我們將探討使用 Spring Boot、Servlet、JAX-RS with Jersey、Apache Commons FileUpload 和 Vert.x 來實現這一功能。


技術方案比較

技術方案 適用場景 學習曲線 效能 推薦指數
Spring Boot 現代 Web 應用、微服務 ⭐⭐⭐⭐⭐
Servlet 傳統專案、學習基礎 ⭐⭐⭐
JAX-RS RESTful API、企業應用 ⭐⭐⭐⭐
Apache Commons 大檔案、複雜需求 ⭐⭐⭐⭐
Vert.x 高並發、即時應用 極高 ⭐⭐⭐⭐

1. 使用 Spring Boot 實現圖片上傳

Spring Boot 提供了簡單且強大的檔案上傳功能。以下示例展示了如何使用 Spring Boot 實現圖片上傳。

完整範例程式碼

@RestController
@RequestMapping("/api/files")
public class FileUploadController {

    @Value("${file.upload-dir}")
    private String uploadDir;

    @PostMapping("/upload")
    public ResponseEntity<Map<String, Object>> handleFileUpload(
            @RequestParam("file") MultipartFile file) {

        Map<String, Object> response = new HashMap<>();

        try {
            // 驗證檔案
            if (file.isEmpty()) {
                response.put("success", false);
                response.put("message", "請選擇檔案");
                return ResponseEntity.badRequest().body(response);
            }

            // 驗證檔案類型
            String contentType = file.getContentType();
            if (!isImageFile(contentType)) {
                response.put("success", false);
                response.put("message", "僅支援圖片格式");
                return ResponseEntity.badRequest().body(response);
            }

            // 生成唯一檔名
            String originalFilename = file.getOriginalFilename();
            String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
            String filename = UUID.randomUUID().toString() + extension;

            // 儲存檔案
            Path path = Paths.get(uploadDir, filename);
            Files.createDirectories(path.getParent());
            Files.write(path, file.getBytes());

            response.put("success", true);
            response.put("filename", filename);
            response.put("url", "/uploads/" + filename);
            response.put("size", file.getSize());

            return ResponseEntity.ok(response);

        } catch (IOException e) {
            response.put("success", false);
            response.put("message", "檔案上傳失敗:" + e.getMessage());
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
        }
    }

    private boolean isImageFile(String contentType) {
        return contentType != null && contentType.startsWith("image/");
    }
}

application.yml 設定

spring:
  servlet:
    multipart:
      max-file-size: 10MB        # 單個檔案最大 10MB
      max-request-size: 50MB     # 整個請求最大 50MB
      enabled: true

file:
  upload-dir: /var/uploads/images

優缺點分析

優點:

  • 簡單易用,整合 Spring 的其他功能(如依賴注入、AOP)
  • 提供良好的錯誤處理和回應機制
  • 易於擴展和維護,支援自動配置
  • 完整的文件和社群支援
  • 內建檔案大小限制與類型驗證

缺點:

  • 需要學習和配置 Spring Boot
  • 依賴於 Spring 框架,增加了一定的複雜性
  • 啟動時間較長(約 3-5 秒)

推薦原因:

Spring Boot 是現代 Java 開發的主流框架之一,簡化了配置和開發過程,適合大多數應用場景,特別是需要快速開發和部署的專案。


2. 使用 Servlet 實現圖片上傳

Servlet 是 Java Web 應用的基礎元件,可以直接處理檔案上傳請求。以下示例展示了如何使用 javax.servlet.http.Part 來處理檔案上傳。

完整範例程式碼

@WebServlet("/upload")
@MultipartConfig(
    maxFileSize = 10485760,      // 10MB
    maxRequestSize = 52428800,   // 50MB
    fileSizeThreshold = 1048576  // 1MB
)
public class FileUploadServlet extends HttpServlet {

    private static final String UPLOAD_DIR = "/var/uploads/images";

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        try {
            Part filePart = request.getPart("file");

            if (filePart == null || filePart.getSize() == 0) {
                sendError(response, "請選擇檔案");
                return;
            }

            // 驗證檔案類型
            String contentType = filePart.getContentType();
            if (!contentType.startsWith("image/")) {
                sendError(response, "僅支援圖片格式");
                return;
            }

            // 取得檔名
            String fileName = Paths.get(filePart.getSubmittedFileName())
                                   .getFileName()
                                   .toString();
            String extension = fileName.substring(fileName.lastIndexOf("."));
            String newFileName = UUID.randomUUID().toString() + extension;

            // 儲存檔案
            File uploadDir = new File(UPLOAD_DIR);
            if (!uploadDir.exists()) {
                uploadDir.mkdirs();
            }

            File file = new File(uploadDir, newFileName);
            try (InputStream input = filePart.getInputStream()) {
                Files.copy(input, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
            }

            // 回傳成功訊息
            sendSuccess(response, newFileName, filePart.getSize());

        } catch (Exception e) {
            sendError(response, "檔案上傳失敗:" + e.getMessage());
        }
    }

    private void sendSuccess(HttpServletResponse response, String filename, long size)
            throws IOException {
        String json = String.format(
            "{"success":true,"filename":"%s","url":"/uploads/%s","size":%d}",
            filename, filename, size
        );
        response.getWriter().write(json);
    }

    private void sendError(HttpServletResponse response, String message)
            throws IOException {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        String json = String.format("{"success":false,"message":"%s"}", message);
        response.getWriter().write(json);
    }
}

優缺點分析

優點:

  • 較低的技術門檻,容易理解
  • 無需額外的依賴,純 Java EE 實現
  • 直接控制每個步驟,學習底層原理
  • 啟動速度快,資源消耗低

缺點:

  • 功能較為基礎,缺乏現代框架的便捷功能
  • 錯誤處理和擴展性較差
  • 需要手動處理 JSON 序列化
  • 缺少依賴注入等現代開發特性

推薦原因:

適合小型項目或需要直接控制和理解每個步驟的場景。適合學習和理解 Java Web 開發的基礎知識。


3. 使用 JAX-RS with Jersey 實現圖片上傳

JAX-RS 是用於構建 RESTful Web 服務的 Java API,Jersey 是其參考實現。以下示例展示了如何使用 JAX-RS 和 Jersey 來處理檔案上傳。

完整範例程式碼

@Path("/upload")
public class FileUploadService {

    private static final String UPLOAD_DIR = "/var/uploads/images";
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

    @POST
    @Path("/file")
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Produces(MediaType.APPLICATION_JSON)
    public Response uploadFile(
            @FormDataParam("file") InputStream uploadedInputStream,
            @FormDataParam("file") FormDataContentDisposition fileDetail) {

        try {
            // 驗證檔案
            if (uploadedInputStream == null || fileDetail == null) {
                return buildErrorResponse("請選擇檔案");
            }

            // 驗證檔案類型
            String fileName = fileDetail.getFileName();
            if (!isImageFile(fileName)) {
                return buildErrorResponse("僅支援圖片格式");
            }

            // 生成新檔名
            String extension = fileName.substring(fileName.lastIndexOf("."));
            String newFileName = UUID.randomUUID().toString() + extension;
            String uploadedFileLocation = UPLOAD_DIR + "/" + newFileName;

            // 儲存檔案
            Files.createDirectories(Paths.get(UPLOAD_DIR));
            long fileSize = Files.copy(
                uploadedInputStream,
                Paths.get(uploadedFileLocation),
                StandardCopyOption.REPLACE_EXISTING
            );

            // 檢查檔案大小
            if (fileSize > MAX_FILE_SIZE) {
                Files.delete(Paths.get(uploadedFileLocation));
                return buildErrorResponse("檔案大小超過限制(最大 10MB)");
            }

            // 建立回應
            Map<String, Object> result = new HashMap<>();
            result.put("success", true);
            result.put("filename", newFileName);
            result.put("url", "/uploads/" + newFileName);
            result.put("size", fileSize);

            return Response.ok(result).build();

        } catch (IOException e) {
            return buildErrorResponse("檔案上傳失敗:" + e.getMessage());
        }
    }

    private boolean isImageFile(String fileName) {
        String extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase();
        return extension.matches("jpg|jpeg|png|gif|bmp|webp");
    }

    private Response buildErrorResponse(String message) {
        Map<String, Object> error = new HashMap<>();
        error.put("success", false);
        error.put("message", message);
        return Response.status(400).entity(error).build();
    }
}

優缺點分析

優點:

  • 適合構建 RESTful 服務,標準化 API 設計
  • 與其他 JAX-RS 功能良好集成
  • 支持多種數據格式和內容協商
  • 自動 JSON 序列化/反序列化
  • 良好的測試支援(Jersey Test Framework)

缺點:

  • 配置和學習曲線較陡
  • 需要額外的依賴(如 Jersey、Jackson)
  • 文件較為分散,學習資源較少

推薦原因:

適合需要構建 RESTful API 的應用,提供了豐富的功能和靈活性,適合中大型項目。


4. 使用 Apache Commons FileUpload 實現圖片上傳

Apache Commons FileUpload 庫提供了一種便捷的方法來處理檔案上傳。以下示例展示了如何使用該庫來處理檔案上傳。

完整範例程式碼

@WebServlet("/upload")
public class FileUploadServlet extends HttpServlet {

    private static final String UPLOAD_DIR = "/var/uploads/images";
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB
    private static final long MAX_REQUEST_SIZE = 50 * 1024 * 1024; // 50MB

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("application/json");
        response.setCharacterEncoding("UTF-8");

        boolean isMultipart = ServletFileUpload.isMultipartContent(request);

        if (!isMultipart) {
            sendError(response, "請使用 multipart/form-data 格式");
            return;
        }

        // 設定上傳參數
        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setSizeThreshold(1024 * 1024); // 1MB
        factory.setRepository(new File(System.getProperty("java.io.tmpdir")));

        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setFileSizeMax(MAX_FILE_SIZE);
        upload.setSizeMax(MAX_REQUEST_SIZE);

        try {
            List<FileItem> items = upload.parseRequest(request);

            for (FileItem item : items) {
                if (!item.isFormField()) {
                    // 驗證檔案類型
                    String contentType = item.getContentType();
                    if (!contentType.startsWith("image/")) {
                        sendError(response, "僅支援圖片格式");
                        return;
                    }

                    // 生成新檔名
                    String fileName = item.getName();
                    String extension = fileName.substring(fileName.lastIndexOf("."));
                    String newFileName = UUID.randomUUID().toString() + extension;

                    // 儲存檔案
                    File uploadDir = new File(UPLOAD_DIR);
                    if (!uploadDir.exists()) {
                        uploadDir.mkdirs();
                    }

                    File file = new File(uploadDir, newFileName);
                    item.write(file);

                    // 回傳成功訊息
                    sendSuccess(response, newFileName, item.getSize());
                    return;
                }
            }

            sendError(response, "請選擇檔案");

        } catch (FileUploadException e) {
            sendError(response, "檔案上傳失敗:" + e.getMessage());
        } catch (Exception e) {
            sendError(response, "處理失敗:" + e.getMessage());
        }
    }

    private void sendSuccess(HttpServletResponse response, String filename, long size)
            throws IOException {
        String json = String.format(
            "{"success":true,"filename":"%s","url":"/uploads/%s","size":%d}",
            filename, filename, size
        );
        response.getWriter().write(json);
    }

    private void sendError(HttpServletResponse response, String message)
            throws IOException {
        response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
        String json = String.format("{"success":false,"message":"%s"}", message);
        response.getWriter().write(json);
    }
}

優缺點分析

優點:

  • 功能強大,支持大文件上傳(可處理 GB 級檔案)
  • 提供豐富的配置選項和擴展性
  • 支援檔案大小限制、進度監控
  • 記憶體友善,自動使用磁碟暫存大檔案
  • 成熟穩定,廣泛應用於企業級專案

缺點:

  • 需要額外的依賴(Apache Commons FileUpload)
  • 配置和使用較為複雜
  • API 較為老舊,不如現代框架直觀

推薦原因:

適合需要處理大文件或複雜上傳場景的應用,提供了豐富的配置選項和擴展性。


5. 使用 Vert.x 實現圖片上傳

Vert.x 是一個事件驅動的工具集,可以用來處理檔案上傳。以下示例展示了如何使用 Vert.x 來處理檔案上傳。

完整範例程式碼

public class FileUploadServer extends AbstractVerticle {

    private static final String UPLOAD_DIR = "/var/uploads/images";
    private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB

    @Override
    public void start() {
        Router router = Router.router(vertx);

        // 設定檔案上傳處理器
        router.route().handler(BodyHandler.create()
            .setUploadsDirectory(UPLOAD_DIR)
            .setBodyLimit(MAX_FILE_SIZE));

        // 處理檔案上傳
        router.post("/upload").handler(routingContext -> {
            Set<FileUpload> uploads = routingContext.fileUploads();

            if (uploads.isEmpty()) {
                sendError(routingContext, "請選擇檔案");
                return;
            }

            FileUpload upload = uploads.iterator().next();

            // 驗證檔案類型
            String contentType = upload.contentType();
            if (!contentType.startsWith("image/")) {
                // 刪除已上傳的檔案
                vertx.fileSystem().delete(upload.uploadedFileName(), result -> {});
                sendError(routingContext, "僅支援圖片格式");
                return;
            }

            // 生成新檔名
            String originalFileName = upload.fileName();
            String extension = originalFileName.substring(originalFileName.lastIndexOf("."));
            String newFileName = UUID.randomUUID().toString() + extension;
            String newFilePath = UPLOAD_DIR + "/" + newFileName;

            // 移動檔案到最終位置
            vertx.fileSystem().move(upload.uploadedFileName(), newFilePath, result -> {
                if (result.succeeded()) {
                    // 取得檔案大小
                    vertx.fileSystem().props(newFilePath, props -> {
                        long fileSize = props.result().size();
                        sendSuccess(routingContext, newFileName, fileSize);
                    });
                } else {
                    sendError(routingContext, "檔案儲存失敗");
                }
            });
        });

        // 啟動 HTTP 伺服器
        vertx.createHttpServer()
            .requestHandler(router)
            .listen(8080, http -> {
                if (http.succeeded()) {
                    System.out.println("HTTP server started on port 8080");
                } else {
                    System.err.println("Failed to start HTTP server");
                }
            });
    }

    private void sendSuccess(RoutingContext context, String filename, long size) {
        JsonObject response = new JsonObject()
            .put("success", true)
            .put("filename", filename)
            .put("url", "/uploads/" + filename)
            .put("size", size);

        context.response()
            .putHeader("content-type", "application/json")
            .end(response.encode());
    }

    private void sendError(RoutingContext context, String message) {
        JsonObject response = new JsonObject()
            .put("success", false)
            .put("message", message);

        context.response()
            .setStatusCode(400)
            .putHeader("content-type", "application/json")
            .end(response.encode());
    }
}

優缺點分析

優點:

  • 高性能,適合處理高並發請求(可達 100,000+ TPS)
  • 事件驅動模型,擴展性強
  • 非阻塞 I/O,資源利用率高
  • 支援多語言(Java、Kotlin、Scala)
  • 內建叢集支援,易於水平擴展

缺點:

  • 學習曲線較陡,配置較為複雜
  • 需要熟悉 Vert.x 框架與非同步程式設計
  • 除錯較困難(非同步堆疊追蹤)
  • 社群規模較小,學習資源相對少

推薦原因:

適合需要高性能和高併發的應用,事件驅動模型提供了極大的靈活性和擴展性。


安全性最佳實踐

  1. 檔案類型驗證
    • 不僅檢查副檔名,還要驗證 MIME type
    • 使用 Apache Tika 或類似工具檢查檔案魔術數字(Magic Number)
    • 拒絕可執行檔案格式(.exe, .sh, .bat 等)
  2. 檔案大小限制
    • 設定合理的檔案大小上限(建議 10MB 以下)
    • 實作請求總大小限制,防止批次上傳攻擊
    • 考慮使用者等級差異化限制
  3. 檔名安全處理
    • 移除或取代特殊字元(../, , null 等)
    • 使用 UUID 或時間戳生成新檔名
    • 限制檔名長度(建議最多 255 字元)
  4. 儲存位置安全
    • 上傳目錄不應在 Web 根目錄下,防止直接存取
    • 設定目錄權限為只寫,禁止執行
    • 使用獨立的檔案伺服器或雲端儲存(如 AWS S3)
  5. 病毒掃描
    • 整合 ClamAV 或商業防毒 API
    • 非同步掃描,避免阻塞使用者請求
    • 發現病毒後自動刪除並記錄日誌
  6. 存取控制
    • 實作使用者驗證,禁止匿名上傳
    • 記錄上傳者 IP、時間、檔案雜湊值
    • 實作速率限制,防止暴力上傳

效能優化建議

  1. 使用串流處理
    // 避免將整個檔案載入記憶體
    try (InputStream input = file.getInputStream();
         OutputStream output = new FileOutputStream(targetFile)) {
        byte[] buffer = new byte[8192];
        int bytesRead;
        while ((bytesRead = input.read(buffer)) != -1) {
            output.write(buffer, 0, bytesRead);
        }
    }
    
  2. 非同步處理
    • 上傳成功後立即回傳,後續處理(如圖片壓縮、病毒掃描)使用訊息佇列非同步執行
    • Spring Boot 可使用 @Async 註解
    • Vert.x 原生支援非同步操作
  3. 圖片壓縮與格式轉換
    • 使用 ThumbnailatorImageMagick 自動生成縮圖
    • 將 PNG 自動轉換為 WebP 格式(可節省 30-50% 檔案大小)
    • 設定合理的圖片品質(通常 80-85 已足夠)
  4. CDN 與快取策略
    • 上傳到雲端儲存後,使用 CDN 加速存取
    • 設定適當的 Cache-Control 標頭(如 max-age=31536000
    • 使用內容雜湊值作為檔名,實現永久快取
  5. 資料庫優化
    • 不要將圖片二進位資料儲存在資料庫中
    • 僅儲存檔案路徑、雜湊值、元資料
    • 為常查詢欄位建立索引

常見問題 FAQ

Q1: 應該選擇哪種上傳方式?

A: 選擇建議:

  • 新專案:使用 Spring Boot(簡單、主流、社群支援完整)
  • 既有 Servlet 專案:使用原生 Servlet 或 Apache Commons FileUpload
  • RESTful API:使用 JAX-RS with Jersey
  • 高並發需求(>10,000 TPS):使用 Vert.x
  • 大檔案上傳(>100MB):使用 Apache Commons FileUpload(支援斷點續傳)

Q2: 如何防止惡意上傳攻擊?

A: 完整防護策略:

  1. 白名單驗證:僅允許 jpg, jpeg, png, gif, webp
  2. 魔術數字檢查:驗證檔案開頭的位元組,例如 PNG 開頭必須是 89 50 4E 47
  3. 重新生成檔名:使用 UUID.randomUUID() 避免路徑遍歷攻擊
  4. 病毒掃描:整合 ClamAV 進行即時掃描
  5. 速率限制:同一 IP 每分鐘最多 10 次上傳
  6. 使用者驗證:禁止匿名上傳

Q3: 大檔案上傳時記憶體不足怎麼辦?

A: 解決方案:

  • 使用串流處理:不要使用 file.getBytes(),改用 InputStream
  • 設定暫存閾值:Apache Commons FileUpload 可設定 sizeThreshold,超過後自動使用磁碟暫存
  • 分段上傳:前端使用 JavaScript 將檔案切成多個片段上傳
  • 增加 JVM 記憶體java -Xmx2g -Xms1g

Q4: 如何實作上傳進度顯示?

A: 實作方式:

  • Apache Commons FileUpload:實作 ProgressListener 介面
  • 前端輪詢:後端將進度儲存在 Redis,前端每秒查詢一次
  • WebSocket:使用 WebSocket 即時推送進度(適合 Vert.x、Spring WebFlux)
// Apache Commons FileUpload 進度監控範例
upload.setProgressListener(new ProgressListener() {
    @Override
    public void update(long bytesRead, long contentLength, int items) {
        int percent = (int) ((bytesRead * 100) / contentLength);
        System.out.println("上傳進度:" + percent + "%");
    }
});

Q5: 上傳到雲端儲存(AWS S3、Azure Blob)如何實作?

A: 以 AWS S3 為例(使用 Spring Boot):

@Service
public class S3UploadService {

    @Autowired
    private AmazonS3 s3Client;

    public String uploadToS3(MultipartFile file) throws IOException {
        String fileName = UUID.randomUUID().toString() + ".jpg";
        String bucketName = "my-images-bucket";

        ObjectMetadata metadata = new ObjectMetadata();
        metadata.setContentType(file.getContentType());
        metadata.setContentLength(file.getSize());

        s3Client.putObject(new PutObjectRequest(
            bucketName,
            fileName,
            file.getInputStream(),
            metadata
        ).withCannedAcl(CannedAccessControlList.PublicRead));

        return s3Client.getUrl(bucketName, fileName).toString();
    }
}

Q6: 如何處理圖片上傳的 CORS 問題?

A: Spring Boot CORS 設定:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://example.com")
                .allowedMethods("POST", "GET", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

總結

本文介紹了五種在 Java Web 應用中實現圖片上傳的常用方法。每種方法都有其優點,可以根據專案需求和技術棧選擇合適的方式來實現圖片上傳功能。

核心建議:

  • Spring Boot:現代專案的首選,開發效率高
  • Servlet:適合學習底層原理或小型專案
  • JAX-RS:RESTful API 的標準選擇
  • Apache Commons FileUpload:大檔案上傳的最佳方案
  • Vert.x:高並發場景的效能王者

安全性提醒:

  • 永遠不要信任使用者輸入的檔名與副檔名
  • 實作完整的檔案類型驗證(魔術數字 + MIME type)
  • 設定合理的檔案大小與請求速率限制
  • 使用雲端儲存服務代替本地磁碟
  • 定期審計上傳日誌,監控異常行為

相關文章

Leave a Comment