你的前端用户传文件,调用 s3.putObject() / storage.upload(),浏览器报 403:
AccessDenied: Access Denied
<RequestId>...</RequestId>
或者 Firebase Storage 提示:
storage/unauthorized: User does not have permission to access this object.
或者 Supabase:
new row violates row-level security policy "..."
存储 403 看似一样,但根因有三大类:bucket 策略 / IAM 权限 / 签名 URL。错误信息里通常已经透露线索,认真读它。
常见原因
按命中率从高到低:
1. bucket policy / Storage rules 太严
最常见。新 bucket 默认 deny-all 或 only-owner。前端用 anon key 上传根本没权限。
如何判断:
- S3:
aws s3api get-bucket-policy --bucket xxx看策略 - Firebase: Console → Storage → Rules
- Supabase: Dashboard → Storage → 选 bucket → Policies
2. IAM 角色 / 用户没 PutObject
S3 经典:你的 server 用 IAM Role 有 s3:GetObject 但漏了 s3:PutObject。GET 没事,PUT 403。
如何判断:AWS Console → IAM → 你的 Role → Policies → 看是否包含 s3:PutObject 和 s3:PutObjectAcl。
3. 签名 URL 过期 / 签错
const url = await getSignedUrl(s3, {
Bucket: 'x', Key: 'y',
Expires: 60 // 60 秒过期
});
// 用户 5 分钟后点上传 → 过期 403
或者签名时用错 secret key、错 region、错 bucket name。
如何判断:上传失败时 URL 已经创建超过 expires 时间。
4. CORS 拦截浏览器直传
浏览器跨域 PUT 到 S3 需要 bucket CORS 允许。新 bucket 默认 CORS 是禁的。
如何判断:浏览器 console 报 CORS error 而非 403。
5. 文件大小 / Content-Type 不在允许范围
某些策略限制 Content-Length / Content-Type:
"Condition": {
"NumericLessThan": {"s3:content-length-range": 5242880} // 5MB
}
传 10MB 就拒。
如何判断:错误含 “ContentLengthExceeded” 或 “ContentType not allowed”。
6. anon 用户上传到要 auth 的路径
Firebase Storage / Supabase 常按路径分权限:
allow write: if request.auth != null && request.auth.uid == userId;
未登录用户传到 /users/{uid}/ 必然失败。
如何判断:错误指向 Storage rules 里的具体行号。
最短修复路径
Step 1:抓完整错误响应
# 重新触发上传
# 浏览器 Network → 找失败请求 → Response
# S3 示例
<?xml version="1.0" encoding="UTF-8"?>
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>...</RequestId>
<Resource>/your-bucket/your-key</Resource>
</Error>
S3 的 Code 和 Message 提示具体原因;Firebase/Supabase 会直接说”policy X denied”。
Step 2:S3 IAM 修复
# 1. 看当前 role 权限
aws iam get-role-policy --role-name your-role --policy-name your-policy
# 2. 加 PutObject
cat > /tmp/upload-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:PutObjectAcl"],
"Resource": "arn:aws:s3:::your-bucket/*"
}]
}
EOF
aws iam put-role-policy \
--role-name your-role \
--policy-name upload-policy \
--policy-document file:///tmp/upload-policy.json
Step 3:Firebase Storage Rules
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
// 用户文件夹:只有本人能传
match /users/{userId}/{allPaths=**} {
allow read: if request.auth.uid == userId;
allow write: if request.auth.uid == userId
&& request.resource.size < 10 * 1024 * 1024
&& request.resource.contentType.matches('image/.*');
}
// 公开 assets
match /public/{allPaths=**} {
allow read: if true;
allow write: if false;
}
}
}
部署:firebase deploy --only storage
Step 4:Supabase Storage Policies
-- Dashboard SQL editor
INSERT INTO storage.objects (bucket_id, name, owner)
VALUES ('avatars', 'test.jpg', auth.uid());
-- 加 policy
CREATE POLICY "Users upload to own folder" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK (
bucket_id = 'avatars'
AND (storage.foldername(name))[1] = auth.uid()::text
);
或在 Dashboard → Storage → bucket → Policies 用 UI 加。
Step 5:签名 URL 修复
// 上传前刚生成
async function uploadFile(file: File) {
const { url } = await fetch('/api/get-upload-url', {
method: 'POST',
body: JSON.stringify({ filename: file.name })
}).then(r => r.json());
// 立即上传,不要拖
await fetch(url, { method: 'PUT', body: file });
}
// server 端
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
const url = await getSignedUrl(s3Client, new PutObjectCommand({
Bucket: 'x',
Key: filename,
}), { expiresIn: 900 }); // 15 分钟
Step 6:S3 CORS
cat > /tmp/cors.json <<EOF
{
"CORSRules": [{
"AllowedOrigins": ["https://yourdomain.com"],
"AllowedMethods": ["PUT", "POST"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3600
}]
}
EOF
aws s3api put-bucket-cors --bucket your-bucket \
--cors-configuration file:///tmp/cors.json
Step 7:客户端时钟检查
签名验证依赖系统时间。如果 client 时钟飘 > 15 分钟:
# macOS
sudo sntp -sS time.apple.com
# Linux
sudo timedatectl set-ntp true
预防建议
- bucket policy / Storage rules 进 git,PR review 能看到权限变更
- 上传路径文档化:哪个 bucket、什么 path、谁能写、谁能读
- 签名 URL expiry 设短(5-15 分钟),客户端拿到立刻上传
- 浏览器直传必配 CORS,开发时就用生产 domain 测
- IAM 最小权限:分开 read-only role 和 write role
- 不要在前端代码硬编码 AWS secret key(用签名 URL 或 STS)
- 监控 4xx upload 比例,> 1% 就有规则错
- 用 emulator / local stack 在本地复现 S3 / Firebase rules,避免生产试错