首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 更多程式語言 >> 程式設計綜合問答

Spring MVC MultipartFile 中文文件名亂碼終極解決方案

     編輯:程式設計綜合問答

問題現象

上傳中文文件名文件,file.getOriginalFilename() 返回亂碼(如 涓婂崍.txt 或 ???.txt)。

根因分析

Tomcat 8+ 默認使用 ISO-8859-1 解析 HTTP Header 中的 Content-Disposition,而瀏覽器發送時編碼不一:

  • Chrome/Firefox:UTF-8 (RFC 5987 filename*)
  • IE/Edge 老版本:GBK/本地編碼
  • 代理/負載均衡可能二次轉碼

分層解決方案

第 1 層:Tomcat 配置(源頭修復)

server.xml 中 Connector 添加 URIEncoding:

<Connector port="8080" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="8443"
           URIEncoding="UTF-8" />

注意:僅影響 URL 查詢參數,不影響 Multipart Header 解析。

第 2 層:Spring MultipartResolver 配置

<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

第 3 層:代碼層面雙重保險(推薦)

@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";
}

第 4 層:最佳實踐 - 存儲時重命名(根治)

// 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());

完整 Controller 示例

@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. 安全第一 絕不信任客戶端文件名

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved