文件上传与存储

Nuxt 模板中的图片上传与存储。

模板包含一个需要登录的图片上传示例和可复用上传 UI。接口目前写入本地 public/uploads;生产环境依赖上传前,请替换这一存储边界。

内置流程

POST /api/storage/upload-imageserver/api/storage/upload-image.post.ts 实现:

  1. requireAuth(event) 拒绝未登录请求。
  2. readMultipartFormData 筛选名为 files 的 part。
  3. 每个文件必须声明 image/* MIME,且不超过 5 MB。
  4. 处理器用 crypto.randomUUID() 生成文件名并写入 public/uploads
  5. 返回 { urls: string[] };配置 runtimeConfig.public.baseUrl 时为绝对 URL, 否则为相对的 /uploads/... URL。

app/components/ai/ImageUploader.vue 向该路由提交 FormData,维护预览、上传、 失败状态,并把已上传 URL emit 给父组件。

当前处理器信任 multipart 声明的 MIME,不会检查文件签名。上线前应增加魔数 或图片解码校验。另外,上传 UI 默认允许 10 MB,而服务端限制为 5 MB;请在 产品中统一这两个值。

本地目录为什么不适合生产

public/uploads 只适合本地开发或单台持久化服务器。在 Vercel/serverless 与 可替换容器中它不持久,也不会在多实例之间共享;部署或重启后文件可能消失。

文件位于公开目录,返回的 URL 对所有人可读。认证只保护创建操作,不保护之后 的读取。

替换存储边界

保留路由中的 multipart 解析、认证、校验和响应结构,把本地 mkdir/writeFile 替换为对象存储适配器:

const object = await storage.put({
  key: `uploads/${crypto.randomUUID()}.${extension}`,
  body: file.data,
  contentType: detectedMime,
})

urls.push(object.publicUrl)

S3、Cloudflare R2 与 Vercel Blob 都适合这个边界。私有内容应保存对象 key 而非 公开 URL,并从授权读取接口返回短时签名 URL。

生产要求

  • **内容校验:**检测 JPEG/PNG/GIF/WebP 签名、实际解码图片、拒绝不匹配内容, 并按需清理敏感元数据。
  • **限制:**服务端限制单文件大小、数量、尺寸与用户总配额,不能只依赖浏览器。
  • **授权:**定义谁能上传、替换、读取和删除,并在数据库记录所有权。
  • **安全 key:**使用生成的 key 与明确的扩展名白名单,绝不写入用户提供的路径。
  • **生命周期:**清理废弃对象,处理替换,并定义保留与备份策略。
  • **分发:**配置 CDN/cache header,以及正确的公开域名或签名 URL 策略。
  • **防滥用:**增加限流、必要时的恶意文件扫描、日志与成本告警。

验证

  • 未登录上传返回 401。
  • 5 MB 以下有效图片返回可访问 URL。
  • 缺少 files、非图片 MIME、超限输入均返回 4xx。
  • 加入签名校验后,伪造 MIME 的内容会被拒绝。
  • 迁移对象存储后,文件在部署/重启后及多实例间仍可用。
  • 删除与保留行为符合产品政策。

相关页面

On this page