首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 解壓 ZIP 文件完整指南:解決中文亂碼、大文件、目錄遍歷攻擊

     編輯:Java 相關知識

前言

Java 內建 `java.util.zip` 包提供了 ZIP 操作功能,但早期版本 (JDK 7 前) **預設使用 UTF-8 編碼讀取條目名稱**,導致在 Windows (GBK/CP936) 或 Linux (可能為其他編碼) 建立的包含中文檔名的 ZIP 檔解壓時出現亂碼甚至異常。本文提供基於 **JDK 7+ 標準 API** 的現代解決方案,涵蓋編碼自動檢測、大文件流式處理、安全防禦 (Zip Slip) 等生產級最佳實踐。

核心痛點:編碼問題根因

  • ZIP 標準缺陷:原始 ZIP 規範 (AppNote.txt) 未強制規定檔名編碼,僅有一個「語言編碼標誌位」 (EFS, bit 11),常被忽略。
  • JDK 行為:
    • JDK 7- : `ZipInputStream` 硬編碼使用 UTF-8 解碼檔名。
    • JDK 7+ : `new ZipInputStream(is, StandardCharsets.UTF_8)` 仍預設 UTF-8,但**支援指定 `Charset`**。
  • 實際場景:Windows 內建壓縮工具、WinRAR、7-Zip (舊版) 預設用系統預設編碼 (中文 Windows 為 GBK/CP936) 寫入檔名,不設 EFS 標誌位。

現代解決方案:指定 Charset + 自動回退

**推薦策略**:優先嘗試 UTF-8 (符合標準、支援多語言);若解碼失敗或檢測到亂碼,回退嘗試系統預設編碼 (GBK/CP936) 或指定編碼。

import java.io.*;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.zip.*;
import java.util.Enumeration;

public class ModernUnzipper {

    /**
     * 解壓 ZIP 到目標目錄
     * @param zipPath ZIP 檔路徑
     * @param destDir 目標目錄 (會自動創建)
     * @param charset 指定編碼,null 表示自動嘗試 UTF-8 -> 系統預設
     */
    public static void unzip(String zipPath, String destDir, Charset charset) throws IOException {
        Path dest = Paths.get(destDir).toAbsolutePath().normalize();
        Files.createDirectories(dest);

        // 嘗試編碼順序
        Charset[] charsets = (charset != null) 
            ? new Charset[]{charset} 
            : new Charset[]{StandardCharsets.UTF_8, Charset.forName("GBK")}; // 中文環境常用回退

        ZipFile zipFile = null;
        Exception lastException = null;

        for (Charset cs : charsets) {
            try {
                zipFile = new ZipFile(zipPath, cs);
                break; // 成功打開,跳出迴圈
            } catch (Exception e) {
                lastException = e;
                if (zipFile != null) { try { zipFile.close(); } catch (IOException ignored) {} }
            }
        }

        if (zipFile == null) {
            throw new IOException("無法以任何支援編碼打開 ZIP: " + zipPath, lastException);
        }

        try (zipFile) {
            Enumeration entries = zipFile.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                Path targetPath = dest.resolve(entry.getName()).normalize();

                // === 關鍵安全防禦:防止 Zip Slip (目錄遍歷攻擊) ===
                if (!targetPath.startsWith(dest)) {
                    throw new SecurityException("檢測到 Zip Slip 攻擊嘗試: " + entry.getName() + " -> " + targetPath);
                }

                if (entry.isDirectory()) {
                    Files.createDirectories(targetPath);
                    continue;
                }

                // 確保父目錄存在
                Files.createDirectories(targetPath.getParent());

                // 流式複製,避免大文件 OOM
                try (InputStream is = zipFile.getInputStream(entry);
                     OutputStream os = Files.newOutputStream(targetPath, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
                    byte[] buffer = new byte[8192];
                    int len;
                    while ((len = is.read(buffer)) > 0) {
                        os.write(buffer, 0, len);
                    }
                }
            }
        }
    }

    // 便捷重載
    public static void unzip(String zipPath, String destDir) throws IOException {
        unzip(zipPath, destDir, null);
    }

    public static void main(String[] args) {
        try {
            unzip("test.zip", "output_dir");
            System.out.println("解壓完成");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

關鍵技術點詳解

1. 編碼自動檢測與回退機制

代碼嘗試 `UTF-8` -> `GBK` 順序。若 ZIP 內含日文/韓文檔名,可能需加入 `Shift_JIS`, `EUC-KR`。生產環境建議:**要求上游壓縮方開啟「UTF-8 編碼」選項 (如 7-Zip 的 `cu=on`),從源頭消除編碼地獄。**

2. 防禦 Zip Slip (CVE-2018-1000000 等)

惡意 ZIP 可包含 `../../etc/passwd` 或 `..\..\windows\system32\malware.dll` 等路徑。targetPath.startsWith(dest) 配合 normalize() 是**唯一可靠防禦**。絕不可直接用 `new File(dest, entry.getName())` 拼接路徑。

3. 大文件流式處理 (避免 OOM)

  • 使用 `ZipFile.getInputStream(entry)` 獲取輸入流。
  • 使用 `Files.newOutputStream` 獲取輸出流。
  • 固定緩衝區 (8KB-64KB) 循環讀寫,**不將整個文件載入記憶體**。
  • `try-with-resources` 確保流關閉。

4. ZipFile vs ZipInputStream

特性ZipFile (推薦)ZipInputStream
隨機訪問支援 (中央目錄)不支援 (順序讀取)
編碼指定構造函數支援 Charset (JDK 7+)構造函數支援 Charset (JDK 7+)
大文件效能優 (可直接定位)需順序讀取
記憶體佔用較高 (保留中央目錄)極低 (流式)
適用場景一般解壓、需列表條目超大 ZIP、記憶體受限、管道流

進階選項:Apache Commons Compress

若需支援更多格式 (TAR, GZIP, 7z, RAR, Zstandard 等) 或更強大的編碼自動檢測 (基於 `org.apache.commons.compress.archivers.zip.ZipArchiveEntry` 的 `setName`/`getName` 機制),建議引入:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-compress</artifactId>
    <version>1.26.0</version>
</dependency>

它能自動處理 UTF-8 標誌位、提供 `ZipEncoding` 接口實現更智能的編碼回退。

總結

Java 解壓 ZIP 的生產級代碼必須處理三大核心問題:**編碼兼容 (指定 Charset + 回退)**、**安全防禦 (normalize + startsWith 防 Zip Slip)**、**記憶體安全 (流式讀寫 + 緩衝區)**。JDK 7+ 的 `ZipFile(Charset)` 讓編碼問題迎刃而解,無需修改 JDK 內部類。請堅持使用標準 API,拒絕複製修改 `java.util.zip` 源碼的反模式。

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