文件上传与存储
Nuxt 模板中的图片上传与存储。
模板包含一个需要登录的图片上传示例和可复用上传 UI。接口目前写入本地
public/uploads;生产环境依赖上传前,请替换这一存储边界。
内置流程
POST /api/storage/upload-image 由
server/api/storage/upload-image.post.ts 实现:
requireAuth(event)拒绝未登录请求。readMultipartFormData筛选名为files的 part。- 每个文件必须声明
image/*MIME,且不超过 5 MB。 - 处理器用
crypto.randomUUID()生成文件名并写入public/uploads。 - 返回
{ 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 的内容会被拒绝。
- 迁移对象存储后,文件在部署/重启后及多实例间仍可用。
- 删除与保留行为符合产品政策。
