以下是针对在Spring Boot中实现 解密并下载ZIP文件接口 的完整解决方案:
一、整体思路
- 接收加密ZIP文件 + 密码
- 解密到临时目录(确保自动清理)
- 打包解密后的文件(或直接返回单个文件)
- 流式返回给前端(避免内存溢出)
二、实现步骤
1. 添加依赖(pom.xml)
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.5</version>
</dependency>
2. 核心Service实现
@Service
public class ZipService {
/**
* 解密ZIP到指定目录
* @param encryptedZipPath 加密文件路径
* @param outputPath 解密输出目录
* @param password 密码
*/
public void decryptZip(String encryptedZipPath, String outputPath, String password) {
try {
ZipFile zipFile = new ZipFile(encryptedZipPath, password.toCharArray());
if (!zipFile.isValidZipFile()) {
throw new RuntimeException("Invalid ZIP file");
}
if (zipFile.isEncrypted()) {
zipFile.setPassword(password.toCharArray());
}
zipFile.extractAll(outputPath);
} catch (ZipException e) {
throw new RuntimeException("Decryption failed: " + e.getMessage());
}
}
}
3. 解密下载Controller
@RestController
@RequestMapping("/api/zip")
public class ZipDownloadController {
@Autowired
private ZipService zipService;
@PostMapping(value = "/decrypt-and-download", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
public ResponseEntity<Resource> decryptAndDownload(
@RequestParam("file") MultipartFile encryptedFile,
@RequestParam String password) {
Path tempDir = null;
try {
// 1. 创建临时工作目录(自动删除)
tempDir = Files.createTempDirectory("zip-decrypt-");
// 2. 保存上传文件
Path encryptedPath = tempDir.resolve(Objects.requireNonNull(encryptedFile.getOriginalFilename()));
encryptedFile.transferTo(encryptedPath);
// 3. 执行解密
Path decryptedDir = tempDir.resolve("decrypted");
Files.createDirectory(decryptedDir);
zipService.decryptZip(encryptedPath.toString(), decryptedDir.toString(), password);
// 4. 获取解密后的文件
File[] decryptedFiles = decryptedDir.toFile().listFiles();
if (decryptedFiles == null || decryptedFiles.length == 0) {
throw new RuntimeException("解密后未发现文件");
}
// 5. 打包结果文件(自动清理)
return buildDownloadResponse(decryptedFiles);
} catch (Exception e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
} finally {
// 确保清理临时文件
if (tempDir != null) {
try {
FileUtils.deleteDirectory(tempDir.toFile());
} catch (IOException ignored) {}
}
}
}
private ResponseEntity<Resource> buildDownloadResponse(File[] decryptedFiles) throws IOException {
// 如果是单文件直接返回
if (decryptedFiles.length == 1) {
Path filePath = decryptedFiles[0].toPath();
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + decryptedFiles[0].getName() + "\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(decryptedFiles[0].length())
.body(new FileSystemResource(filePath));
}
// 多文件打包成新ZIP
Path zipPath = Files.createTempFile("decrypted-", ".zip");
try (ZipOutputStream zos = new ZipOutputStream(Files.newOutputStream(zipPath))) {
for (File file : decryptedFiles) {
ZipEntry entry = new ZipEntry(file.getName());
zos.putNextEntry(entry);
Files.copy(file.toPath(), zos);
zos.closeEntry();
}
}
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"decrypted_files.zip\"")
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.contentLength(Files.size(zipPath))
.body(new InputStreamResource(Files.newInputStream(zipPath)) {
@Override
public void close() throws IOException {
super.close();
Files.deleteIfExists(zipPath); // 删除临时ZIP
}
});
}
}
三、关键优化点
1. 安全防护措施
// 在Controller方法开头添加:
// 文件大小限制(最大100MB)
if (encryptedFile.getSize() > 100 * 1024 * 1024) {
throw new RuntimeException("文件大小超过100MB限制");
}
// 文件名消毒
String sanitizedFilename = encryptedFile.getOriginalFilename()
.replaceAll("[^a-zA-Z0-9\\-_.]", "_")
.replaceAll("\\.\\.", "");
2. 流式处理大文件
// 修改buildDownloadResponse中的多文件打包逻辑:
return ResponseEntity.ok()
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"decrypted_files.zip\"")
.body(new StreamingResponseBody() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
try (ZipOutputStream zos = new ZipOutputStream(outputStream)) {
for (File file : decryptedFiles) {
try (InputStream is = new FileInputStream(file)) {
ZipEntry entry = new ZipEntry(file.getName());
zos.putNextEntry(entry);
IOUtils.copy(is, zos);
zos.closeEntry();
}
}
}
}
});
3. 增强错误处理
@ExceptionHandler(ZipException.class)
public ResponseEntity<String> handleZipException(ZipException e) {
String message = e.getMessage().contains("wrong password") ?
"密码错误" : "ZIP文件损坏";
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(message);
}
四、前端调用示例(React)
function DecryptDownload() {
const [file, setFile] = useState(null);
const [password, setPassword] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('file', file);
formData.append('password', password);
try {
const response = await axios.post('/api/zip/decrypt-and-download', formData, {
responseType: 'blob'
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', file.name.replace(/.zip$/i, '_decrypted.zip'));
document.body.appendChild(link);
link.click();
link.remove();
} catch (error) {
alert(`下载失败: ${error.response?.data || error.message}`);
}
};
return (
<form onSubmit={handleSubmit}>
<input type="file" onChange={e => setFile(e.target.files[0])} />
<input type="password" value={password} onChange={e => setPassword(e.target.value)} />
<button type="submit">解密并下载</button>
</form>
);
}
五、部署注意事项
-
JCE策略文件
如果使用AES-256加密,需在$JAVA_HOME/jre/lib/security
中替换:- local_policy.jar
- US_export_policy.jar
-
服务器配置(application.yml)
server:
max-http-header-size: 16KB # 防止过长的文件名攻击
spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
- 监控指标
添加Prometheus监控:
@Timed(value = "zip.download.time", description = "Time taken to process ZIP download")
@PostMapping("/decrypt-and-download")
public ResponseEntity<Resource> decryptAndDownload(...) {
// ...
}
六、方案优势
特性 | 说明 |
---|---|
内存安全 | 使用临时目录+流式处理,避免OOM |
完整清理机制 | 确保操作后无残留临时文件 |
防御性编程 | 包含文件大小限制、文件名消毒等防护 |
多文件自动打包 | 自动处理单文件/多文件场景 |
完善错误处理 | 明确区分密码错误、文件损坏等情况 |
通过以上实现,可以安全高效地处理加密ZIP文件的解密下载需求。