Supabase Storage实战:文件上传、权限控制与CDN加速
那天凌晨三点,我盯着控制台报错信息发呆。用户头像上传功能上线半小时,就有用户反馈:所有人的头像都变成同一个人的了。
排查下来,问题出在 Storage 的 RLS Policy 配置——我压根没配置。bucket 是公开的,上传路径没做用户隔离,任何人的上传操作都能覆盖别人的文件。说白了,一个权限配置的疏忽,差点酿成生产事故。
Supabase Storage 这东西,用起来简单,但要真正用好——权限控制、CDN 加速、图片转换——这里面的坑可不少。这篇文章把我踩过的坑、摸索出的经验,一次说清楚。
一、快速上手:标准文件上传
先说最基础的——把文件传上去。
创建 Bucket
打开 Supabase 控制台,左侧菜单找到 Storage,点击 “New bucket”。给 bucket 起个名字,比如 avatars 存头像,posts 存文章配图。这里有个选项问你 “Make this bucket public?”——先别急着勾选,后面权限章节会详细讲。
我的习惯是:私有 bucket 放敏感文件,公开 bucket 放静态资源。默认先创建私有 bucket,后面按需调整。
SDK 上传代码
假设你已经装好 @supabase/supabase-js,代码写起来很简单:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
// 上传文件
async function uploadFile(file: File) {
const filePath = `uploads/${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('avatars') // bucket 名称
.upload(filePath, file, {
cacheControl: '3600', // 缓存1小时
upsert: false // 文件已存在则报错,不覆盖
})
if (error) {
console.error('上传失败:', error.message)
return null
}
return data.path // 返回文件路径
}
说实话,这段代码我写了不下十遍。关键是 filePath 的设计——后面会讲为什么用时间戳前缀,以及如何做用户隔离。
文件大小限制
官方文档说标准上传支持最大 5GB 文件。但实际用下来,小于 6MB 的文件用标准上传,体验最好;超过 6MB,建议用 TUS 协议的断点续传。
TUS 是什么?简单说,就是大文件上传时支持断点续传。网络断了,重新连上后接着传,不用从头来。这对于视频、大图这类文件,体验好太多了——想象一下用户传了 90% 的进度突然断网,没有 TUS,只能重传。
启用 TUS 上传需要额外配置,如果你暂时用不上,标准上传够应付大部分场景了。
// TUS 上传示例(大文件推荐)
const { data, error } = await supabase.storage
.from('videos')
.upload('large-video.mp4', file, {
duplex: 'half', // 启用流式上传
// TUS 会自动处理断点续传
})
二、安全配置:RLS Policy 详解
回到我凌晨三点踩的那个坑——权限没配置,文件随便覆盖。
Supabase 的 Storage 和数据库一样,底层都用 PostgreSQL。所以权限控制也是 RLS(Row Level Security)那一套。bucket 相当于一张表,每个文件是一条记录。
Bucket 的公开与私有
创建 bucket 时有个选项:“Public bucket” 还是 “Private bucket”。
Public bucket:任何人都能读取,不需要认证。适合存放公开的头像、网站 Logo 这类静态资源。
Private bucket:需要认证才能访问。但这里有个坑——认证只是门槛,具体谁能读、谁能写,还要看 RLS Policy。
我的建议:除非文件真的完全公开,否则默认创建 Private bucket。权限配好了再公开,比公开了再补救,安全得多。
RLS Policy 的几种类型
在 Storage 的 Policy 页面,你会看到四种操作:
- SELECT:读取文件(下载、获取 URL)
- INSERT:上传新文件
- UPDATE:更新/覆盖已有文件
- DELETE:删除文件
每种操作都可以单独配置 Policy。最常见的配置模式:
-- 用户只能操作自己的文件
CREATE POLICY "Users manage own files"
ON storage.objects FOR ALL
USING (auth.uid()::text = (storage.foldername(name))[1]);
这段 SQL 看起来有点复杂,拆开说:
auth.uid()获取当前登录用户的 IDstorage.foldername(name)提取文件路径的第一级目录名- 比如文件路径是
user123/avatar.jpg,第一级目录就是user123
所以整个 Policy 的逻辑是:只有当文件路径的第一级目录等于用户 ID 时,用户才能操作这个文件。这就是用户隔离的核心思路。
用户隔离的实现
具体怎么做?上传时把用户 ID 放在路径第一级:
async function uploadAvatar(userId: string, file: File) {
// 路径设计:用户ID/文件名
const filePath = `${userId}/avatar-${Date.now()}.jpg`
const { data, error } = await supabase.storage
.from('avatars')
.upload(filePath, file)
return data?.path
}
这样一来,每个用户的文件都在自己的”文件夹”下。RLS Policy 只允许用户操作以自己 ID 开头的路径,别人的文件碰不到。
生成带签名的访问 URL
Private bucket 的文件,直接访问会报 404。需要生成带签名的 URL:
// 生成临时访问链接(有效期1小时)
const { data, error } = await supabase.storage
.from('avatars')
.createSignedUrl('user123/avatar.jpg', 3600)
console.log(data?.signedUrl) // 带签名的完整 URL
签名的有效期自己定。太长不安全,太短用户体验差。一般设 1-4 小时比较合适。
如果你希望文件完全公开但又不想改 bucket 设置,可以用 getPublicUrl:
const { data } = supabase.storage
.from('public-assets')
.getPublicUrl('logo.png')
// 这个 URL 不需要签名,任何人都能访问
Policy 配置的常见坑
踩过的坑列几个:
-
忘了配 INSERT Policy:用户能登录,但上传不了文件。报错信息是 “new row violates row-level security policy”
-
Policy 写太宽松:比如用
USING (true),等于所有文件所有人都能操作。这和没配 RLS 一个效果。 -
路径设计不合理:如果用户 ID 不在路径第一级,RLS 的
foldername提取就失效。我之前踩过这个坑,路径写成uploads/user123/file.jpg,结果提取出来的是uploads,Policy 判断就错了。
配置 Policy 时,先在控制台的 SQL 编辑器里测试,确认逻辑没问题,再应用到正式环境。
三、性能提升:Smart CDN 与图片转换
文件能上传了,权限也配好了。接下来要考虑的是:怎么让文件加载更快?
Smart CDN 原理
Supabase 的 Smart CDN 不是普通的 CDN。它会根据文件的访问频率自动决定缓存策略:热门文件缓存时间长,冷门文件缓存时间短。
官方文档说,缓存失效的全球同步时间最多 60 秒。这意味着你在东京更新了文件,60 秒内纽约的用户也能看到最新版本。比传统 CDN 的几分钟甚至几小时,快了不少。
但 Smart CDN 是付费功能,需要 Pro Plan(每月 $25)。如果你是 Free Plan,文件还是能访问,只是没有 CDN 加速,直接从 Supabase 的服务器读取。
图片转换参数
这功能我很喜欢——不用自己处理图片缩放、裁剪,Supabase 直接在 URL 上加参数就行。
基础参数:
?width=300&height=200 // 指定尺寸
?resize=contain // 保持比例,不裁剪
?resize=cover // 填满尺寸,裁剪多余部分
?quality=80 // 图片质量(1-100)
?format=webp // 转成 WebP 格式,体积更小
组合起来用:
const baseUrl = supabase.storage
.from('avatars')
.getPublicUrl('user123/avatar.jpg').data.publicUrl
// 生成缩略图
const thumbnailUrl = `${baseUrl}?width=100&height=100&resize=cover`
图片转换的限制:
- 尺寸范围:1 到 2500 像素
- 原文件大小:不超过 25MB
- 兼容格式:JPEG、PNG、WebP、GIF、AVIF
超过限制会报错。我有一次上传了一张 30MB 的原图想缩放,直接被拒了。
计费:每个项目免费额度
图片转换是按转换次数计费的,不是按存储大小。
每个项目每月前 100 张图片转换免费。超过后,每 1000 张收费 $5。
说实话,对于个人项目或小团队,100 张基本够用。我自己的博客项目,每月也就转换几十张头像和配图。除非你在做类似 Instagram 的图片社交应用,否则不太需要担心这个费用。
Next.js 集成:Image Loader
如果你用 Next.js,可以配置 Supabase 的 Image Loader,让 next/image 自动处理图片转换:
// next.config.js
module.exports = {
images: {
loader: 'custom',
loaderFile: './supabase-image-loader.js',
}
}
然后写 loader 文件:
// supabase-image-loader.js
export default function supabaseLoader({ src, width, quality }) {
const params = new URLSearchParams()
params.set('width', width.toString())
params.set('quality', (quality || 75).toString())
params.set('format', 'webp')
return `${src}?${params.toString()}`
}
这样在 Next.js 里用 <Image src="..." width={300} />,自动就会加上转换参数。
Pro Plan 门槛
前面说的 Smart CDN 和图片转换,都需要 Pro Plan。Free Plan 用户只能用基础的上传、下载功能。
要不要升级?看你的项目需求。如果只是存几张头像,Free Plan 完够。但如果要处理大量图片,做性能调优,Pro Plan 的 CDN 加速和图片转换确实能省不少事——不用自己搭 CDN,不用自己写图片处理服务。
我自己的选择:项目上线前先用 Free Plan 测试,等流量稳定了再升级 Pro。毕竟 $25/月不是小数目。
四、实战案例:博客项目的完整配置
说这么多理论,不如看一个完整案例。这是我博客项目的 Storage 配置,从零到可用。
场景:用户头像 + 文章配图
需要两个 bucket:
avatars:用户头像,私有 bucket,用户只能操作自己的头像post-images:文章配图,私有 bucket,作者能上传,任何人能读取(用签名 URL)
Step 1:创建 Bucket
控制台操作:
- Storage > New bucket > 名称输入
avatars,勾选 Private - 同样方式创建
post-images
Step 2:配置 RLS Policy
avatars bucket 的 Policy:
-- 允许用户读取所有头像(公开读取)
CREATE POLICY "Anyone can view avatars"
ON storage.objects FOR SELECT
USING (bucket_id = 'avatars');
-- 用户只能上传和更新自己的头像
CREATE POLICY "Users manage own avatar"
ON storage.objects FOR INSERT
WITH CHECK (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);
-- 用户只能删除自己的头像
CREATE POLICY "Users delete own avatar"
ON storage.objects FOR DELETE
USING (bucket_id = 'avatars' AND auth.uid()::text = (storage.foldername(name))[1]);
post-images bucket 的 Policy:
-- 作者能上传文章配图(假设作者有 author 角色)
CREATE POLICY "Authors can upload post images"
ON storage.objects FOR INSERT
WITH CHECK (
bucket_id = 'post-images'
AND auth.jwt() ->> 'role' = 'author'
);
-- 所有人能读取文章配图
CREATE POLICY "Public read post images"
ON storage.objects FOR SELECT
USING (bucket_id = 'post-images');
Step 3:前端上传代码
头像上传组件:
async function handleAvatarUpload(file: File) {
const user = await supabase.auth.getUser()
if (!user.data.user) return alert('请先登录')
// 路径:用户ID/avatar.jpg(固定文件名,每次上传覆盖旧头像)
const filePath = `${user.data.user.id}/avatar.jpg`
const { error } = await supabase.storage
.from('avatars')
.upload(filePath, file, { upsert: true })
if (!error) {
// 获取公开 URL(因为 SELECT Policy 允许所有人读取)
const url = supabase.storage.from('avatars').getPublicUrl(filePath)
setUserAvatar(url.data.publicUrl)
}
}
文章配图上传:
async function handlePostImageUpload(file: File) {
const filePath = `posts/${Date.now()}-${file.name}`
const { data, error } = await supabase.storage
.from('post-images')
.upload(filePath, file)
if (!error) {
// 生成签名 URL,有效期 24 小时
const { data: urlData } = await supabase.storage
.from('post-images')
.createSignedUrl(filePath, 86400)
insertImageToEditor(urlData?.signedUrl)
}
}
Step 4:测试验证
上线前验证几个关键点:
- 未登录用户能否看到文章配图?(应该能,SELECT Policy 允许)
- 普通用户能否上传文章配图?(应该不能,只有 author 角色)
- 用户 A 能否覆盖用户 B 的头像?(应该不能,路径隔离)
每个点都测一遍,确认 Policy 配置无误。凌晨三点的教训,我不想再经历第二次。
总结
Supabase Storage 的核心三件事:上传、权限、加速。
上传最简单,几行代码搞定。但权限配置要多花点心思——RLS Policy 不是配一次就完事,要结合业务场景反复测试。CDN 和图片转换是锦上添花,Pro Plan 才能用,但确实能省不少开发时间。
我的经验是:先把基础的上传和权限搞定,确保不出安全事故。后面有性能需求再加 CDN,有图片处理需求再加转换。一步步来,别贪多。
如果你也在用 Supabase Storage,欢迎分享你的踩坑经历。我那个凌晨三点的教训,应该不止我一个人遇到过。
Supabase Storage 完整配置流程
从创建 Bucket 到配置权限再到 CDN 加速的完整实战
⏱️ 预计耗时: 30 分钟
- 1
步骤1: 创建 Bucket
在 Supabase 控制台创建私有 Bucket:
• 进入 Storage > New bucket
• 输入名称(如 avatars)
• 勾选 Private(推荐默认私有)
• 点击 Create bucket - 2
步骤2: 配置 RLS Policy
为 Bucket 配置 Row Level Security:
• 进入 Storage > 选择 Bucket > Policies
• 点击 New Policy
• 选择操作类型(SELECT/INSERT/UPDATE/DELETE)
• 编写 Policy 规则(如用户隔离)
• 测试后应用到正式环境 - 3
步骤3: 上传文件
使用 SDK 上传文件:
• 设计路径结构(如 userId/filename)
• 调用 storage.from().upload()
• 设置 cacheControl 和 upsert 参数
• 处理上传错误和返回路径 - 4
步骤4: 配置 CDN 和图片转换(可选)
升级 Pro Plan 后可使用高级功能:
• Smart CDN 自动缓存热门文件
• 图片转换 URL 参数(width/height/format)
• Next.js Image Loader 集成
• 监控免费额度(100张/月)
常见问题
Public bucket 和 Private bucket 有什么区别?
如何配置 RLS Policy 实现用户隔离?
• 上传路径设计为 userId/filename
• Policy 使用 auth.uid()::text = (storage.foldername(name))[1]
• 这样用户只能操作以自己 ID 开头的路径
Private bucket 的文件如何对外分享?
文件上传有什么限制?
图片转换支持哪些参数和限制?
• width/height:尺寸(范围 1-2500 像素)
• resize:contain(保持比例)或 cover(裁剪填满)
• quality:质量(1-100)
• format:webp/jpeg/png/gif/avif
限制:原文件不超过 25MB
Smart CDN 和图片转换需要付费吗?
10 分钟阅读 · 发布于: 2026年4月9日 · 修改于: 2026年4月9日
相关文章
Docker Compose 多服务编排:本地开发环境一键启动
Docker Compose 多服务编排:本地开发环境一键启动
n8n 进阶实战:Webhook 触发与 IF/Switch 条件分支设计
n8n 进阶实战:Webhook 触发与 IF/Switch 条件分支设计
GitHub Actions Matrix 矩阵构建:多版本并行测试实战

评论
使用 GitHub 账号登录后即可评论