使用 Apache Commons FileUpload 元件處理 multipart/form-data 請求,是傳統 JSP/Servlet 專案上傳檔案的標準做法。本文涵蓋前端表單、後端 Servlet、依賴管理、限制設定與安全考量。
WebApp/ ├─ src/ │ └─ com/example/UploadServlet.java ├─ WebContent/ │ ├─ upload.jsp (上傳表單) │ ├─ result.jsp (結果頁) │ └─ WEB-INF/ │ ├─ lib/ │ │ ├─ commons-fileupload-1.4.jar │ │ └─ commons-io-2.11.0.jar │ └─ web.xml
Maven 依賴:
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.11.0</version> </dependency>
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title>檔案上傳</title></head>
<body>
<h2>檔案上傳表單</h2>
<form action="UploadServlet" method="post" enctype="multipart/form-data">
<p>選擇檔案:<input type="file" name="uploadFile" required></p>
<p>說明:<input type="text" name="description"></p>
<p><input type="submit" value="上傳"></p>
</form>
</body></html>
method="post":GET 無法傳送檔案內容enctype="multipart/form-data":必要,瀏覽器才會正確編碼檔案二進位資料name="uploadFile":Servlet 端用此名稱取得 FileItempackage com.example;
import jakarta.servlet.*;
import jakarta.servlet.annotation.*;
import jakarta.servlet.http.*;
import org.apache.commons.fileupload.*;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import java.io.*;
import java.util.*;
@WebServlet("/UploadServlet")
@MultipartConfig // Servlet 3.0+ 也可用原生支援,但 FileUpload 更彈性
public class UploadServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final String UPLOAD_DIR = "uploads"; // 相對於 webapp 根目錄
private static final int MEM_THRESHOLD = 1024 * 1024 * 3; // 3MB
private static final int MAX_FILE_SIZE = 1024 * 1024 * 50; // 50MB
private static final int MAX_REQUEST_SIZE = 1024 * 1024 * 60; // 60MB
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 檢查是否為 multipart 請求
if (!ServletFileUpload.isMultipartContent(req)) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "請求必須為 multipart/form-data");
return;
}
// 2. 配置工廠與上傳處理器
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(MEM_THRESHOLD);
factory.setRepository(new File(System.getProperty("java.io.tmpdir")));
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(MAX_FILE_SIZE);
upload.setSizeMax(MAX_REQUEST_SIZE);
upload.setHeaderEncoding("UTF-8"); // 解決檔名中文亂碼
// 3. 決定儲存路徑 (真實部署建議放在 Web 根目錄外)
String uploadPath = getServletContext().getRealPath("") + File.separator + UPLOAD_DIR;
File uploadDir = new File(uploadPath);
if (!uploadDir.exists()) uploadDir.mkdirs();
String message = "";
try {
// 4. 解析請求
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(fieldName + " = " + value);
} else {
// 檔案欄位
String originalName = new File(item.getName()).getName(); // 防止路徑遍歷
String savedName = System.currentTimeMillis() + "_" + originalName; // 避免同名覆蓋
File saveFile = new File(uploadPath, savedName);
item.write(saveFile);
message = "檔案「" + originalName + "」上傳成功!儲存為:" + savedName;
}
}
} catch (FileUploadException e) {
message = "上傳失敗:" + e.getMessage();
} catch (Exception e) {
message = "伺服器錯誤:" + e.getMessage();
e.printStackTrace();
}
// 5. 轉向結果頁
req.setAttribute("message", message);
req.getRequestDispatcher("/result.jsp").forward(req, resp);
}
}
<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>上傳結果</title></head>
<body>
<h2><%= request.getAttribute("message") %></h2>
<p><a href="upload.jsp">繼續上傳</a></p>
</body></html>
<servlet> <servlet-name>UploadServlet</servlet-name> <servlet-class>com.example.UploadServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>UploadServlet</servlet-name> <url-pattern>/UploadServlet</url-pattern> </servlet-mapping>
| 風險 | 對策 |
|---|---|
| 惡意檔案類型 | 驗證 Content-Type、副檔名、魔數;僅允許白名單類型 |
| 檔名路徑遍歷 | 使用 new File(item.getName()).getName() 取得純檔名 |
| 超大檔案攻擊 | 設定 setFileSizeMax、setSizeMax、Nginx client_max_body_size |
| 儲存路徑可直接下載 | 檔案存放在 WEB-INF 外部或資料庫,提供下載 Servlet 控制權限 |
| 覆蓋既有檔案 | 重新命名 (UUID、時間戳、雜湊) |
| 中文檔名亂碼 | upload.setHeaderEncoding("UTF-8")、Tomcat URIEncoding="UTF-8" |
若不想引入第三方 JAR,可用 @MultipartConfig + request.getPart():
@WebServlet("/UploadServlet")
@MultipartConfig(
location = "/tmp", // 暫存目錄
maxFileSize = 50_000_000, // 50MB
maxRequestSize = 60_000_000, // 60MB
fileSizeThreshold = 3_000_000 // 3MB
)
public class UploadServlet extends HttpServlet {
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Part filePart = req.getPart("uploadFile");
String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString();
filePart.write(uploadPath + File.separator + fileName);
// ...
}
}