如何为React下载按钮添加Authorization请求头实现鉴权下载
带鉴权的前后端文件下载实现方案
现有代码存在两个直接导致功能失效的核心问题:
- 前端错误将二进制文件流响应按JSON格式解析,没有触发浏览器原生下载行为
- 后端
res.download()方法仅支持读取本地文件系统的文件返回,如果文件二进制内容直接存储在数据库中,该方法无法正常工作;路由未挂载鉴权中间件时,鉴权逻辑本身不会生效
后端(Express.js)实现
根据文件存储方式二选一即可:
场景1:数据库仅存储文件本地路径,文件实体存在服务器磁盘
路由必须挂载鉴权校验中间件,确保请求经过token校验后才会执行下载逻辑:
// 路由添加鉴权中间件,authCheck替换为项目实际的鉴权逻辑 router.get('/download/:fid', authCheck, filesControllers.downloadFile); const downloadFile = async (req, res, next) => { const fileId = req.params.fid; try { const filePost = await File.findById(fileId); if (!filePost) { return next(new HttpError("指定文件不存在", 404)); } // res.download第一个参数是文件本地绝对路径,第二个参数是下载时显示的文件名 res.download(filePost.file, filePost.originalName, (err) => { if (err) return next(new HttpError("文件读取失败", 500)); }); } catch (err) { return next(new HttpError("服务器查询异常", 500)); } };
场景2:文件二进制内容直接存储在数据库中(如MongoDB BinData类型)
不要使用res.download(),手动设置响应头后直接返回Buffer内容即可:
router.get('/download/:fid', authCheck, filesControllers.downloadFile); const downloadFile = async (req, res, next) => { const fileId = req.params.fid; try { const filePost = await File.findById(fileId); if (!filePost) { return next(new HttpError("指定文件不存在", 404)); } // 设置二进制流响应头,处理中文文件名编码 res.setHeader('Content-Type', filePost.mimeType || 'application/octet-stream'); res.setHeader('Content-Disposition', `attachment; filename=${encodeURIComponent(filePost.originalName)}`); // 直接返回数据库中存储的文件Buffer res.send(filePost.file); } catch (err) { return next(new HttpError("服务器异常", 500)); } };
注意:前后端分离跨域部署时,需要在CORS配置中添加Access-Control-Expose-Headers: Content-Disposition,否则前端无法读取响应头中的文件名信息
前端(React)实现
原生a标签的跳转请求无法携带自定义Authorization请求头,因此需要先通过fetch携带token拉取文件二进制内容,再手动创建临时a标签触发下载:
const handleDownload = async () => { try { const res = await fetch(`${process.env.REACT_APP_BACKEND_URL}/files/download/${props.id}`, { headers: { 'Authorization': auth.token }, // 如果鉴权token存在cookie中,打开下面这行配置 // credentials: 'include' }); if (!res.ok) throw new Error('下载请求失败'); // 从响应头解析文件名 const disposition = res.headers.get('Content-Disposition'); let fileName = '未命名文件'; if (disposition) { const matchRes = disposition.match(/filename\*?=(?:UTF-8'')?([^;]+)/i); if (matchRes) fileName = decodeURIComponent(matchRes[1]); } // 将响应转为二进制Blob对象 const fileBlob = await res.blob(); // 生成临时Blob地址 const tempUrl = window.URL.createObjectURL(fileBlob); // 创建临时a标签触发下载 const tempLink = document.createElement('a'); tempLink.href = tempUrl; tempLink.download = fileName; document.body.appendChild(tempLink); tempLink.click(); // 清理临时DOM和内存地址,避免内存泄漏 document.body.removeChild(tempLink); window.URL.revokeObjectURL(tempUrl); } catch (err) { console.error('下载失败:', err); // 此处可添加项目内的全局错误提示逻辑 } } return ( <button onClick={handleDownload}>Download File</button> )
常见踩坑点
- 禁止对二进制文件流调用
.json()方法解析,该方法仅能处理JSON格式响应,文件类响应必须使用.blob()或.arrayBuffer()解析 - 中文文件名必须经过
encodeURIComponent编码后放在响应头,前端拿到后再解码,否则会出现乱码 - 大文件下载场景下建议添加进度提示,避免用户重复点击
- 鉴权失败(401状态码)的逻辑可以在fetch的响应判断中统一处理,比如跳转到登录页
内容的提问来源于stack exchange,提问作者V.T
相关产品推荐
相关产品推荐

