上傳中文文件名文件,file.getOriginalFilename() 返回亂碼(如 涓婂崍.txt 或 ???.txt)。
Tomcat 8+ 默認使用 ISO-8859-1 解析 HTTP Header 中的 Content-Disposition,而瀏覽器發送時編碼不一:
filename*)server.xml 中 Connector 添加 URIEncoding:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8" />
注意:僅影響 URL 查詢參數,不影響 Multipart Header 解析。
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8"/> <!-- 關鍵 -->
<property name="maxUploadSize" value="10485760"/>
</bean>
或 Spring Boot:
spring.servlet.multipart.enabled=true
spring.servlet.multipart.file-size-threshold=0
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=10MB
# Spring Boot 2.6+ 需額外設置
spring.servlet.multipart.resolve-lazily=true
@RequestMapping("/upload")
public String upload(@RequestParam("file") MultipartFile file, HttpServletRequest request) {
String originalFilename = file.getOriginalFilename();
// 方案 A:手動轉碼(針對 ISO-8859-1 誤解碼)
if (originalFilename != null) {
try {
// Tomcat 8+ 默認 ISO-8859-1,實際為 UTF-8 字節
originalFilename = new String(originalFilename.getBytes("ISO-8859-1"), "UTF-8");
} catch (UnsupportedEncodingException e) {
log.warn("文件名轉碼失敗", e);
}
}
// 方案 B:使用 RFC 5987 編碼的 filename*(需解析 Header)
// 參考:HttpServletRequest.getHeader("Content-Disposition")
// 後續業務邏輯...
return "success";
}
// 1. 獲取副檔名
String ext = "";
int dotIndex = originalFilename.lastIndexOf('.');
if (dotIndex > 0) {
ext = originalFilename.substring(dotIndex); // 含點
}
// 2. 生成唯一文件名(UUID + 時間戳 + 隨機數)
String storedName = UUID.randomUUID().toString().replace("-", "")
+ "_" + System.currentTimeMillis()
+ ext;
// 3. 數據庫存儲原始文件名(供下載顯示)
fileRecord.setOriginalName(originalFilename);
fileRecord.setStoredName(storedName);
fileRecord.setExt(ext);
// 4. 物理文件按日期分目錄存儲
String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd"));
Path targetDir = rootPath.resolve(datePath);
Files.createDirectories(targetDir);
file.transferTo(targetDir.resolve(storedName).toFile());
@RestController
@RequestMapping("/api/files")
public class FileUploadController {
@Value("${file.upload.root}")
private String rootPath;
@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()) return Result.error("文件為空");
String originalName = fixFilenameEncoding(file.getOriginalFilename());
String ext = getExtension(originalName);
String storedName = generateStoredName(ext);
try {
Path dateDir = Paths.get(rootPath, LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")));
Files.createDirectories(dateDir);
file.transferTo(dateDir.resolve(storedName).toFile());
// 保存元數據到 DB
fileService.save(FileRecord.builder()
.originalName(originalName)
.storedName(storedName)
.path(dateDir.toString())
.size(file.getSize())
.contentType(file.getContentType())
.build());
return Result.success(storedName);
} catch (IOException e) {
log.error("文件保存失敗", e);
return Result.error("保存失敗");
}
}
private String fixFilenameEncoding(String name) {
if (name == null) return null;
try {
return new String(name.getBytes("ISO-8859-1"), "UTF-8");
} catch (UnsupportedEncodingException e) {
return name; // 兜底
}
}
private String getExtension(String name) { /* ... */ }
private String generateStoredName(String ext) { /* ... */ }
}
| 錯誤做法 | 後果 | 正確做法 |
|---|---|---|
| 只設置 request.setCharacterEncoding("UTF-8") | 對 Multipart 無效,Header 已解析完成 | 配置 MultipartResolver.defaultEncoding |
| 直接用原始文件名存儲 | 文件系統不支持特殊字符、重名覆蓋、路徑遍歷風險 | 重命名存儲,DB 記錄原名 |
| 信任瀏覽器發送的文件名 | 可構造 ../../etc/passwd 等惡意路徑 | 嚴格提取副檔名,其餘丟棄 |
| 單目錄存儲大量文件 | 文件系統性能下降、inode 耗盡 | 按日期/哈希分級目錄 |
核心原則:
1. 源頭配置 MultipartResolver UTF-8
2. 代碼兜底 手動 ISO-8859-1 → UTF-8 轉碼
3. 業務隔離 物理存儲用 UUID,邏輯名稱存 DB
4. 安全第一 絕不信任客戶端文件名