文件上傳下載是 Web 開發高頻需求。本文系統對比三種主流方案:Servlet 3.0+ 原生 API(零依賴)、SmartUpload(經典組件)、Spring Boot/MultipartFile(現代標準),涵蓋大文件分片、斷點續傳、安全防範、雲存儲整合等生產級實踐。
表單設置 enctype="multipart/form-data",瀏覽器以二進制流編碼提交,服務端解析請求體邊界分隔符,提取文件字節流持久化。
設置響應頭 Content-Disposition: attachment; filename="xxx" 觸發瀏覽器下載行為,輸出流寫入文件字節。
@WebServlet("/upload")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 1MB 寫入臨時文件
maxFileSize = 1024 * 1024 * 50, // 50MB 單文件限制
maxRequestSize = 1024 * 1024 * 100 // 100MB 總請求限制
)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
Part filePart = req.getPart("file");
String fileName = Paths.get(filePart.getSubmittedFileName()).getFileName().toString();
// 安全校驗
if (!isAllowedExtension(fileName)) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "文件類型不允許");
return;
}
// 生成唯一文件名防覆蓋
String savedName = UUID.randomUUID() + "_" + fileName;
Path savePath = Paths.get(getServletContext().getRealPath("/uploads"), savedName);
Files.createDirectories(savePath.getParent());
try (InputStream in = filePart.getInputStream()) {
Files.copy(in, savePath, StandardCopyOption.REPLACE_EXISTING);
}
resp.getWriter().write("上傳成功:" + savedName);
}
private boolean isAllowedExtension(String fileName) {
String ext = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
return Arrays.asList("jpg", "png", "pdf", "docx").contains(ext);
}
}
@WebServlet("/download")
public class DownloadServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String fileName = req.getParameter("filename");
// 防止路徑遍歷攻擊
fileName = Paths.get(fileName).getFileName().toString();
Path filePath = Paths.get(getServletContext().getRealPath("/uploads"), fileName);
if (!Files.exists(filePath)) {
resp.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
resp.setContentType(getServletContext().getMimeType(fileName));
resp.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
resp.setContentLengthLong(Files.size(filePath));
try (InputStream in = Files.newInputStream(filePath);
OutputStream out = resp.getOutputStream()) {
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
}
}
注意: SmartUpload 早已停止維護,新項目不建議使用。
SmartUpload su = new SmartUpload();
su.initialize(getServletConfig(), request, response);
su.setMaxFileSize(10 * 1024 * 1024); // 10MB
su.setTotalMaxFileSize(100 * 1024 * 1024); // 100MB
su.setAllowedFilesList("txt,jpg,png,gif,pdf");
su.setDeniedFilesList("exe,bat,sh,jsp,php");
try {
su.upload();
int count = su.save(uploadPath, SmartUpload.SAVE_PHYSICAL);
// 獲取文件元信息
for (int i = 0; i < su.getFiles().getCount(); i++) {
com.jspsmart.upload.File f = su.getFiles().getFile(i);
System.out.println("字段名:" + f.getFieldName());
System.out.println("原文件名:" + f.getFileName());
System.out.println("大小:" + f.getSize());
System.out.println("擴展名:" + f.getFileExt());
}
} catch (SmartUploadException e) {
// 錯誤碼處理:1010/1015 類型錯誤、1105/1110 大小超限
}
response.setContentType("application/x-msdownload");
response.setHeader("Content-Disposition", "attachment; filename=batch.zip");
String[] filenames = request.getParameterValues("filename");
try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
for (String name : filenames) {
Path file = Paths.get(uploadPath, name);
if (Files.exists(file)) {
zos.putNextEntry(new ZipEntry(name));
Files.copy(file, zos);
zos.closeEntry();
}
}
}
spring:
servlet:
multipart:
enabled: true
max-file-size: 50MB
max-request-size: 100MB
file-size-threshold: 1MB
location: ${java.io.tmpdir}/uploads # 臨時文件目錄
@RestController
@RequestMapping("/api/files")
public class FileController {
@Value("${file.upload-dir:/data/uploads}")
private String uploadDir;
@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 savePath = Paths.get(uploadDir, savedName);
try {
Files.createDirectories(savePath.getParent());
file.transferTo(savePath);
} catch (IOException e) {
log.error("上傳失敗", e);
return Result.error("存儲異常");
}
return Result.ok(savedName);
}
@GetMapping("/download/{filename}")
public void download(@PathVariable String filename, HttpServletResponse response) {
filename = Paths.get(filename).getFileName().toString(); // 安全過濾
Path filePath = Paths.get(uploadDir, filename);
if (!Files.exists(filePath)) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
response.setHeader("Content-Disposition",
"attachment; filename=\"" + filename + "\"");
try (InputStream in = Files.newInputStream(filePath)) {
IOUtils.copy(in, response.getOutputStream());
} catch (IOException e) {
log.error("下載失敗", e);
}
}
@PostMapping("/chunked-upload")
public Result chunkedUpload(@RequestParam("file") MultipartFile file,
@RequestParam("chunk") int chunk,
@RequestParam("chunks") int chunks,
@RequestParam("md5") String md5) {
// 分片上傳邏輯:臨時目錄存分片 → 合併驗證 MD5 → 移動到正式目錄
// 參考:Resumable.js、Plupload 前端組件配合
}
}
| 維度 | 措施 |
|---|---|
| 文件類型 | 雙重校驗:擴展名白名單 + Magic Number(文件頭字節)驗證 |
| 文件大小 | 客戶端/網關/應用三層限制,防止 OOM |
| 文件名 | UUID 重命名,去除路徑遍歷字符(../、..\) |
| 存儲隔離 | 上傳目錄禁用腳本執行權限,獨立域名/存儲桶 |
| 惡意掃描 | 集成 ClamAV/ICAP 病毒掃描,異步處理 |
| 大文件 | 分片上傳、斷點續傳、秒傳(MD5 秒傳)、異步合併 |
| 雲存儲 | 對接 S3/OSS/COS SDK,預簽名 URL 直傳,減輕應用服務器壓力 |
| 下載限速 | Token Bucket 算法限制單用戶帶寬 |
| 防盜鏈 | Referer 檢查、簽名 URL 時效性、防盜鏈 Token |
// 後端生成預簽名 URL
@GetMapping("/presign")
public String getPresignedUrl(@RequestParam String filename, @RequestParam String contentType) {
String key = "uploads/" + UUID.randomUUID() + "_" + filename;
return ossClient.generatePresignedUrl(bucket, key, Date.from(Instant.now().plusSeconds(3600)), HttpMethod.PUT);
}
// 前端直傳 OSS
fetch(presignedUrl, {
method: 'PUT',
headers: { 'Content-Type': contentType },
body: file
}).then(() => notifyBackendComplete(key));
優勢:應用服務器零流量經手、自動擴容、CDN 加速、成本更低。