S3 预签名 URL 大文件上传中途 403

AWS S3 预签名 URL 小文件没事、大文件传到一半 403。修法:加长 TTL、改 multipart 上传、或者直接用 SDK 的上传管理器。

浏览器对一条预签名 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 GBmultipart,每段一条预签名 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。

标签: #后端 #排查 #s3