核心代碼
import java.io.*;
import java.util.zip.*;
import java.nio.file.*;
public class UnzipUtil {
public static void unzip(String zipPath, String destDir) throws IOException {
Path dest = Paths.get(destDir).toAbsolutePath().normalize();
Files.createDirectories(dest);
try (ZipInputStream zis = new ZipInputStream(
new BufferedInputStream(new FileInputStream(zipPath)))) {
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Path target = dest.resolve(entry.getName()).normalize();
// 防止 Zip Slip 攻擊 (目錄遍歷)
if (!target.startsWith(dest)) {
throw new SecurityException("Zip Slip 嘗試: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(target);
} else {
Files.createDirectories(target.getParent());
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(target))) {
byte[] buffer = new byte[8192];
int len;
while ((len = zis.read(buffer)) > 0) {
out.write(buffer, 0, len);
}
}
}
zis.closeEntry();
}
}
}
}
關鍵點解析
- ZipInputStream:流式讀取,不將整個 ZIP 載入記憶體,適合大文件。
- 目錄遍歷防護:
target.startsWith(dest) 確保解壓路徑不逃逸目的目錄 (CVE-2018-1000000 類漏洞)。
- 緩衝區大小:8KB 平衡記憶體與系統調用次數。
- 中文文件名亂碼:JDK 7 及之前預設用 IBM437/CP437 解碼條目名,導致中文亂碼。JDK 8+ 可透過系統屬性
-Dsun.zip.encoding=UTF-8 或使用 Apache Commons Compress ZipFile 指定編碼。
中文亂碼完整解決方案 (Apache Commons Compress)
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.26.0</version>
</dependency>
public static void unzipWithEncoding(String zipPath, String destDir, String encoding) throws IOException {
try (org.apache.commons.compress.archivers.zip.ZipFile zipFile =
new org.apache.commons.compress.archivers.zip.ZipFile(zipPath, encoding)) {
for (ZipArchiveEntry entry : zipFile) {
Path target = Paths.get(destDir).resolve(entry.getName()).normalize();
if (!target.startsWith(Paths.get(destDir).normalize())) {
throw new SecurityException("Zip Slip: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(target);
} else {
Files.createDirectories(target.getParent());
try (InputStream in = zipFile.getInputStream(entry);
OutputStream out = Files.newOutputStream(target)) {
in.transferTo(out); // Java 9+
}
}
}
}
}
進階:並行解壓 (大量小文件)
// 使用 ForkJoinPool 或自定義線程池並行處理條目
// 注意:ZipInputStream 非線程安全,需先收集條目列表再並行
List entries = new ArrayList<>();
try (ZipInputStream zis = new ZipInputStream(...)) {
ZipEntry e;
while ((e = zis.getNextEntry()) != null) entries.add(e);
}
entries.parallelStream().forEach(entry -> { /* 解壓單個條目 */ });