授权上传的重点是在不暴露长期密钥的前提下,让浏览器、App 或第三方客户端安全完成上传。
前端只负责采集文件、调用业务上传授权接口、使用 uploadUrl 提交文件,并在成功后把 objectKey 交回业务后端。前端不负责生成关键路径,也不负责保存长期密钥。
后端负责鉴权、生成文件路径、限制文件类型和目录范围、签发 uploadUrl、记录 requestId,并在上传完成后把文件与业务记录关联起来。
POST /api/storage/presign-upload
{
"fileName": "avatar.png",
"contentType": "image/png",
"resourceType": "user-avatar"
}{
"success": true,
"bucket": "media-assets",
"objectKey": "uploads/2026/06/avatar.png",
"uploadUrl": "https://your-upload-url",
"expiresIn": 600,
"requestId": "req_xxx"
}前端不要直接上传到存储服务,也不要自行拼接 objectKey。第一步应先请求业务后端,由后端完成鉴权和路径生成。
POST /api/storage/presign-upload
{
"fileName": "avatar.png",
"contentType": "image/png",
"resourceType": "user-avatar"
}后端根据用户身份、项目、业务类型和文件类型生成安全的文件路径,并返回 bucket、objectKey、uploadUrl、expiresIn 和 requestId。
{
"success": true,
"bucket": "media-assets",
"objectKey": "uploads/2026/06/avatar.png",
"uploadUrl": "https://your-upload-url",
"expiresIn": 600,
"requestId": "req_xxx"
}浏览器只负责拿 uploadUrl 执行 PUT 上传。上传成功后,再调用业务确认接口,把 objectKey 挂到用户、项目或素材记录上。
async function uploadFile(file: File) {
const authResponse = await fetch('/api/storage/presign-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
}),
});
const { uploadUrl, objectKey } = await authResponse.json();
await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type,
},
body: file,
});
await fetch('/api/assets/confirm-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ objectKey }),
});
}推荐由业务后端统一完成鉴权、路径生成和 uploadUrl 签发。下面是一段最小示例。
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const client = new S3Client({
region: 'auto',
endpoint: process.env.SURUN_S3_ENDPOINT,
credentials: {
accessKeyId: process.env.SURUN_ACCESS_KEY_ID!,
secretAccessKey: process.env.SURUN_SECRET_ACCESS_KEY!,
},
});
export async function createUploadAuthorization(params: {
fileName: string;
contentType: string;
userId: string;
}) {
const objectKey = 'uploads/' + params.userId + '/' + String(Date.now()) + '-' + params.fileName;
const command = new PutObjectCommand({
Bucket: 'media-assets',
Key: objectKey,
ContentType: params.contentType,
});
const uploadUrl = await getSignedUrl(client, command, { expiresIn: 600 });
return {
bucket: 'media-assets',
objectKey,
uploadUrl,
expiresIn: 600,
};
}前端只负责申请授权、提交文件和回传 objectKey,不应直接保存或计算长期凭证。
async function uploadFile(file: File) {
const authResponse = await fetch('/api/storage/presign-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
}),
});
const { uploadUrl, objectKey } = await authResponse.json();
await fetch(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type,
},
body: file,
});
await fetch('/api/assets/confirm-upload', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ objectKey }),
});
}优先检查后端签发时设置的 expiresIn 是否过短,以及前端是否在拿到授权后等待太久才开始上传。
通常要检查对象路径是否超出后端允许范围、Content-Type 是否不匹配,或者签发时的 bucket 与实际上传目标不一致。
这通常不是存储问题,而是上传完成后没有把 objectKey 回传业务后端确认入库。上传成功与业务完成是两件事。
建议固定资源类型和目录规则,例如 avatars/{userId}/。这样后续替换头像、清理旧资源和做访问控制都更简单。
建议 objectKey 里带上项目、工单或业务主键,而不是只保留文件名。这样后续确认入库和排查问题时不会丢失上下文。
流程保持一致,均应先通过业务后端获取 uploadUrl。变化的是调用端,而不是权限模型。
因为 objectKey 本质上就是业务路径。把它完全交给前端,意味着客户端可能写入不应开放的目录,也会让后续审计和治理失控。
因为对象传到存储里,只代表文件存在了,不代表业务系统已经认领它。很多场景都需要把 objectKey 与用户、项目、工单或素材记录绑定起来。
可以,但如果文件很大、网络不稳定或需要断点续传,通常更推荐走分段上传方案。
文档中心帮助你完成接入,客户服务页帮助你了解采购、支持与服务信息。