news 2026/9/7 5:58:41

Supabase Storage 签名可续传上传(Signed Resumable Upload):基于 Uppy + TUS 与 createSignedUploadUrl 的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Supabase Storage 签名可续传上传(Signed Resumable Upload):基于 Uppy + TUS 与 createSignedUploadUrl 的完整实战

Supabase Storage 签名可续传上传(Signed Resumable Upload):基于 Uppy + TUS 与 createSignedUploadUrl 的完整实战

【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase

导读

对于大文件上传,网络中断、弱网环境与并发限制都容易导致整包重传。Supabase Storage 内置了基于 TUS 协议的可续传上传能力,本仓库examples/storage/resumable-upload-signed-uppy提供了一份完整的参考实现:它在浏览器端使用 Uppy 作为上传 UI 与 TUS 客户端,通过边缘函数为每一个文件调用createSignedUploadUrl()申请签名令牌,并经由x-signature请求头随 TUS 分片请求提交,从而在不暴露服务端密钥的前提下完成安全、可续传的大文件上传。读完本文,你将掌握这套“按文件签名 + Uppy/TUS 续传”方案的前后端完整接线方式、本地运行步骤,以及其中每一个关键配置与调用在仓库源码中的真实依据。

原文入口:README.md,本文所有路径均以仓库根目录为基准。

一、签名可续传上传要解决什么问题

Supabase Storage 实现并对外暴露了 TUS 协议(The Upload Server)用于可续传上传,相关的能力说明与建议可以参阅文档 resumable-uploads.mdx。它推荐在大文件(可能超过 6MB)、弱网环境、需要上传进度事件的场景下使用可续传上传。

在本仓库的examples/storage目录下并存着两个高度相似但鉴权方式截然不同的示例:

对比维度resumable-upload-signed-uppy(本文主角)resumable-upload-uppy
TUS 端点…/storage/v1/upload/resumable/sign…/storage/v1/upload/resumable
携带凭据每个文件独立签发的x-signature令牌固定的authorization: Bearer <token>
凭据来源服务端createSignedUploadUrl()按需签发客户端静态配置(如用户会话令牌)
安全性令牌按文件路径签发、可限时,无需暴露长期密钥需要把令牌直接配置在页面里

可以看出,签名方案的核心价值在于:不再需要把服务端或用户的长期凭据写死在页面 / 暴露给最终上传者,而是由可信的服务端代码(本示例中的 Supabase Edge Function)在文件入队时动态签发“仅能写该文件的受限令牌”,浏览器端只持有这张短时令牌即可通过 TUS 完成大文件上传。与此对应的实现背景可参考兄弟示例 resumable-upload-uppy/README.md。

二、示例文件结构与职责划分

整个示例只有一张单页应用和一个小小的边缘函数,却能完整覆盖“前端界面 + TUS 续传 + 服务端签名 + 存储授权”整条链路:

examples/storage/resumable-upload-signed-uppy/ ├── index.html # 前端单页:Uppy Dashboard + TUS 上传 └── supabase/ ├── config.toml # 本地项目配置:bucket、函数开关 ├── functions/ │ └── create-upload-token/ │ ├── deno.json # 函数 import map │ └── index.ts # 边缘函数:签发 x-signature 令牌 └── migrations/ └── 20241128121139_storage_rls.sql # 允许匿名用户写入 uploads bucket 的 RLS 策略

各文件的详细职责如下:

  • index.html:引入 Uppy v3.6.1 的 Dashboard 与 Tus 插件;在file-added事件里为每个文件申请令牌并写入该文件的 TUS header。
  • create-upload-token/index.ts:接收{ filename },用服务端管理员上下文调用createSignedUploadUrl(),把token返回给前端。
  • config.toml:本地开发配置,声明了 bucketuploads、函数create-upload-token(含verify_jwt = false)。
  • 20241128121139_storage_rls.sql:一条 RLS 策略,放行匿名用户对uploadsbucket 的 INSERT。

三、本地运行步骤(附完整命令)

依据 README 的说明,本地完整跑通这套签名上传只需三步:

第 1 步:启动本地 Supabase 项目

supabase start

CLI 会按项目内 config.toml 启动本地 API、Storage、Functions 等组件,并在启动结束后在终端输出本地服务的各类密钥。

第 2 步:配置发布密钥并指向本地服务

打开 index.html,在文件顶部的常量区把SUPABASE_PUBLISHABLE_KEY替换为supabase start输出的发布密钥(anon key):

const SUPABASE_PUBLISHABLE_KEY = '' // your project's publishable (anon) key const PROJECT_URL = 'http://127.0.0.1:54321' const STORAGE_BUCKET = 'uploads'
  • PROJECT_URL固定为本地 CLI 默认端口54321(config.toml 中并未修改默认端口)。
  • STORAGE_BUCKET对应配置里声明的 bucket 名uploads,见下方 config 说明。

