首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> JSP 程式設計 >> 關於JSP

JSP 檔案上傳完整實作:Servlet 3.0 標準 API + Apache Commons FileUpload 兩種方案

     編輯:關於JSP

檔案上傳是 Web 應用基本功能。現代 Servlet 容器 (Tomcat 7+/Jetty 9+/WildFly) 內建 **Servlet 3.0 `multipart` 支援**,無需第三方庫即可處理上傳;舊版環境則使用 **Apache Commons FileUpload**。本文提供兩種方案完整程式碼、配置細節、安全性檢查、進階功能 (進度條、多檔、雲端儲存) 實作指南。

方案一:Servlet 3.0 原生 API (推薦,零依賴)

1. Servlet 類別 (標準註解配置)

package com.example.upload;

import jakarta.servlet.*;
import jakarta.servlet.annotation.*;
import jakarta.servlet.http.*;
import java.io.*;
import java.nio.file.*;
import java.util.*;

@WebServlet("/upload")
@MultipartConfig(
    location = "/tmp/uploads",           // 臨時檔案目錄 (超過 fileSizeThreshold 時使用)
    maxFileSize = 10 * 1024 * 1024,      // 單檔上限 10MB
    maxRequestSize = 50 * 1024 * 1024,   // 總請求上限 50MB
    fileSizeThreshold = 1024 * 1024      // 1MB 以上寫入臨時檔
)
public class FileUploadServlet extends HttpServlet {

    private static final String UPLOAD_DIR = "uploads"; // 相對於 webapp 根目錄
    private static final Set ALLOWED_EXT = Set.of("jpg", "jpeg", "png", "gif", "pdf", "doc", "docx", "zip");
    private static final long MAX_SIZE = 10 * 1024 * 1024;

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.setCharacterEncoding("UTF-8");
        String action = req.getParameter("action");
        
        // 取得真實儲存路徑
        String appPath = getServletContext().getRealPath("");
        Path saveDir = Paths.get(appPath, UPLOAD_DIR);
        Files.createDirectories(saveDir);

        Collection parts = req.getParts();
        List messages = new ArrayList<>();

        for (Part part : parts) {
            if (part.getName().equals("file")) { // 僅處理 file 欄位
                String fileName = Paths.get(part.getSubmittedFileName()).getFileName().toString(); // 防止路徑遍歷
                if (fileName.isEmpty()) {
                    messages.add("未選擇檔案");
                    continue;
                }

                // 檔案類型驗證 (副檔名 + Content-Type 雙重檢查)
                String ext = getExtension(fileName).toLowerCase();
                String contentType = part.getContentType();
                if (!ALLOWED_EXT.contains(ext) || !isAllowedContentType(contentType, ext)) {
                    messages.add("禁止的檔案類型: " + fileName);
                    continue;
                }

                // 檔案大小驗證
                if (part.getSize() > MAX_SIZE) {
                    messages.add("檔案過大 (>10MB): " + fileName);
                    continue;
                }

                // 生成唯一檔名避免覆蓋 (UUID + 原副檔名)
                String storedName = UUID.randomUUID() + "." + ext;
                Path target = saveDir.resolve(storedName);

                try (InputStream in = part.getInputStream()) {
                    Files.copy(in, target, StandardCopyOption.REPLACE_EXISTING);
                    messages.add("上傳成功: " + fileName + " → " + storedName);
                } catch (IOException e) {
                    messages.add("上傳失敗: " + fileName + " - " + e.getMessage());
                }

                // 刪除臨時檔 (若 part 為臨時檔)
                part.delete();
            }
        }

        req.setAttribute("messages", messages);
        req.getRequestDispatcher("/upload_result.jsp").forward(req, resp);
    }

    private String getExtension(String filename) {
        int dot = filename.lastIndexOf('.');
        return (dot > 0) ? filename.substring(dot + 1) : "";
    }

    private boolean isAllowedContentType(String contentType, String ext) {
        if (contentType == null) return false;
        return switch (ext) {
            case "jpg", "jpeg" -> contentType.equals("image/jpeg");
            case "png" -> contentType.equals("image/png");
            case "gif" -> contentType.equals("image/gif");
            case "pdf" -> contentType.equals("application/pdf");
            case "doc" -> contentType.equals("application/msword");
            case "docx" -> contentType.equals("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
            case "zip" -> contentType.equals("application/zip");
            default -> false;
        };
    }
}

2. HTML 表單 (必要屬性)

<form action="upload" method="post" enctype="multipart/form-data">
    <input type="file" name="file" accept="image/*,.pdf,.doc,.docx,.zip" required />
    <input type="hidden" name="action" value="upload" />
    <button type="submit">上傳</button>
</form>

<!-- 多檔上傳 (HTML5) -->
<input type="file" name="files" multiple />
<!-- Servlet 端: req.getParts() 迴圈處理每個 part -->
關鍵:`enctype="multipart/form-data"` **必須**設定,否則 `req.getParts()` 為空、`req.getParameter()` 失效。

3. 進階:上傳進度條 (Servlet 3.1+ 非標準、建議前端直傳)

標準 API 無進度事件。現代架構採用 **前端直傳雲端儲存 (S3/OSS/GCS)**:
1. 後端簽發預簽名 URL (Presigned URL)。
2. 前端 `fetch/XHR` 直接 `PUT` 至雲端,原生支援 `onprogress`。
3. 上傳完成回調後端驗證並入庫。
此法卸載伺服器頻寬、支援大檔斷點續傳、天然分散式。

方案二:Apache Commons FileUpload (舊版容器 / 需精細控制)

1. Maven 依賴

<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.5</version> <!-- 需 commons-io 2.6+ -->
</dependency>

2. Servlet 實作

