下面我将为你提供一个完整的Spring Boot后端实现,用于支持Vue3前端的文件下载功能。这个实现包括文件下载API、中文文件名支持、安全检查和错误处理。
Spring Boot文件下载实现
java
复制
下载
import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; @RestController @RequestMapping("/api/files") public class FileDownloadController { // 文件存储的基础目录(根据实际情况修改) private static final String FILE_DIRECTORY = "/path/to/your/files"; /** * 基础文件下载API * @param filename 文件名 * @return 文件资源响应 */ @GetMapping("/download") public ResponseEntity<Resource> downloadFile(@RequestParam String filename) { try { // 安全校验:防止路径遍历攻击 if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) { throw new SecurityException("非法文件名"); } Path filePath = Paths.get(FILE_DIRECTORY).resolve(filename).normalize(); Resource resource = new UrlResource(filePath.toUri()); if (!resource.exists()) { return ResponseEntity.notFound().build(); } // 处理中文文件名 String encodedFilename = new String(filename.getBytes(StandardCharsets.UTF_8), "ISO-8859-1"); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_OCTET_STREAM) .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFilename + "\"") .body(resource); } catch (MalformedURLException e) { return ResponseEntity.badRequest().body(null); } catch (SecurityException e) { return ResponseEntity.status(403).body(null); } } /** * 从数据库下载文件的API * @param fileId 文件ID * @return 文件流响应 */ @GetMapping("/download-from-db") public ResponseEntity<StreamingResponseBody> downloadFromDatabase(@RequestParam Long fileId) { // 实际项目中从数据库获取文件信息 FileInfo fileInfo = fileService.getFileInfo(fileId); if (fileInfo == null) { return ResponseEntity.notFound().build(); } // 处理中文文件名 String encodedFilename = new String(fileInfo.getFilename().getBytes(StandardCharsets.UTF_8), "ISO-8859-1"); StreamingResponseBody responseBody = outputStream -> { try (InputStream inputStream = fileService.getFileContent(fileId)) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } }; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFilename + "\"") .contentType(MediaType.parseMediaType(fileInfo.getContentType())) .body(responseBody); } /** * 大文件下载(流式传输) * @param filename 文件名 * @return 流式响应体 */ @GetMapping("/download-large") public ResponseEntity<StreamingResponseBody> downloadLargeFile(@RequestParam String filename) { Path filePath = Paths.get(FILE_DIRECTORY).resolve(filename).normalize(); java.io.File file = filePath.toFile(); if (!file.exists()) { return ResponseEntity.notFound().build(); } // 处理中文文件名 String encodedFilename = new String(filename.getBytes(StandardCharsets.UTF_8), "ISO-8859-1"); StreamingResponseBody responseBody = outputStream -> { try (InputStream inputStream = new java.io.FileInputStream(file)) { byte[] buffer = new byte[8192]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } } }; return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + encodedFilename + "\"") .contentType(MediaType.APPLICATION_OCTET_STREAM) .body(responseBody); } } // 文件信息实体类(示例) @Data class FileInfo { private Long id; private String filename; private String contentType; private long size; } // 文件服务接口(示例) @Service class FileService { @Autowired private JdbcTemplate jdbcTemplate; public FileInfo getFileInfo(Long fileId) { // 实际数据库查询逻辑 return jdbcTemplate.queryForObject( "SELECT id, filename, content_type, size FROM files WHERE id = ?", (rs, rowNum) -> { FileInfo info = new FileInfo(); info.setId(rs.getLong("id")); info.setFilename(rs.getString("filename")); info.setContentType(rs.getString("content_type")); info.setSize(rs.getLong("size")); return info; }, fileId ); } public InputStream getFileContent(Long fileId) { // 实际获取文件流的逻辑 return jdbcTemplate.queryForObject( "SELECT content FROM files WHERE id = ?", (rs, rowNum) -> rs.getBinaryStream("content"), fileId ); } }
关键配置说明
文件存储路径:
java
复制
下载
private static final String FILE_DIRECTORY = "/path/to/your/files";
替换为你的实际文件存储路径
中文文件名处理:
java
复制
下载
String encodedFilename = new String(filename.getBytes(StandardCharsets.UTF_8), "ISO-8859-1");
确保浏览器正确显示中文文件名
安全措施:
java
复制
下载
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) { throw new SecurityException("非法文件名"); }
防止路径遍历攻击
大文件处理:
使用StreamingResponseBody
实现流式传输,避免内存溢出:java
复制
下载
StreamingResponseBody responseBody = outputStream -> { // 流式读写逻辑 };
前端对应调整
根据后端API调整前端请求:
typescript
复制
下载
// 下载本地文件 const handleDownload = async () => { try { const response = await axios.get('/api/files/download', { params: { filename: '报告.pdf' }, responseType: 'blob' }); // ... 处理下载逻辑 } catch (error) { // 错误处理 } }; // 下载数据库文件 const downloadFromDB = async (fileId: number) => { try { const response = await axios.get('/api/files/download-from-db', { params: { fileId }, responseType: 'blob' }); // ... 处理下载逻辑 } catch (error) { // 错误处理 } };
错误处理增强
在Spring Boot中添加全局异常处理:
java
复制
下载
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(SecurityException.class) public ResponseEntity<String> handleSecurityException(SecurityException ex) { return ResponseEntity.status(HttpStatus.FORBIDDEN) .body("文件访问被拒绝: " + ex.getMessage()); } @ExceptionHandler(FileNotFoundException.class) public ResponseEntity<String> handleFileNotFound(FileNotFoundException ex) { return ResponseEntity.status(HttpStatus.NOT_FOUND) .body("文件不存在: " + ex.getMessage()); } @ExceptionHandler(Exception.class) public ResponseEntity<String> handleGeneralException(Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body("服务器错误: " + ex.getMessage()); } }
响应头说明
后端设置的关键响应头:
Content-Type:
application/octet-stream
(通用二进制流)Content-Disposition:
attachment; filename="filename.ext"
(触发浏览器下载)Content-Length:自动设置(大文件下载建议设置)
数据库设计示例
文件存储表结构:
sql
复制
下载
CREATE TABLE files ( id BIGINT AUTO_INCREMENT PRIMARY KEY, filename VARCHAR(255) NOT NULL, content_type VARCHAR(100) NOT NULL, size BIGINT NOT NULL, content LONGBLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
最佳实践建议
文件存储策略:
小文件(<1MB)可直接存数据库
大文件建议存储在文件系统或对象存储(如S3)
敏感文件需要加密存储
访问控制:
java
复制
下载
@PreAuthorize("hasPermission(#fileId, 'FILE', 'DOWNLOAD')") @GetMapping("/download-secured") public ResponseEntity<Resource> downloadSecuredFile(@RequestParam Long fileId) { // 需要权限校验的文件下载 }
下载限流:
java
复制
下载
@RateLimiter(name = "fileDownload", fallbackMethod = "downloadLimitExceeded") @GetMapping("/download") public ResponseEntity<Resource> downloadFile() { // ... }
日志记录:
java
复制
下载
@PostFilter("hasPermission(filterObject, 'READ')") public void logDownload(String filename, String user) { log.info("用户 {} 下载了文件 {}", user, filename); }
这个实现提供了完整的文件下载功能,包括:
本地文件系统下载
数据库文件下载
大文件流式传输
中文文件名支持
安全防护
错误处理
权限控制
前端可以无缝对接这些API实现文件下载功能。