第 3 步:用静态服务器打开页面开始上传

README 给出了两种等价方式:

# python http server python3 -m http.server # npm http-server npx http-server

由于 index.html 使用<script type="module">从 CDN import Uppy 的 ES Module,直接用浏览器打开本地文件可能受 CORS / module 加载限制,因此需要一个本地静态服务器来承载页面。

四、核心原理一:前端如何把一个文件变成“可签名续传上传”

4.1 组装 TUS 客户端与上传端点

前端的关键在于 Uppy 的 Tus 插件配置,见 index.html:

import { Uppy, Dashboard, Tus } from 'https://releases.transloadit.com/uppy/v3.6.1/uppy.min.mjs' var uppy = new Uppy() .use(Dashboard, { inline: true, limit: 10, target: '#drag-drop-area', showProgressDetails: true, }) .use(Tus, { endpoint: `${PROJECT_URL}/storage/v1/upload/resumable/sign`, headers: { apikey: SUPABASE_PUBLISHABLE_KEY, }, uploadDataDuringCreation: true, chunkSize: 6 * 1024 * 1024, allowedMetaFields: ['bucketName', 'objectName', 'contentType', 'cacheControl'], onError: function (error) { console.log('Failed because: ' + error) }, })

逐个解读这些参数在签名场景下的意义:

  • endpoint:指向/storage/v1/upload/resumable/sign。注意与普通(Bearer 令牌)示例 resumable-upload-uppy/index.html 中使用的/storage/v1/upload/resumable相比,这里多了/sign后缀,这正是签名可续传上传的专用入口。
  • headers.apikey:固定带上本地 anon key,作为基础请求身份。
  • uploadDataDuringCreation: true:在创建 TUS 上传时即携带元数据,让服务端在创建阶段就能拿到 bucket / 对象路径做签名校验。
  • chunkSize: 6 * 1024 * 1024:TUS 分片大小取 6MB,与文档 resumable-uploads.mdx 中的示例保持一致;断点续传正是以分片为单位记录进度,中断后可只重传未完成分片。
  • allowedMetaFields:白名单之外的元数据不会随请求发出。签名上传必须的bucketName / objectName / contentType都在其中,cacheControl则用于按文件设置缓存策略。
  • Dashboard 的limit: 10限制并发上传数量;showProgressDetails: true在界面上展示分片级进度详情。

4.2 file-added 钩子:每个文件独立申请签名并注入 x-signature

签名方案与“一次性配置一个 Bearer token”最大的不同,在于令牌需要逐文件申请、逐文件注入。这体现在 index.html 的file-added事件处理中:

