multipart/form-data 編碼傳送檔案流WEB-INF 下,防直接存取)<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version> <!-- 建議升級至 1.5+ -->
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
@WebServlet("/upload")
@MultipartConfig(
fileSizeThreshold = 1024 * 100, // 100KB 閾值,超過寫入暫存檔
maxFileSize = 1024 * 1024, // 單檔 1MB
maxRequestSize = 1024 * 1024 * 10 // 總請求 10MB
)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String saveRoot = getServletContext().getRealPath("/WEB-INF/upload");
String tempDir = getServletContext().getRealPath("/WEB-INF/temp");
new File(tempDir).mkdirs();
String message = "上傳成功";
try {
// 解析所有 Part (Servlet 3.0+ 原生支援,無需 Commons FileUpload)
for (Part part : req.getParts()) {
if (part.getName().equals("file")) { // 檔案欄位
String originalName = Paths.get(part.getSubmittedFileName()).getFileName().toString();
if (originalName.isEmpty()) continue;
// 檔案類型驗證
String ext = originalName.substring(originalName.lastIndexOf('.') + 1).toLowerCase();
if (Set.of("zip", "rar", "exe", "sh").contains(ext)) {
throw new IllegalArgumentException("禁止上傳執行檔/壓縮檔");
}
// 唯一檔名:UUID + 原始檔名
String storedName = UUID.randomUUID() + "_" + originalName;
// 雜湊分散儲存:兩層子目錄
int hash = storedName.hashCode();
String subDir = saveRoot + File.separator + (hash & 0xF) + File.separator + ((hash & 0xF0) >> 4);
new File(subDir).mkdirs();
// NIO 高效複製 (Files.copy 內部使用零拷貝/直接緩衝區)
try (InputStream in = part.getInputStream()) {
Files.copy(in, Paths.get(subDir, storedName), StandardCopyOption.REPLACE_EXISTING);
}
part.delete(); // 刪除暫存檔
} else { // 一般表單欄位
String value = req.getParameter(part.getName());
log.info("表單欄位: {} = {}", part.getName(), value);
}
}
} catch (IllegalArgumentException e) {
message = e.getMessage();
} catch (Exception e) {
log.error("上傳失敗", e);
message = "系統錯誤:" + e.getMessage();
}
req.setAttribute("message", message);
req.getRequestDispatcher("/message.jsp").forward(req, resp);
}
}
| 問題 | 解決方案 |
|---|---|
| 安全性 | 儲存於 WEB-INF 非公開目錄;驗證副檔名與 MIME 類型;掃描病毒 |
| 檔名衝突 | UUID + 原始檔名 或 時間戳 + 隨機數 |
| 單目錄檔案過多 | 雜湊分散:hash & 0xF 兩層子目錄,單目錄檔案數可控 |
| 大檔案記憶體溢位 | fileSizeThreshold 閾值觸發暫存檔;NIO Files.copy 避免堆外記憶體 |
| 中文檔名亂碼 | Servlet 3.0+ 原生支援;舊版 fileUpload.setHeaderEncoding("UTF-8") |
| 進度監聽 | fileUpload.setProgressListener 或前端 JS 監聽 progress 事件 |
@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String storedName = req.getParameter("filename"); // UUID_xxx.pdf
if (storedName == null || storedName.isEmpty()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// 還原儲存路徑 (同上傳時的雜湊算法)
int hash = storedName.hashCode();
String subDir = getServletContext().getRealPath("/WEB-INF/upload")
+ File.separator + (hash & 0xF)
+ File.separator + ((hash & 0xF0) >> 4);
Path filePath = Paths.get(subDir, storedName);
if (!Files.exists(filePath)) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "檔案不存在");
return;
}
// 解析原始檔名 (去除 UUID_ 前綴)
String originalName = storedName.substring(storedName.indexOf('_') + 1);
// 設定下載標頭 (RFC 5987 編碼支援中文檔名)
resp.setContentType(Files.probeContentType(filePath));
resp.setHeader("Content-Disposition",
"attachment; filename*=UTF-8''" + URLEncoder.encode(originalName, StandardCharsets.UTF_8));
resp.setContentLengthLong(Files.size(filePath));
// NIO 零拷貝高效輸出
try (FileChannel in = FileChannel.open(filePath, StandardOpenOption.READ);
WritableByteChannel out = Channels.newChannel(resp.getOutputStream())) {
in.transferTo(0, Files.size(filePath), out);
}
}
}
<c:forEach var="file" items="${fileList}">
<a href="download?filename=${file.storedName}">${file.originalName}</a>
<span>${file.size} bytes</span>
<span>${file.uploadTime}</span>
</c:forEach>
現代專案不再需要 Commons FileUpload。@MultipartConfig + request.getParts() 即可完整替代,減少依賴、效能更優、API 更簡潔。
參考資源:xdp-gacl 博客