切换语言
切换主题

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() 获取当前登录用户的 ID
  • storage.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 配置的常见坑

踩过的坑列几个:

  1. 忘了配 INSERT Policy:用户能登录,但上传不了文件。报错信息是 “new row violates row-level security policy”

  2. Policy 写太宽松:比如用 USING (true),等于所有文件所有人都能操作。这和没配 RLS 一个效果。

  3. 路径设计不合理:如果用户 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

控制台操作:

  1. Storage > New bucket > 名称输入 avatars,勾选 Private
  2. 同样方式创建 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:测试验证

上线前验证几个关键点:

  1. 未登录用户能否看到文章配图?(应该能,SELECT Policy 允许)
  2. 普通用户能否上传文章配图?(应该不能,只有 author 角色)
  3. 用户 A 能否覆盖用户 B 的头像?(应该不能,路径隔离)

每个点都测一遍,确认 Policy 配置无误。凌晨三点的教训,我不想再经历第二次。

总结

Supabase Storage 的核心三件事:上传、权限、加速。

上传最简单,几行代码搞定。但权限配置要多花点心思——RLS Policy 不是配一次就完事,要结合业务场景反复测试。CDN 和图片转换是锦上添花,Pro Plan 才能用,但确实能省不少开发时间。

我的经验是:先把基础的上传和权限搞定,确保不出安全事故。后面有性能需求再加 CDN,有图片处理需求再加转换。一步步来,别贪多。

如果你也在用 Supabase Storage,欢迎分享你的踩坑经历。我那个凌晨三点的教训,应该不止我一个人遇到过。

Supabase Storage 完整配置流程

从创建 Bucket 到配置权限再到 CDN 加速的完整实战

⏱️ 预计耗时: 30 分钟

  1. 1

    步骤1: 创建 Bucket

    在 Supabase 控制台创建私有 Bucket:

    • 进入 Storage > New bucket
    • 输入名称(如 avatars)
    • 勾选 Private(推荐默认私有)
    • 点击 Create bucket
  2. 2

    步骤2: 配置 RLS Policy

    为 Bucket 配置 Row Level Security:

    • 进入 Storage > 选择 Bucket > Policies
    • 点击 New Policy
    • 选择操作类型(SELECT/INSERT/UPDATE/DELETE)
    • 编写 Policy 规则(如用户隔离)
    • 测试后应用到正式环境
  3. 3

    步骤3: 上传文件

    使用 SDK 上传文件:

    • 设计路径结构(如 userId/filename)
    • 调用 storage.from().upload()
    • 设置 cacheControl 和 upsert 参数
    • 处理上传错误和返回路径
  4. 4

    步骤4: 配置 CDN 和图片转换(可选)

    升级 Pro Plan 后可使用高级功能:

    • Smart CDN 自动缓存热门文件
    • 图片转换 URL 参数(width/height/format)
    • Next.js Image Loader 集成
    • 监控免费额度(100张/月)

常见问题

Public bucket 和 Private bucket 有什么区别?
Public bucket 任何人都能读取,适合公开的静态资源如 Logo、公开头像。Private bucket 需要认证才能访问,但具体权限由 RLS Policy 控制,更安全。
如何配置 RLS Policy 实现用户隔离?
核心思路是用户 ID 放在路径第一级:

• 上传路径设计为 userId/filename
• Policy 使用 auth.uid()::text = (storage.foldername(name))[1]
• 这样用户只能操作以自己 ID 开头的路径
Private bucket 的文件如何对外分享?
使用 createSignedUrl() 生成带签名的临时访问 URL,可设置有效期。太长不安全,太短体验差,一般设 1-4 小时。
文件上传有什么限制?
标准上传最大 5GB。小于 6MB 用标准上传体验最好,超过 6MB 建议用 TUS 协议的断点续传,网络中断后可继续上传。
图片转换支持哪些参数和限制?
支持的参数:

• width/height:尺寸(范围 1-2500 像素)
• resize:contain(保持比例)或 cover(裁剪填满)
• quality:质量(1-100)
• format:webp/jpeg/png/gif/avif

限制:原文件不超过 25MB
Smart CDN 和图片转换需要付费吗?
是的,需要 Pro Plan($25/月)。Free Plan 只能用基础的上传下载功能。图片转换有免费额度:每项目每月 100 张,超过后每 1000 张收费 $5。

10 分钟阅读 · 发布于: 2026年4月9日 · 修改于: 2026年4月9日

评论

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

相关文章