async function getSignedUploadToken(filename) { const res = await fetch(`${PROJECT_URL}/functions/v1/create-upload-token`, { method: 'POST', headers: { 'Content-Type': 'application/json', apikey: SUPABASE_PUBLISHABLE_KEY }, body: JSON.stringify({ filename }), }) if (!res.ok) throw new Error('Failed to get upload token') const data = await res.json() return data.token } uppy.on('file-added', async (file) => { const supabaseMetadata = { bucketName: STORAGE_BUCKET, objectName: file.name, contentType: file.type, } file.meta = { ...file.meta, ...supabaseMetadata } // Important - add signing token header const token = await getSignedUploadToken(file.name) uppy.setFileState(file.id, { tus: { headers: { 'x-signature': token, }, }, }) }) uppy.on('complete', (result) => { console.log('Upload complete! We've uploaded these files:', result.successful) })

这里有四个关键动作:

  1. 填充上传元数据:把bucketNameobjectNamecontentType合并进file.meta,供 TUS 创建上传时按allowedMetaFields提交。
  2. 按文件回调签名函数getSignedUploadToken(file.name)以文件名为入参 POST 到本地 Functions 的/functions/v1/create-upload-token
  3. 把令牌写进该文件的 TUS header:通过uppy.setFileState(file.id, { tus: { headers: { 'x-signature': token } } })实现。这是本示例最核心的一行代码——它保证了签名令牌只会被用于与其绑定的那一个文件的上传请求,而不是全局共享一个令牌。
  4. 完成回调uppy.on('complete')在全部上传结束后打印result.successful,可在此接入服务端回调、二次校验等后续逻辑。

代码注释中的// Important - add signing token header也提示了:缺少这一步,服务端将无法校验签名,上传会被拒绝。

五、核心原理二:边缘函数如何签发令牌

5.1 函数入口与鉴权模式

签名必须由“持有服务端权限”的代码完成,本示例把它放在 Edge Function 中,见 create-upload-token/index.ts:

import 'jsr:@supabase/functions-js/edge-runtime.d.ts' import { withSupabase } from 'npm:@supabase/server@^1' // Deploy with verify_jwt = false. export default { fetch: withSupabase({ auth: 'secret' }, async (req, ctx) => { try { const { filename } = await req.json() if (!filename) { return Response.json({ error: 'Missing filename' }, { status: 400 }) } const { data, error } = await ctx.supabaseAdmin.storage .from('uploads') .createSignedUploadUrl(filename) if (error) { return Response.json({ error: error.message }, { status: 500 }) } return Response.json({ token: data.token }) } catch (error) { return Response.json({ error: (error as Error).message }, { status: 500 }) } }), }

要点拆解:

  • withSupabase({ auth: 'secret' }, ...):声明函数内部使用服务端密钥上下文auth: 'secret'),因此ctx.supabaseAdmin拥有绕过 RLS 的管理员能力——它需要替“匿名”的浏览器上传者签发令牌。withSupabasesupabaseAdmin均来自npm:@supabase/server@^1
  • 参数校验req.json()解析出的filename缺失时直接返回400 Missing filename,避免对空路径签发令牌。
  • 核心调用ctx.supabaseAdmin.storage.from('uploads').createSignedUploadUrl(filename)在 Storage SDK 中以“bucket + 对象路径”为输入签发受限上传令牌。仓库中对该 SDK 方法的接口定义可参见规范文件 supabase_js_v2.yml。
  • 令牌返回:只把data.token回传给前端,函数内部持有的密钥永远不会离开服务端。
  • 签名与路径强绑定:因为令牌是对uploads/{filename}这个对象路径签发的,前端在 TUS 元数据里提交的objectName必须与该路径一致(本示例中objectName: file.name),签名校验才能通过——这也是为什么令牌要逐文件申请。

5.2 与官方文档的行为对照

在官方文档 resumable-uploads.mdx 的“Presigned uploads”一节中,对同一机制有明确描述:可续传上传支持通过调用 SDK 的createSignedUploadUrl方法生成限时的共享 URL,并把返回的 token 放进x-signature头参与可续传上传。本文示例 index.html 正是该机制的完整落地形态:服务端负责签发、浏览器负责把令牌作为x-signature请求头随 TUS 请求发出。

六、配套基建:config.toml、迁移与 bucket 授权

6.1 本地项目配置 config.toml

config.toml 做了四项与本示例强相关的声明:

project_id = "resumable-upload-uppy" [api] # Disable data API since we are not using the PostgREST client in this example. enabled = false [storage] file_size_limit = "50MiB" [storage.image_transformation] enabled = false [storage.buckets.uploads] public = true # file_size_limit = "50MiB" # allowed_mime_types = ["image/png", "image/jpeg"] # objects_path = "./buckets/uploads" [functions.create-upload-token] enabled = true verify_jwt = false import_map = "./functions/create-upload-token/deno.json" entrypoint = "./functions/create-upload-token/index.ts"

逐项说明:

  • project_id = "resumable-upload-uppy":本地项目标识,用于在同一机器上区分不同 Supabase 项目。
  • [api] enabled = false:本示例只走 Storage 与 Functions,不依赖 PostgREST 数据 API,因此显式关闭,可减少本地启动的组件。
  • [storage] file_size_limit = "50MiB":整个项目所有 bucket 的默认单文件上限为 50MiB。如果想上传更大的文件,需要同步调大此处,同时把前端 TUS 的chunkSize保持在低于该上限的合理值。
  • [storage.buckets.uploads] public = true:声明一个名为uploads的公开 bucket(public只影响对象是否可公开读取,不影响写入授权——写入仍由 RLS 策略决定)。被注释掉的file_size_limitallowed_mime_typesobjects_path分别用于对单 bucket 覆盖大小限制、限定 MIME 类型(如["image/png", "image/jpeg"])以及映射本地目录预置对象。
  • [functions.create-upload-token]
    • verify_jwt = false:允许不带用户 JWT 调用该函数。这与源码注释// Deploy with verify_jwt = false相互印证——因为本示例走“匿名 + 按文件签名”而非“登录用户”模式,函数依赖自身的auth: 'secret'服务端上下文来代签。
    • import_map/entrypoint:分别指向函数目录内的 deno.json 与index.ts。deno.json 目前为空导入映射({"imports": {}}),函数所需依赖(如@supabase/server)直接以npm:/jsr:前缀在源码中声明。
    • 被注释掉的static_files展示了如需随函数托管静态资源时可使用的 glob 配置方式。

6.2 RLS 迁移:放行匿名写入

20241128121139_storage_rls.sql 只有一条策略,但它是整条链路“最后一公里”的授权保障:

CREATE POLICY "allow uploads" ON storage.objects FOR INSERT TO public WITH CHECK (bucket_id = 'uploads');
  • FOR INSERT TO public:允许public(含匿名)角色向storage.objects插入记录;
  • WITH CHECK (bucket_id = 'uploads'):插入对象的 bucket 必须被限定为uploads,把写入范围收窄到单一 bucket。

配合uploadsbucket 声明为public = true(读公开)与上面这条 INSERT 策略(匿名可写),浏览器端才能仅凭“anon key + x-signature 令牌”把分片写入该 bucket。如果去掉该策略,即使拿到了签名令牌,匿名上传仍会被 RLS 拦截。

七、端到端请求链与安全边界

综合前文,一次成功的签名可续传上传其完整调用链如下:

  1. 用户在 Uppy Dashboard 中添加文件,触发file-added事件;
  2. 前端为该文件填充bucketName / objectName / contentType元数据;
  3. 前端 POST{ filename }到边缘函数create-upload-token
  4. 函数在auth: 'secret'的服务端上下文中调用createSignedUploadUrl(filename),得到受限令牌;
  5. 前端通过uppy.setFileState把令牌写入该文件 TUS 请求的x-signature头;
  6. Uppy Tus 插件把文件按 6MB 分片 POST 到/storage/v1/upload/resumable/sign,服务端校验签名与元数据后进行 TUS 续传写入;
  7. 中断后 Tus 插件自动按 TUS 协议续传剩余分片;全部完成后触发uppy.on('complete')

这套设计的几条安全边界值得在生产中沿用:

  • 最小权限:浏览器端只持有“单 bucket、单对象路径”的受限签名令牌,而不是长期有效的管理员密钥或用户访问令牌;
  • 逐文件隔离:令牌在file-added内按文件名申请并通过setFileState注入,某一文件令牌泄露不会波及其他文件路径(签名与该文件对象路径强绑定);
  • 函数收敛:唯一能代签的入口是部署时以verify_jwt = false暴露的边缘函数,需确保该函数自身逻辑最小化——本示例仅做参数校验与转发,不做任何文件内容处理;
  • 上传终点收紧:即使 RLS 放行了匿名 INSERT,也通过bucket_id = 'uploads'WITH CHECK把可写入的 bucket 锁定。

从源码结构可以推断,若要在生产环境使用这套模板,通常还应在签名函数中加入用户态校验(如改用auth: 'user'鉴权后校验登录用户是否有权上传)、对文件名做规范化处理,以及把PROJECT_URL由本地http://127.0.0.1:54321替换为线上项目域名(大文件场景下建议使用直连存储域名)。

八、与普通可续传上传示例的取舍

仓库中还提供了不签名、直接携带Bearer令牌的版本 resumable-upload-uppy,两者仅在“如何表达上传者身份”上分叉:

  • 携带用户会话令牌(Bearer):适合“登录用户才能上传”的业务,令牌本身即代表用户身份,上传与用户账号天然关联,实现最简单;
  • 按文件签发 x-signature:适合“把上传能力开放给匿名或第三方、但不想扩散长期密钥”的业务,例如分享式网盘、问卷附件、票据系统等;缺点是每次文件入队都需要一次额外的函数调用。

两个示例共用同一套前端骨架(Uppy v3.6.1、Dashboard、Tus 插件、6MB 分片),差异仅在 TUS 端点(/resumable还是/resumable/sign)与令牌注入方式上,非常适合对照阅读:先读懂 resumable-upload-uppy/index.html 的基线行为,再看本文示例中多出的“申请令牌 → 注入 x-signature”两步,即可快速迁移出自己的签名上传方案。

总结

examples/storage/resumable-upload-signed-uppy用“一个 Uppy 单页 + 一个 30 行边缘函数 + 一条 RLS 策略”演示了 Supabase Storage 签名可续传上传的最小可用闭环。其设计要点可以概括为一句话:上传能力与密钥彻底分离——服务端用createSignedUploadUrl()按对象路径签发受限令牌,浏览器仅凭x-signature请求头驱动 TUS 分片续传。无论是做移动端 / Web 端的大文件直传,还是构建对匿名用户开放的分享式上传,这套模板连同 config.toml、迁移脚本 与 官方文档 都是可以直接对照落地的参考基线。

【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 5:58:00

OpenCV人脸识别实战:从方案选型到参数调优全解析

简介&#xff1a;这是一份面向 OpenCV 入门开发者的 Java 人脸识别示例工程&#xff0c;适合想学习 Haar 级联检测、人脸特征提取与识别流程&#xff0c;或需要在 Eclipse 环境中快速搭建 OpenCV 项目的读者。资源包为一个 485KB 的 zip 压缩包&#xff0c;共含 25 个文件&…

作者头像 李华
网站建设 2026/9/7 5:55:44

OFDM系统PAPR抑制实战:PTS算法三种分割方式的MATLAB实现

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 5:54:35

YOLOv8+PyQt5路面缺陷检测系统设计:从模型训练到部署全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华