Servlet 文件上传
multipart 表单、@MultipartConfig、Part 读取与落盘的安全限制。
Key Points表单要 enctype=multipart/form-data,Servlet 要标 @MultipartConfig,用 Part 读字节并重命名落盘。
表单
HTML
<form action="upload" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="file" multiple><br>
说明:<input type="text" name="note"><br>
<button type="submit">上传</button>
</form>Without enctype="multipart/form-data" the browser sends only the file name, not the bytes, and Part is empty — the most common upload failure.
Servlet 端
Java
@WebServlet("/upload")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 超过 1MB 写临时文件
maxFileSize = 5 * 1024 * 1024, // 单文件上限 5MB
maxRequestSize = 20 * 1024 * 1024 // 整个请求上限 20MB
)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
Path dir = Path.of(getServletContext().getRealPath("/uploads"));
Files.createDirectories(dir);
resp.setContentType("text/plain;charset=UTF-8");
try (PrintWriter out = resp.getWriter()) {
for (Part part : req.getParts()) {
String name = part.getSubmittedFileName();
if (name == null || name.isBlank()) {
out.println("字段 " + part.getName() + " = " + readText(part));
continue;
}
String ext = name.contains(".")
? name.substring(name.lastIndexOf('.')) : "";
String stored = UUID.randomUUID() + ext; // 不信任原文件名
part.write(dir.resolve(stored).toString());
out.println("已保存 " + name + " -> " + stored
+ " (" + part.getSize() + " 字节)");
}
}
}
private static String readText(Part part) throws IOException {
try (InputStream in = part.getInputStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
}Output
已保存 report.pdf -> 6f1c2a9e-3b7d-4a10-9c55-2e8d4f0a1b23.pdf (184320 字节)
字段 note = 月度报告三条安全规则
- 重命名落盘 —— 原始文件名可能是
../../web.xml,直接用会覆盖任意文件 - 校验扩展名与 MIME —— 白名单,不要黑名单;
image/jpeg才允许 - 落在 Web 根之外或禁止执行 —— 上传目录能跑 JSP 就等于给了 webshell
Java
private static final Set<String> ALLOWED = Set.of(".jpg", ".jpeg", ".png", ".pdf");
private static String safeExt(String filename) {
String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
if (!ALLOWED.contains(ext)) {
throw new IllegalArgumentException("不支持的类型:" + ext);
}
return ext;
}Exceeding
maxFileSize makes the container throw IllegalStateException (or ServletException on some versions) with no friendly message. Catch it and sendError(413) to tell the user why.大文件与进度
Part.write() stages the whole file in a temp directory, doubling disk use for GB-sized uploads. For very large files switch to a streaming parser (Commons FileUpload Streaming API) that reads and writes in chunks.
表单
HTML
<form action="upload" method="post" enctype="multipart/form-data">
选择文件:<input type="file" name="file" multiple><br>
说明:<input type="text" name="note"><br>
<button type="submit">上传</button>
</form>少了 enctype="multipart/form-data",浏览器只发文件名不发内容,Part 拿不到字节。这是上传失败最常见的原因。
Servlet 端
Java
@WebServlet("/upload")
@MultipartConfig(
fileSizeThreshold = 1024 * 1024, // 超过 1MB 写临时文件
maxFileSize = 5 * 1024 * 1024, // 单文件上限 5MB
maxRequestSize = 20 * 1024 * 1024 // 整个请求上限 20MB
)
public class UploadServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
Path dir = Path.of(getServletContext().getRealPath("/uploads"));
Files.createDirectories(dir);
resp.setContentType("text/plain;charset=UTF-8");
try (PrintWriter out = resp.getWriter()) {
for (Part part : req.getParts()) {
String name = part.getSubmittedFileName();
if (name == null || name.isBlank()) {
out.println("字段 " + part.getName() + " = " + readText(part));
continue;
}
String ext = name.contains(".")
? name.substring(name.lastIndexOf('.')) : "";
String stored = UUID.randomUUID() + ext; // 不信任原文件名
part.write(dir.resolve(stored).toString());
out.println("已保存 " + name + " -> " + stored
+ " (" + part.getSize() + " 字节)");
}
}
}
private static String readText(Part part) throws IOException {
try (InputStream in = part.getInputStream()) {
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
}
}
}Output
已保存 report.pdf -> 6f1c2a9e-3b7d-4a10-9c55-2e8d4f0a1b23.pdf (184320 字节)
字段 note = 月度报告三条安全规则
- 重命名落盘 —— 原始文件名可能是
../../web.xml,直接用会覆盖任意文件 - 校验扩展名与 MIME —— 白名单,不要黑名单;
image/jpeg才允许 - 落在 Web 根之外或禁止执行 —— 上传目录能跑 JSP 就等于给了 webshell
Java
private static final Set<String> ALLOWED = Set.of(".jpg", ".jpeg", ".png", ".pdf");
private static String safeExt(String filename) {
String ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
if (!ALLOWED.contains(ext)) {
throw new IllegalArgumentException("不支持的类型:" + ext);
}
return ext;
}maxFileSize 超限时容器抛 IllegalStateException(部分版本是 ServletException),不会返回友好提示。想给用户看清楚原因,要 try/catch 后 sendError(413)。大文件与进度
Part.write() 会把整个文件先落到临时目录,GB 级文件要占双倍磁盘。超大文件应改用流式解析(如 Commons FileUpload 的 Streaming API),边读边写,内存与磁盘都只占一份。