背景
基于SpringBoot+Vue前后端分离项目中进行文件下载
SpringBoot版本:2.0.3.RELEASE
vue版本:2.5.2
本博客中前端实现文件下载的方式有3种方式如下:
通过a链接下载(需要绕过安全校验框架的token验证);
axios+Blob发送post请求实现下载(经过安全校验框架的登录或者token验证,但是下载复杂类型文件异常,尽可以支持.txt或者csv文件);
XMLHttpRequest+Blob发送post请求实现下载(经过安全校验框架的登录或者token验证,可以下载所有的文件)。
1.后台
1.1后台下载文件的工具类
该工具类型的主要处理逻辑是设置响应头属性,将待下载文件转换为字节流写入HttpServletResponse 实例中。
Content-type:说明了实体主体内对象的媒体类型;不同文件对应的value ,而下面代码中使用的application/octet-stream则表示以流方式下载文件,可以匹配所有类型的文件。
Content-Disposition:下载文件的一个标识字段,filename属性值是下载得到的文件的文件名;
Content-Length:该资源的大小,单位为字节。
package com.mark.common.utils;
import com.mark.common.exception.Campuso2oException;
import org.apache.commons.lang3.StringUtils;
import sun.misc.BASE64Encoder;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
/**
* @Description: 下载文件的工具类
* @Author: Mark
* @CreateDate: 2019/2/23 16:29
* @Version: 2.0
* @Copyright : 豆浆油条个人非正式工作室
*/
public class DownloadFileUtil {
/**
* 下载文件
* @param originalFileName :下载文件的原始文件名
* @param file :下载的文件
* @param response :相应对象
*/
public static void downloadFile(String originalFileName, File file, HttpServletResponse response, HttpServletRequest request) {
// 数据校验
checkParam(originalFileName,file);
//相应头的处理
//清空response中的输出流
response.reset();
//设置文件大小
response.setContentLength((int) file.length());
//设置Content-Type头
response.setContentType("application/octet-stream;charset=UTF-8");
//设置Content-Disposition头 以附件形式解析
String encodedFilename = getEncodedFilename(request, originalFileName);
response.addHeader("Content-Disposition", "attchment;filename=" + encodedFilename);
//将来文件流写入response中
FileInputStream fileInputStream = null;
ServletOutputStream outputStream = null;
try {
//获取文件输入流
fileInputStream = new FileInputStream(file);
//创建数据缓冲区
byte[] buffers = new byte[1024];
//通过response中获取ServletOutputStream输出流
outputStream = response.getOutputStream();
int length;
while ((length = fileInputStream.read(buffers)) > 0) {
//写入到输出流中
outputStream.write(buffers, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//流的关闭
if(fileInputStream != null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.flush();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 下载文件的参数的校验,如果参数不合法则抛出自定义异常
* @param originalFileName :文件原始文件名
* @param file :待下载的文件
*/
private static void checkParam(String originalFileName, File file) {
if(StringUtils.isBlank(originalFileName)){
throw new Campuso2oException("输入的文件原始文件名为空");
}
if(file == null || !file.exists() ){
throw new Campuso2oException("待在下载的文件不存在!");
}
}
/**
* 获取URL编码后的原始文件名
* @param request :客户端请求
* @param originalFileName :原始文件名
* @return :
*/
private static String getEncodedFilename(HttpServletRequest request, String originalFileName) {
String encodedFilename = null;
String agent = request.getHeader("User-Agent");
if(agent.contains("MSIE")){
//IE浏览器
try {
encodedFilename = URLEncoder.encode(originalFileName, "utf-8");
encodedFilename = encodedFilename.replace("+", " ");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}else if(agent.contains("Firefox")){
//火狐浏览器
BASE64Encoder base64Encoder = new BASE64Encoder();
encodedFilename = "=?utf-8?B?" + base64Encoder.encode(originalFileName.getBytes(StandardCharsets.UTF_8))+"?=";
}else{
//其他浏览器
try {
encodedFilename = URLEncoder.encode(originalFileName, "utf-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return encodedFilename;
}
}
1.2.Controller层的调用测试
package com.mark.web.common.controller;
import com.mark.common.utils.DownloadFileUtil;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
/**
* @Description: 通用文件下载类
* @Author: Kingsley
* @CreateDate: 2019/2/23 20:22
* @Version: 2.0
* @Copyright : 豆浆油条个人非正式工作室
*/
@RestController()
@RequestMapping("/common/file/")
public class FileDownloadController {
/**
* 下载文件的demo
*/
@RequestMapping(value = "download")
public void downloadFile(HttpServletRequest request, HttpServletResponse response) {
File file = new File("C:\\Users\\Administrator\\Desktop\\bak\\test.jpg");
String fileName = "测试文件下载.png";
DownloadFileUtil.downloadFile(fileName,file,response,request);
}
}
2.前端下载
2.1前端使用a连接标签下载
a标签的download属性
<a href="http://localhost:8080/common/file/download" download="true">下载文件</a>
1
下载结果演示
左边为原始文件,右边为下载的前端演示以及下载成功后的得到的文件。下载完成后比较两个文件的大小,发现是一致的!
问题
以上可以实现通过a链接下载文件。但这种下载文件的有一个安全问题,通过a链接下载是无法进行token验证的,如果后台有实现了权限验证框架,例如Shiro。那么这种下载方式是需要在Shiro中设置url放行。
2.2优化1-axios发送post请求下载(有bug)
前端vue框架中居于axios组件通过post请求实现文件的下载,这样前端会被后台的权限验证拦截,验证token信息。
注意响应类型需要设置为blob ,Blob对象使用说明
/**
* 文件的下载
* @param url : 请求的Url
* @param data : 请求的数据
* @param errorHandler : 请求发生异常的回调函数
*/
loadLoadFile:(url, data, errorHandler) =>{
axios.post(url,data,{
headers :{
responseType: 'blob'
}
}).then(res=>{
const blob = new Blob([res.data])
let url = window.URL.createObjectURL(blob)
//创建一个a标签元素,设置下载属性,点击下载,最后移除该元素
let link = document.createElement('a')
link.href = url
link.style.display = 'none'
//res.headers.fileName 取出后台返回下载的文件名
const downlaodFileName = decodeURIComponent(res.headers.filename)
console.log(res.headers)
link.setAttribute('download',downlaodFileName)
link.click()
window.URL.revokeObjectURL(url)
}).catch(errorHandler)
},
工具需要在响应头信息中添加一个自定义的下载文件名
注意:虽然后台设置文件名的key为fileName,请求返回客户端的时候会转为小写即filename
//添加下载文件名
response.addHeader("fileName",encodedFilename);
1
2
结果演示
从下载结果中可以看出虽然可以下载文件,但是下载后的文件的大小比原始的文件的大小不一致。更重要的经过测试发现如果下载文件为文档、图片、表格等文件会出现乱码或者文件打不开的情况。搜索一堆网上的博客文档,提到可能是axios下载的锅,响应得到文件流已经是乱码不正确的了。基于axios请求下载文件还没找到更好的解决方法。换个方式呢?下面通过原始XHR请求可以解决这个问题。
2.3优化2:使用XMLHttpRequest发送post请求
关于XMLHttpRequest的使用
window.URL.createObjectURL()和 window.URL.revokeObjectURL() 的使用说明
/**
* 通过XMLHttpRequest发送post请求下载文件
*@param url : 请求的Url
* @param data : 请求的数据
*/
XHRLoadLoadFile:(url, data)=>{
let xhr = new XMLHttpRequest()
xhr.open('post',url)
//如果需要请求头中这是token信息可以在这设置
xhr.setRequestHeader('Content-Type','application/json;charset=UTF-8')
xhr.responseType = 'blob'
xhr.send(JSON.stringify(data))
xhr.onreadystatechange = function(){
if(xhr.readyState ===4 && xhr.status === 200){
const blob = new Blob([xhr.response])
let url = window.URL.createObjectURL(blob)
//创建一个a标签元素,设置下载属性,点击下载,最后移除该元素
let link = document.createElement('a')
link.href = url
link.style.display = 'none'
//取出下载文件名
const fileName = xhr.getResponseHeader('filename')
const downlaodFileName = decodeURIComponent(fileName)
link.setAttribute('download',downlaodFileName)
link.click()
window.URL.revokeObjectURL(url)
}
}
},
结果演示:终于可以正常下载了