springboot项目前后端通用下载方法、问题和解决方案
springboot项目前后端通用下载方法、问题和解决方案
- 一、前言
- 二、后端通用方法
- 1、工具类
- 2、接口,建议get请求
- 三、前端通用方法
- 四、常见问题与解决方案
- 五、前后端协议一致性建议
- 六、总结
一、前言
下面是前后端下载通用方案,包括完整的下载方法(前端 + 后端)、可能遇到的问题、及解决方案,适用于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 |
工具类 | 后端抽取文件下载工具类统一封装,提高复用性 |