浏览器对一条预签名 URL 用 PUT 上传 500 MB 视频。前 200 MB 顺利,然后返回 403 Forbidden,body 写着 Request has expired。这条签名 URL TTL 是 5 分钟——传缩略图够用,传大文件就远远不够。修法是别再用单段 PUT 传超过 100 MB 的东西。改用 S3 multipart 上传、每段一条预签名 URL;或者把活交给 SDK 的上传管理器,分块、并行、重试都帮你做。
常见原因
按踩坑频率排序。
1. URL TTL 比上传时间短
签名 URL 5 分钟过期。用户 5 Mbps 的上行需要 800 秒才能推完 500 MB。传到一半 S3 看 X-Amz-Date + X-Amz-Expires 已经过期,直接拒。
怎么判断:响应 body 是 <Error><Code>AccessDenied</Code><Message>Request has expired</Message></Error>,URL 上能看到 X-Amz-Expires。
2. 单段 PUT 传超过 5 GB
S3 单段 PUT 硬上限 5 GB。就算 TTL 改了,6 GB 文件永远失败。
怎么判断:错误码 EntityTooLarge。
3. 签发方时钟偏移
签发方(你的后端)时钟慢了,S3 看到签名一来就已经过期。
怎么判断:所有签名 URL 都 403,连小文件也跑不通;X-Amz-Date 显示已经是过去时间。
4. CORS preflight 把预算耗掉
浏览器 PUT 之前先发 OPTIONS 预检。桶 CORS 不允许 PUT,上传根本没开始、签名 URL 的时钟已经在走了。
怎么判断:Network 里 preflight 403,或者根本没发 PUT。
5. 用了 SigV2、但 region 只认 SigV4
老代码用了 v2 签名。2014 之后的新 region、KMS 加密桶都强制 v4。
怎么判断:InvalidRequest: The authorization mechanism you have provided is not supported。
最短修复路径
Step 1: 按文件大小选模式
| 文件大小 | 模式 |
|---|---|
| 小于 100 MB | 单段 PUT 预签名 URL,TTL 15 分钟 |
| 100 MB - 5 GB | multipart,每段一条预签名 URL |
| 大于 5 GB | 必须 multipart |
5 分钟 TTL 对面向用户的上传太紧。默认 900 秒(15 分钟)。
Step 2: 单段 PUT 配合合理 TTL
# Python boto3——单段 PUT 预签名 URL
import boto3
from botocore.client import Config
s3 = boto3.client('s3', region_name='us-east-1',
config=Config(signature_version='s3v4'))
url = s3.generate_presigned_url(
'put_object',
Params={'Bucket': 'uploads', 'Key': 'video.mp4', 'ContentType': 'video/mp4'},
ExpiresIn=900, # 15 分钟
HttpMethod='PUT',
)
前端直接 fetch(url, { method: 'PUT', body: file, headers: {'Content-Type': 'video/mp4'} })。
Step 3: Multipart 上传,每段一条预签名
大文件就该这么传。每段一条签名 URL,浏览器并行推,后端做 complete。
# 1. 启动
mpu = s3.create_multipart_upload(Bucket='uploads', Key='video.mp4', ContentType='video/mp4')
upload_id = mpu['UploadId']
# 2. 给每段签 URL(比如 8 MB 一段)
def sign_part(part_number):
return s3.generate_presigned_url(
'upload_part',
Params={
'Bucket': 'uploads',
'Key': 'video.mp4',
'UploadId': upload_id,
'PartNumber': part_number,
},
ExpiresIn=3600,
)
# 3. 客户端每段上传完,从响应头拿 ETag
# 4. 完成
s3.complete_multipart_upload(
Bucket='uploads', Key='video.mp4', UploadId=upload_id,
MultipartUpload={'Parts': [
{'PartNumber': 1, 'ETag': '"abc..."'},
{'PartNumber': 2, 'ETag': '"def..."'},
]},
)
浏览器对每段 PUT 上对应 URL,从响应头读 ETag,把清单回传给后端。
Step 4: 能用 SDK 上传管理器就别手搓
服务端上传别自己搓 multipart——用管理器。
// Node AWS SDK v3
import { S3Client } from '@aws-sdk/client-s3';
import { Upload } from '@aws-sdk/lib-storage';
import { createReadStream } from 'node:fs';
const s3 = new S3Client({ region: 'us-east-1' });
await new Upload({
client: s3,
params: {
Bucket: 'uploads',
Key: 'video.mp4',
Body: createReadStream('./video.mp4'),
ContentType: 'video/mp4',
},
queueSize: 4, // 并行段数
partSize: 8 * 1024 * 1024,
}).done();
管理器把分块、重试、失败 abort 全包了。
Step 5: 修时钟、修 CORS
# 签发方机器
timedatectl status
sudo chronyc makestep
{
"CORSRules": [
{
"AllowedOrigins": ["https://app.example.com"],
"AllowedMethods": ["GET", "PUT", "POST"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
}
ExposeHeaders: ["ETag"] 必须有,不然浏览器读不到 ETag,complete 时拼不出来。
预防
- 默认 TTL:单段 PUT 15 分钟,multipart 段 1 小时。
- 超过 100 MB 走 multipart,每段 8-16 MB。
- 签发方机器跑 NTP,时钟偏移超过 1 秒告警。
- 永远显式
signature_version='s3v4',连 SigV4 端点。 - 服务端上传优先用 SDK
Upload/TransferManager,别手搓 multipart。