如何通过Next.js/Node.js/TS正确上传文件至Amazon S3?
正确实现Next.js + TypeScript 上传图片到S3的方案
一、原代码的问题分析
- 未触发实际上传:
s3.upload()返回的是ManagedUpload对象而非直接Promise,必须调用.promise()才能通过await等待上传完成,原代码缺少这一步,导致代码执行完但上传未真正触发。 - 密钥暴露风险:前端直接嵌入IAM密钥会导致密钥泄露,这是AWS严格禁止的,也是旧上传方式被弃用的核心原因。
- 类型不严谨:
selectedFile用any类型,建议改为File | null提升代码健壮性。 - 权限与CORS问题:需确保IAM用户拥有
s3:PutObject权限,且存储桶CORS配置允许前端域名的PUT请求。
二、推荐方案:预签名URL上传(符合AWS最佳实践)
预签名URL由后端(Next.js API路由)生成临时上传凭证,前端通过该URL直接向S3发起PUT请求,全程不暴露IAM密钥,安全且合规。
步骤1:配置IAM权限与S3 CORS
- IAM权限策略:给你的IAM用户添加以下权限(替换
your-bucket-name):
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::your-bucket-name/*" }, { "Effect": "Allow", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::your-bucket-name/*" } ] }
- S3 CORS配置:在存储桶「权限」→「跨域资源共享(CORS)」中添加规则(允许前端域名的PUT/GET请求):
[ { "AllowedHeaders": ["*"], "AllowedMethods": ["PUT", "GET"], "AllowedOrigins": ["http://localhost:3000"], // 生产环境替换为你的前端域名 "ExposeHeaders": [] } ]
步骤2:创建Next.js API路由生成预签名URL
使用AWS SDK v3(v2已弃用),在pages/api/s3-presigned-url.ts(App Router对应app/api/s3-presigned-url/route.ts)编写后端逻辑:
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import type { NextApiRequest, NextApiResponse } from "next"; export default async function handler( req: NextApiRequest, res: NextApiResponse ) { if (req.method !== "POST") return res.status(405).json({ message: "Method not allowed" }); const { fileName, fileType } = req.body; if (!fileName || !fileType) return res.status(400).json({ message: "Missing fileName or fileType" }); const s3Client = new S3Client({ region: process.env.S3_REGION as string, credentials: { accessKeyId: process.env.S3_ACCESS_KEY as string, secretAccessKey: process.env.S3_SECRET_KEY as string, }, }); try { const command = new PutObjectCommand({ Bucket: process.env.S3_BUCKET_NAME as string, Key: fileName, ContentType: fileType, }); const signedUrl = await getSignedUrl(s3Client, command, { expiresIn: 300 }); // URL有效期300秒 res.status(200).json({ signedUrl }); } catch (error) { console.error("生成预签名URL失败:", error); res.status(500).json({ message: "Failed to generate presigned URL" }); } }
步骤3:前端上传逻辑(TypeScript)
修改组件代码,先获取预签名URL再上传文件:
import { useState } from "react"; import { SubmitHandler } from "react-hook-form"; import type { RegisterFormSchemaType } from "./types"; export default function UploadForm() { const [selectedFile, setSelectedFile] = useState<File | null>(null); const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => { const file = e.target.files?.[0]; if (file) setSelectedFile(file); }; const onSubmit: SubmitHandler<RegisterFormSchemaType> = async (data) => { if (!selectedFile) return; try { // 1. 获取预签名URL const presignedRes = await fetch("/api/s3-presigned-url", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fileName: `uploads/${Date.now()}-${selectedFile.name}`, // 加前缀避免文件名冲突 fileType: selectedFile.type, }), }); const { signedUrl } = await presignedRes.json(); // 2. 直接上传到S3 const uploadRes = await fetch(signedUrl, { method: "PUT", body: selectedFile, headers: { "Content-Type": selectedFile.type }, }); uploadRes.ok ? console.log("上传成功") : console.error("上传失败"); } catch (error) { console.error("上传出错:", error); } }; return ( <form onSubmit={onSubmit}> <input type="file" onChange={handleFileUpload} accept="image/*" /> <button type="submit">上传图片</button> </form> ); }
步骤4:配置环境变量
在项目根目录.env.local中添加:
S3_ACCESS_KEY=你的IAM访问密钥 S3_SECRET_KEY=你的IAM秘密密钥 S3_REGION=存储桶区域(如us-east-1) S3_BUCKET_NAME=你的存储桶名称
三、预签名URL的优势
- 安全:IAM密钥仅在后端使用,不会暴露给前端用户。
- 合规:基于AWS SDK v3,符合官方最新规范。
- 可控:可自定义URL有效期、上传路径、文件类型等规则。
四、常见问题排查
- CORS错误:检查S3 CORS配置是否允许前端域名的PUT请求。
- 预签名URL生成失败:确认IAM权限包含
s3:PutObject,环境变量配置正确。 - 上传失败:检查文件类型是否匹配、URL是否过期、文件名是否含特殊字符。
内容的提问来源于stack exchange,提问作者Mathew
相关产品推荐
相关产品推荐

