springboot项目前后端通用下载方法、问题和解决方案
一、前言
下面是前后端下载通用方案,包括完整的下载方法(前端 + 后端)、可能遇到的问题、及解决方案,适用于Word、Excel、PDF 等常见导出文件的下载场景。
二、后端通用方法
1、工具类
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
public class FileDownloadUtil {
private static final String FILE_EXTENSION = ".xlsx";
/**
* 从 classpath 下载模板文件
*/
public static ResponseEntity<?> downloadFromClasspath(String dirPath, String fileName) {
// 路径校验
if (!isValidFileName(fileName)) {
return ResponseEntity
.badRequest()
.body(BaseResponse.error(HttpStatus.BAD_REQUEST.value(), "非法文件名"));
}
String fullFileName = fileName + FILE_EXTENSION;
Resource resource = new ClassPathResource(dirPath + fullFileName);
return buildResponseEntity(resource, fullFileName);
}
/**
* 从文件系统下载文件
*/
public static ResponseEntity<?> downloadFromFileSystem(String fullFilePath, String downloadFileNameWithoutExtension) {
if (!isValidFileName(downloadFileNameWithoutExtension)) {
return ResponseEntity
.badRequest()
.body(BaseResponse.error(HttpStatus.BAD_REQUEST.value(), "非法文件名"));
}
String fullFileName = downloadFileNameWithoutExtension + FILE_EXTENSION;
Resource resource = new FileSystemResource(fullFilePath);
return buildResponseEntity(resource, fullFileName);
}
/**
* 封装 ResponseEntity 下载响应
*/
private static ResponseEntity<?> buildResponseEntity(Resource resource, String fileName) {
if (!resource.exists()) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(BaseResponse.error(HttpStatus.NOT_FOUND.value(), "文件不存在"));
}
try {
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
String contentDisposition = "attachment; filename=\"" +
new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1) +
"\"; filename*=UTF-8''" + encodedFileName;
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition)
.body(resource);
} catch (UnsupportedEncodingException e) {
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(BaseResponse.error(HttpStatus.INTERNAL_SERVER_ERROR.value(), "文件下载失败"));
}
}
/**
* 校验文件名合法性
*/
private static boolean isValidFileName(String fileName) {
return fileName != null &&
!fileName.isEmpty() &&
!fileName.contains("..") &&
!fileName.contains("/") &&
!fileName.contains("\\");
}
}
如需支持不同扩展名(如 .xlsx, .docx, .csv 等),可以将 FILE_EXTENSION 提供为方法参数即可
2、接口,建议get请求
文件目录建议放在springboot项目的resources目录下
@GetMapping("/download-template")
public ResponseEntity<?> downloadTemplate(@RequestParam String fileName) {
return FileDownloadUtil.downloadFromClasspath("files/", fileName);
}
- downloadFromClasspath(…): 用于从 classpath 中的某个目录(比如 /resources/files/)下载。
- downloadFromFileSystem(…): 如果你未来切换为磁盘路径文件下载,也可以使用。
- isValidFileName(…): 用于防止路径穿越攻击。
- 返回值 ResponseEntity<?> 支持直接被 Spring MVC 用作响应体返回,并兼容你的 BaseResponse 结构。
在 Spring Boot 项目中,后端提供下载模板接口时,选择 POST 或 GET 需综合考虑以下因素:
- 使用 POST 的情况
传输数据量大:如果下载模板需要附带参数(如筛选条件、动态数据),POST 无 URL 长度限制,适合传输较大数据。
安全性要求高:POST 请求的参数不会直接暴露在 URL 中,适合敏感数据(如权限校验信息)。
幂等性要求低:下载操作通常是非幂等的(多次请求可能生成不同内容),符合 POST 语义。
- 使用 GET 的情况
简单无参数:若模板是静态文件或仅需少量参数(如模板类型),GET 更简洁直观。
缓存友好:浏览器或网关可能缓存 GET 请求结果,提升重复下载效率。
符合 REST 语义:GET 通常用于获取资源,适合无副作用的下载操作。
- 推荐实践
优先 GET:若下载无需复杂参数且模板为静态资源,GET 更符合习惯。
选择 POST:若需动态生成模板、传递敏感数据或参数较多,使用 POST。
混合方案:通过 GET 获取预签名 URL(如 AWS S3),再由前端直接下载,分散服务器压力。
三、前端通用方法
以 Vue / Axios 为例
// 下载 Word/Excel/PDF 等二进制文件
export function downloadFileByPost(url: string, data: any, fileName: string) {
axios.post(url, data, {
responseType: 'blob',
}).then((res) => {
const blob = new Blob([res.data], { type: res.headers['content-type'] || 'application/octet-stream' });
const link = document.createElement('a');
const objectUrl = URL.createObjectURL(blob);
link.href = objectUrl;
link.download = fileName;
link.click();
URL.revokeObjectURL(objectUrl);
}).catch((error) => {
console.error('Download failed', error);
});
}
四、常见问题与解决方案
问题 | 原因 | 解决方案 |
---|---|---|
❌ 浏览器拒绝加载 Blob 文件 | 前端是 HTTPS,后端是 HTTP(协议不一致) | ✅ 后端升级为 HTTPS,或前端降级为 HTTP(不推荐) |
❌ 下载文件名乱码 | 没有正确设置响应头 Content-Disposition |
后端使用双重编码:filename*=UTF-8''URLEncoder.encode(...) |
❌ 点击下载没反应 | Blob 创建成功但没有触发 click 或被拦截 | 检查 a.click() 位置、浏览器安全策略 |
❌ CORS 跨域下载失败 | 后端未设置跨域 + 下载接口不支持 OPTIONS | 后端添加 CORS 支持,或使用 Spring 的 @CrossOrigin |
❌ 下载到的是 HTML 页面 | 后端返回了错误页(如 404),但前端当成 Blob 下载 | 检查返回的 content-type 是否为 text/html ,判断是否真的是文件 |
❌ 非法路径访问 | 文件名拼接存在路径穿越(…/) | 后端做文件名校验,拒绝包含 / 、.. 的名称 |
我遇到过的问题就是:前端下载后文件里面是[object Object],没有其他内容
最终原因是前端协议是http的,导致浏览器拒绝blob文件。
五、前后端协议一致性建议
场景 | 是否允许 |
---|---|
✅ 前端 HTTPS + 后端 HTTPS | 推荐 |
❌ 前端 HTTPS + 后端 HTTP | 浏览器拦截下载,拒绝加载 Blob |
✅ 前端 HTTP + 后端 HTTP | 可用但 不安全,生产环境不推荐 |
✅ 同源同协议(如同域名反向代理) | 推荐 |
六、总结
项 | 建议 |
---|---|
后端协议 | 必须使用 HTTPS,避免被浏览器拦截 |
文件名处理 | 后端编码处理 Content-Disposition ,避免乱码 |
安全 | 校验文件名防路径穿越,拒绝非法字符 |
跨域 | 后端接口允许跨域访问,设置 Access-Control-Allow-Origin |
工具类 | 后端抽取文件下载工具类统一封装,提高复用性 |