在完成了文件上传、验证、存储和图片处理之后,另一个同样关键的问题是:
用户如何安全地下载这些文件?谁可以访问哪些文件?
如果直接暴露文件真实路径或云存储 URL,往往会带来以下风险:
因此,在真实项目中,文件下载与访问控制几乎是文件系统的必备模块。本文将从实战角度,讲解如何在 Node.js 中设计一个安全、可控、可扩展的文件下载与访问控制机制。
一、为什么不能直接暴露文件地址
很多项目在早期会这样做:
/uploads/20240101-abc123.pdf
或者直接返回云存储的公网 URL。
这种方式存在明显问题:
一旦链接泄露,文件就相当于“公开资源”,几乎无法回收权限。
二、文件下载的基本设计思路
一个相对安全的下载流程通常包括:
这种方式可以在服务端对所有访问行为进行控制。
三、本地文件下载实现
1. 基本下载接口
const path = require('path');
const fs = require('fs');
app.get('/download/:id', async (req, res) => {
const fileId = req.params.id;
const file = awaitgetFileById(fileId); // 从数据库查询文件信息
if (!file) {
return res.status(404).json({ error: '文件不存在' });
}
const filePath = path.resolve(file.path);
if (!fs.existsSync(filePath)) {
return res.status(404).json({ error: '文件已丢失' });
}
res.download(filePath, file.originalName);
});
这里没有直接暴露真实路径,而是通过文件 ID 间接访问。
2. 使用流方式下载(大文件推荐)
app.get('/download/:id', async (req, res) => {
const file = awaitgetFileById(req.params.id);
if (!file) return res.status(404).end();
const filePath = path.resolve(file.path);
const stream = fs.createReadStream(filePath);
res.setHeader('Content-Disposition', `attachment; filename="${file.originalName}"`);
res.setHeader('Content-Type', 'application/octet-stream');
stream.pipe(res);
});
这种方式可以避免一次性加载大文件到内存。
四、基础访问控制实现
1. 用户身份校验
在下载接口中,必须先校验用户身份:
functionauthMiddleware(req, res, next) {
if (!req.user) {
return res.status(401).json({ error: '未登录' });
}
next();
}
路由中使用:
app.get('/download/:id', authMiddleware, downloadHandler);
2. 文件归属与权限校验
functioncheckFilePermission(user, file) {
return file.ownerId === user.id || user.role === 'admin';
}
在下载前进行判断:
if (!checkFilePermission(req.user, file)) {
return res.status(403).json({ error: '无权访问该文件' });
}
五、下载 Token 机制(防盗链核心)
为了防止文件链接被直接分享,可以引入一次性下载 Token。
1. 生成临时 Token
const crypto = require('crypto');
functiongenerateDownloadToken(fileId, userId) {
const raw = `${fileId}:${userId}:${Date.now()}`;
return crypto.createHash('md5').update(raw).digest('hex');
}
存储到 Redis 或数据库,并设置过期时间。
2. 使用 Token 下载
app.get('/download', async (req, res) => {
const { fileId, token } = req.query;
const record = awaitgetTokenRecord(token);
if (!record || record.fileId !== fileId) {
return res.status(403).json({ error: '无效或过期链接' });
}
const file = awaitgetFileById(fileId);
// 权限校验略
res.download(file.path, file.originalName);
});
这样即使链接被转发,过期后也无法继续使用。
六、云存储下载与访问控制
如果使用对象存储,推荐使用私有读 + 临时授权 URL。
1. 生成临时下载链接
以阿里云 OSS 为例:
constOSS = require('ali-oss');
const client = newOSS({
region: 'oss-cn-hangzhou',
accessKeyId: process.env.OSS_KEY,
accessKeySecret: process.env.OSS_SECRET,
bucket: 'my-bucket'
});
functiongenerateSignedUrl(objectKey) {
return client.signatureUrl(objectKey, {
expires: 60 * 5
});
}
2. 下载流程
这种方式性能最好,且无需后端转发大文件流量。
七、下载日志与审计
在企业级系统中,通常需要记录下载行为:
示例:
awaitsaveDownloadLog({
userId: req.user.id,
fileId: file.id,
ip: req.ip,
ua: req.headers['user-agent']
});
这在安全审计与纠纷处理时非常有价值。
八、常见安全风险与防护建议
1️⃣ 防止路径穿越
永远不要使用用户传入的路径直接访问文件。
2️⃣ 防止暴力下载
限制单 IP / 单用户下载频率。
3️⃣ 防止盗链
使用下载 Token 或云存储签名 URL。
4️⃣ 防止越权访问
严格校验文件归属关系。
5️⃣ 敏感文件加密存储
数据库备份、合同文件建议加密后存储。
九、完整下载流程总结
一个推荐的文件下载与访问控制流程:
通过这种方式,可以最大程度保证文件系统的安全性与可控性。
十、总结
在 Node.js 文件上传与处理系统中,下载与访问控制是比上传更重要的一环。如果缺乏权限校验与防盗链机制,任何一个上传系统都可能演变成数据泄露的入口。
通过引入:
可以构建一个安全、稳定、可审计的文件访问系统。
在《Node.js 编程实战》系列中,文件下载模块为后续的文件管理、权限体系与合规模块打下了坚实基础。