<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" />
<input type="text" name="description" />
<button type="submit">上傳</button>
</form>
enctype="multipart/form-data" 必須設置method="post" 必須name 屬性用於後端獲取@WebServlet("/upload")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1MB 閾值,超過寫入臨時文件
maxFileSize = 1024 * 1024 * 10, // 10MB 單文件限制
maxRequestSize = 1024 * 1024 * 50 // 50MB 總請求限制
)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("application/json;charset=UTF-8");
// 獲取普通表單字段
String description = req.getParameter("description");
// 獲取上傳文件 Part
Part filePart = req.getPart("file");
if (filePart == null || filePart.getSize() == 0) {
resp.getWriter().write("{\"code\":400,\"msg\":\"文件為空\"}");
return;
}
// 解析原始文件名
String originalName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString();
// 安全文件名:UUID + 原後綴
String ext = originalName.substring(originalName.lastIndexOf("."));
String savedName = UUID.randomUUID().toString().replace("-", "") + ext;
// 保存路徑
String uploadDir = getServletContext().getRealPath("/uploads");
File dir = new File(uploadDir);
if (!dir.exists()) dir.mkdirs();
File dest = new File(uploadDir, savedName);
try (InputStream in = filePart.getInputStream();
OutputStream out = new FileOutputStream(dest)) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
// 記錄映射關係 (可存 DB/Redis)
// fileMap.put(savedName, originalName);
resp.getWriter().write(
String.format("{\"code\":200,\"msg\":\"成功\",\"fileName\":\"%s\",\"originalName\":\"%s\"}",
savedName, originalName));
}
}
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.5</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
@WebServlet("/upload-legacy")
public class LegacyUploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 1. 檢查是否 multipart
if (!ServletFileUpload.isMultipartContent(req)) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "非文件上傳請求");
return;
}
// 2. 磁盤工廠 (臨時文件目錄、大小閾值)
File tempDir = new File(System.getProperty("java.io.tmpdir"), "upload");
DiskFileItemFactory factory = new DiskFileItemFactory(1024 * 1024, tempDir);
// 3. 解析器
ServletFileUpload upload = new ServletFileUpload(factory);
upload.setFileSizeMax(10 * 1024 * 1024); // 10MB
upload.setSizeMax(50 * 1024 * 1024); // 50MB
upload.setHeaderEncoding("UTF-8");
try {
List items = upload.parseRequest(req);
for (FileItem item : items) {
if (item.isFormField()) {
// 普通字段
String fieldName = item.getFieldName();
String value = item.getString("UTF-8");
} else {
// 文件字段
String fileName = item.getName();
if (fileName != null && !fileName.isEmpty()) {
String ext = fileName.substring(fileName.lastIndexOf("."));
String savedName = UUID.randomUUID() + ext;
File dest = new File(uploadDir, savedName);
FileUtils.copyInputStreamToFile(item.getInputStream(), dest);
}
}
}
} catch (FileUploadException e) {
throw new ServletException("解析失敗", e);
}
}
}
@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String fileName = req.getParameter("fileName"); // 存儲名
String originalName = req.getParameter("originalName"); // 原始名
if (fileName == null || fileName.isEmpty()) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "參數缺失");
return;
}
// 安全校驗:防止路徑遍歷攻擊
fileName = Paths.get(fileName).getFileName().toString();
String uploadDir = getServletContext().getRealPath("/uploads");
File file = new File(uploadDir, fileName);
if (!file.exists() || !file.isFile()) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND, "文件不存在");
return;
}
// 設置響應頭
resp.setContentType("application/octet-stream"); // 強制下載
resp.setContentLengthLong(file.length());
// 文件名編碼 (RFC 5987 / RFC 6266)
String encodedName = URLEncoder.encode(originalName != null ? originalName : fileName,
StandardCharsets.UTF_8.toString()).replace("+", "%20");
resp.setHeader("Content-Disposition",
"attachment; filename=\"" + encodedName + "\"; filename*=UTF-8''" + encodedName);
// 傳輸文件
try (InputStream in = new BufferedInputStream(new FileInputStream(file));
OutputStream out = resp.getOutputStream()) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
out.flush();
}
}
}
Range: bytes=0-1023 斷點續傳| 風險 | 防護措施 |
|---|---|
| 路徑遍歷 | Paths.get(fileName).getFileName() 截取純文件名 |
| 惡意文件類型 | 校驗 Content-Type、魔數、後綴白名單 |
| 文件名注入 | 存儲用 UUID,原名存 DB 映射 |
| 大文件 DoS | 限制 maxFileSize/maxRequestSize、流式處理 |
| 腳本執行 | 上傳目錄配置不可執行、隔離域名、CDN 防盜鏈 |
| 併發衝突 | UUID/雪花算法生成唯一文件名 |
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) return Result.error("文件為空");
String originalName = file.getOriginalFilename();
String ext = originalName.substring(originalName.lastIndexOf("."));
String savedName = UUID.randomUUID() + ext;
Path dest = Paths.get(uploadDir, savedName);
Files.createDirectories(dest.getParent());
file.transferTo(dest); // 一行搞定
return Result.ok(savedName, originalName);
}
// application.yml
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 50MB
file-size-threshold: 1MB
掌握標準 API 與工具庫兩種上傳方式、下載響應頭規範、安全防護體系,構建企業級文件服務能力。