Node.js 编程实战:文件上传与处理系统 —— 下载与访问控制

37 阅读4分钟

在完成了文件上传、验证、存储和图片处理之后,另一个同样关键的问题是: 用户如何安全地下载这些文件?谁可以访问哪些文件?

如果直接暴露文件真实路径或云存储 URL,往往会带来以下风险:

  • 任意用户可下载私有文件
  • 文件链接被外部盗用
  • 资源被恶意刷流量
  • 敏感数据泄露

因此,在真实项目中,文件下载与访问控制几乎是文件系统的必备模块。本文将从实战角度,讲解如何在 Node.js 中设计一个安全、可控、可扩展的文件下载与访问控制机制。


一、为什么不能直接暴露文件地址

很多项目在早期会这样做:

/uploads/20240101-abc123.pdf

或者直接返回云存储的公网 URL。

这种方式存在明显问题:

  • 没有权限校验
  • 链接可被随意分享
  • 无法控制访问次数
  • 难以实现下载日志与审计

一旦链接泄露,文件就相当于“公开资源”,几乎无法回收权限。


二、文件下载的基本设计思路

一个相对安全的下载流程通常包括:

  1. 前端请求下载接口
  2. 后端校验用户身份
  3. 校验文件归属或权限
  4. 校验文件状态(是否删除、是否过期)
  5. 记录下载日志
  6. 返回文件流或临时下载地址

这种方式可以在服务端对所有访问行为进行控制。


三、本地文件下载实现

1. 基本下载接口

const path = require('path');
const fs = require('fs');

app.get('/download/:id', async (req, res) => {
  const fileId = req.params.id;

  const file = await getFileById(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 = await getFileById(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. 用户身份校验

在下载接口中,必须先校验用户身份:

function authMiddleware(req, res, next) {
  if (!req.user) {
    return res.status(401).json({ error: '未登录' });
  }
  next();
}

路由中使用:

app.get('/download/:id', authMiddleware, downloadHandler);

2. 文件归属与权限校验

function checkFilePermission(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');

function generateDownloadToken(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 = await getTokenRecord(token);
  if (!record || record.fileId !== fileId) {
    return res.status(403).json({ error: '无效或过期链接' });
  }

  const file = await getFileById(fileId);
  // 权限校验略

  res.download(file.path, file.originalName);
});

这样即使链接被转发,过期后也无法继续使用。


六、云存储下载与访问控制

如果使用对象存储,推荐使用私有读 + 临时授权 URL

1. 生成临时下载链接

以阿里云 OSS 为例:

const OSS = require('ali-oss');

const client = new OSS({
  region: 'oss-cn-hangzhou',
  accessKeyId: process.env.OSS_KEY,
  accessKeySecret: process.env.OSS_SECRET,
  bucket: 'my-bucket'
});

function generateSignedUrl(objectKey) {
  return client.signatureUrl(objectKey, {
    expires: 60 * 5
  });
}

2. 下载流程

  1. 前端请求后端下载接口
  2. 后端校验权限
  3. 生成临时 URL
  4. 返回给前端
  5. 前端直接访问云存储

这种方式性能最好,且无需后端转发大文件流量。


七、下载日志与审计

在企业级系统中,通常需要记录下载行为:

  • 下载人
  • 下载时间
  • 下载文件
  • IP 地址
  • User-Agent

示例:

await saveDownloadLog({
  userId: req.user.id,
  fileId: file.id,
  ip: req.ip,
  ua: req.headers['user-agent']
});

这在安全审计与纠纷处理时非常有价值。


八、常见安全风险与防护建议

1️⃣ 防止路径穿越 永远不要使用用户传入的路径直接访问文件。

2️⃣ 防止暴力下载 限制单 IP / 单用户下载频率。

3️⃣ 防止盗链 使用下载 Token 或云存储签名 URL。

4️⃣ 防止越权访问 严格校验文件归属关系。

5️⃣ 敏感文件加密存储 数据库备份、合同文件建议加密后存储。


九、完整下载流程总结

一个推荐的文件下载与访问控制流程:

  1. 前端请求下载接口
  2. 校验用户身份
  3. 校验文件权限
  4. 校验文件状态
  5. 生成下载 Token 或签名 URL
  6. 记录下载日志
  7. 返回文件流或临时链接

通过这种方式,可以最大程度保证文件系统的安全性与可控性。


十、总结

在 Node.js 文件上传与处理系统中,下载与访问控制是比上传更重要的一环。如果缺乏权限校验与防盗链机制,任何一个上传系统都可能演变成数据泄露的入口。

通过引入:

  • 身份认证
  • 权限校验
  • 下载 Token
  • 云存储签名 URL
  • 下载日志

可以构建一个安全、稳定、可审计的文件访问系统。

在《Node.js 编程实战》系列中,文件下载模块为后续的文件管理、权限体系与合规模块打下了坚实基础。