@WebServlet("/upload-legacy")
public class LegacyUploadServlet extends HttpServlet {
    private static final String UPLOAD_DIR = "uploads";

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        boolean isMultipart = ServletFileUpload.isMultipartContent(req);
        if (!isMultipart) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "非 multipart 請求"); return; }

        DiskFileItemFactory factory = new DiskFileItemFactory();
        factory.setRepository(new File(System.getProperty("java.io.tmpdir"))); // 臨時目錄
        factory.setSizeThreshold(1024 * 1024); // 1MB 閾值

        ServletFileUpload upload = new ServletFileUpload(factory);
        upload.setFileSizeMax(10 * 1024 * 1024);
        upload.setSizeMax(50 * 1024 * 1024);
        upload.setHeaderEncoding("UTF-8"); // 解決中文檔名亂碼

        String appPath = getServletContext().getRealPath("");
        File saveDir = new File(appPath, UPLOAD_DIR);
        saveDir.mkdirs();

        try {
            List items = upload.parseRequest(req);
            for (FileItem item : items) {
                if (item.isFormField()) {
                    // 普通表單欄位
                    String fieldName = item.getFieldName();
                    String value = item.getString("UTF-8");
                    System.out.println("Form field: " + fieldName + " = " + value);
                } else {
                    // 檔案欄位
                    String fileName = new File(item.getName()).getName(); // 防路徑遍歷
                    if (fileName.isEmpty()) continue;

                    // 驗證邏輯同方案一...
                    
                    String ext = getExtension(fileName).toLowerCase();
                    String storedName = UUID.randomUUID() + "." + ext;
                    File target = new File(saveDir, storedName);
                    item.write(target); // 自動處理記憶體/臨時檔
                    item.delete(); // 釋放臨時檔
                }
            }
        } catch (Exception e) {
            throw new ServletException("上傳處理失敗", e);
        }
    }
    // getExtension()...
}

安全性檢查清單 (必做)

風險對策
惡意檔案類型 (雙副檔名、偽裝 Content-Type)副檔名白名單 + Content-Type 驗證 + **魔數檢查** (讀取檔頭 bytes)
路徑遍歷 (`../../etc/passwd`)`Paths.get(submittedName).getFileName()` / `new File(name).getName()` 只取檔名部份
超大檔案耗盡磁碟/記憶體`maxFileSize`、`maxRequestSize`、`sizeThreshold` 三層限制
檔名衝突覆蓋UUID/時間戳/雜湊重命名,資料庫記錄原始檔名
腳本執行 (上傳 .jsp/.php/.asp)**儲存目錄設為不可執行** (Nginx/Apache `location ~ \.php$ { deny all; }`、IIS 移除執行權限)、副檔名白名單
DoS (大量小檔、慢速傳輸)限制單位時間上傳次數、連線逾時、請求體大小、WAF 規則
病毒/惡意代碼整合 ClamAV / 商業反病毒 API 掃描入庫前

魔數檢查實作 (Java)

public boolean isValidImage(File file) throws IOException {
    try (InputStream is = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
        byte[] header = new byte[8];
        is.read(header);
        // JPEG: FF D8 FF
        if (header[0] == (byte)0xFF && header[1] == (byte)0xD8 && header[2] == (byte)0xFF) return true;
        // PNG: 89 50 4E 47 0D 0A 1A 0A
        if (header[0] == (byte)0x89 && header[1] == (byte)0x50 && header[2] == (byte)0x4E && header[3] == (byte)0x47) return true;
        // GIF: 47 49 46 38 (GIF8)
        if (header[0] == 'G' && header[1] == 'I' && header[2] == 'F' && header[3] == '8') return true;
        // PDF: 25 50 44 46 (%PDF)
        if (header[0] == '%' && header[1] == 'P' && header[2] == 'D' && header[3] == 'F') return true;
        return false;
    }
}

儲存策略演進

  1. 本地檔案系統:簡單、低延遲,**但無法水平擴展、單點故障、備份困難**。
  2. 分散式檔案系統 (MinIO, Ceph, HDFS, SeaweedFS):S3 相容 API、擴展性強、支援擬碼/EC、適合私有雲。
  3. 公有雲物件儲存 (AWS S3, Azure Blob, GCP Cloud Storage, 阿里云 OSS, 騰訊云 COS):全託管、CDN 整合、生命週期管理、成本優化。**首選生產環境方案**。
  4. 資料庫 BLOB:僅適合極小檔案 (<1MB)、需事務一致性、極少量場景。

資料庫記錄設計

CREATE TABLE uploaded_files (
    id BIGSERIAL PRIMARY KEY,
    original_name VARCHAR(255) NOT NULL,
    stored_name VARCHAR(255) NOT NULL UNIQUE,
    content_type VARCHAR(100) NOT NULL,
    size_bytes BIGINT NOT NULL,
    storage_path VARCHAR(500) NOT NULL, -- 相對路徑或 S3 Key
    storage_type VARCHAR(20) NOT NULL,  -- LOCAL, S3, OSS, MINIO
    uploader_id BIGINT REFERENCES users(id),
    upload_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    checksum_md5 CHAR(32),              -- 可選:完整性驗證
    status VARCHAR(20) DEFAULT 'ACTIVE' -- ACTIVE, DELETED, QUARANTINED
);
**Servlet 3.0 `@MultipartConfig` + `req.getParts()` 是現代標準**,零依賴、型別安全、配置集中。舊環境用 Commons FileUpload。**安全性三寶:白名單副檔名 + Content-Type 驗證 + 魔數檢查**。生產環境**強烈建議前端直傳雲端物件儲存 (S3/OSS) + 後端簽發預簽名 URL**,徹底解決頻寬、擴展、大檔、安全痛點。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved