springboot项目前后端通用下载方法、问题和解决方案

发布于:2025-08-07 ⋅ 阅读:(32) ⋅ 点赞:(0)

一、前言

下面是前后端下载通用方案,包括完整的下载方法(前端 + 后端)、可能遇到的问题、及解决方案,适用于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
工具类 后端抽取文件下载工具类统一封装,提高复用性

网站公告

今日签到

点亮在社区的每一天
去签到