)
使用 Supabase、Svelte 与 Vite 构建带行级安全的多用户 Todo 应用完整实战指南【免费下载链接】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本指南以仓库中的 examples/todo-list/sveltejs-todo-list 官方示例为核心讲解如何用 Svelte TypeScript Vite 作为前端配合 Supabase 托管的 Postgres 数据库与supabase/supabase-js客户端从零搭建一个支持邮箱/密码注册登录、GitHub/Google 第三方登录、以及数据实时增删改查的 Todo 应用。读完本文你将掌握Supabase 项目的初始化流程、Todo 快速启动 SQL 的执行方法、anon与secret两类密钥的区别以及 Postgres Row Level SecurityRLS如何做到每个用户只能看到并操作自己的待办事项。技术栈与目录结构总览该示例遵循官方推荐的三层架构浏览器端 Svelte 组件只负责渲染与交互所有数据读写都通过 Supabase.js 走到托管 Postgres 后端的 RESTful APIPostgREST而数据库端则由 Postgres 的 RLS 策略充当最终的安全守门员。前端Svelte、TypeScript、Vite配合 Tailwind CSS 负责样式后端在 Supabase 云平台创建的托管 Postgres 数据库自动暴露 RESTful API 供 Supabase.js 使用关键依赖supabase/supabase-js示例使用^2大版本完整依赖见 package.json。示例的源码目录结构如下组件划分清晰、非常适合作为学习范本src/lib/db.ts创建全局唯一 Supabase 客户端src/lib/schema.ts手写的数据库类型声明Database接口src/lib/Auth.svelte登录/注册 UI 与逻辑src/lib/Home.svelte登出后的主界面壳层src/lib/TodoList.svelteTodo 列表的查询、新增、删除src/lib/Todo.svelte单个 Todo 项的完成状态切换src/App.svelte会话状态管理与会话监听。第一步在 Supabase 创建新项目在开始写任何代码之前需要先在云端准备数据库。前往 Supabase Dashboard 注册账号并创建一个新项目随后等待数据库完成初始化通常会持续一两分钟。创建成功后你会拿到一组与项目绑定的配置信息其中最关键的是Project URLAPI 地址与API Key它们将在第三步配置到前端环境中。数据库本身无需手工建表下一步的快速启动 SQL 会一次性完成建表与授权策略的创建。第二步运行 Todo List 快速启动 SQL数据库启动后进入 Dashboard 项目的SQL Editor标签页向下滚动找到名为TODO LIST: Build a basic todo list with Row Level Security的模板直接点击运行即可。它会替你在数据库中完成建表、开启行级安全、创建四条访问策略的全部工作。如果希望跳过 Dashboard 手动执行也可以走更贴近工程化的迁移方式仓库中配套的 examples/todo-list/nextjs-todo-list/supabase/migrations/20230712094349_init.sql 保存了与本示例完全一致的初始化 SQL可用 Supabase CLI 的supabase db push等迁移命令导入到你的项目效果等价。在阅读下一节 SQL 之前先记住一个核心心智模型todo 表上所有的数据行属于哪个用户由user_id字段承载而谁能访问哪些行由 RLS 策略在数据库端强制执行。前端代码无论怎么写都绕不过这一层数据库权限校验。第三步获取 Project URL 与 anon 密钥进入项目设置齿轮图标打开API标签页找到如下两样东西并记录下来下一步将用到Project URL形如https://project-ref.supabase.co的 API 端点anon公钥用于客户端侧的 API 密钥。理解这两类密钥的作用是配置安全的关键anon密钥面向客户端的公开密钥允许对数据库做匿名访问直到用户真正登录。用户登录成功后客户端发出的请求会自动携带用户自己的 JWT登录令牌此时数据库端会依据 JWT 中的角色与auth.uid()切换数据可见范围从而为数据启用行级安全。本示例刻意在 src/lib/db.ts 与 .env.example 中使用VITE_SUPABASE_PUBLISHABLE_KEY命名该密钥——在较新版本的 Supabase 中anon密钥已被逐步更名为publishable key可发布密钥二者指向同一把可安全暴露在浏览器端的密钥读者在控制台看到的名称以实际为准secret密钥拥有数据的完整访问权限会绕过所有安全策略因此必须严格保密只能在服务端环境中使用绝不能放进客户端或浏览器代码凡是VITE_前缀的变量都会被打包进前端产物一旦放置 secret 密钥等于公开泄漏。配置前端环境变量拿到上述两样配置后将本示例根目录下的 .env.example 复制为.env并填入真实值# 在 examples/todo-list/sveltejs-todo-list 目录下 cp .env.example .env对应内容如下# 从 Supabase 项目设置 API 中获取 VITE_SUPABASE_URLhttps://your-project.supabase.co VITE_SUPABASE_PUBLISHABLE_KEYyour-publishable-keyVITE_前缀是 Vite 暴露环境变量给客户端代码的约定只有以此前缀开头的变量才会被import.meta.env读取。随后启动开发服务器npm install npm run dev从 package.json 可以看到dev脚本用concurrently同时运行 Tailwind CSS 监听编译与 Vite 开发服务器二者缺一不可npm run build则会先压缩编译 CSS 再执行vite build产出静态文件。仓库还提供了svelte-check脚本用于对 Svelte 组件做类型检查。客户端初始化与类型安全前端唯一需要初始化的全局单例就是 Supabase 客户端src/lib/db.ts 的实现如下import { createClient } from supabase/supabase-js import type { Database } from ./schema export const supabase createClientDatabase( import.meta.env.VITE_SUPABASE_URL, import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY )这里值得注意两个工程细节createClientDatabase是泛型调用Database类型来自 src/lib/schema.ts。该文件把todos表的Row查询返回形态、Insert插入形态、Update更新形态逐字段声明清楚——例如id在Row中必填、在Insert中可选由数据库自增生成inserted_at同样允许缺省。有了这套类型所有.from(todos).insert(...)调用都会获得编译期校验与自动补全这是本示例强调 TypeScript 的根本原因直接用import.meta.env.VITE_SUPABASE_URL读取环境变量若.env未正确配置客户端会在运行时收到空值错误因此务必先完成第三步的配置。用户认证流程从会话监听谈起应用根组件 src/App.svelte 通过一个user变量在登录页与Todo 主界面之间切换。会话恢复与实时同步的代码如下onMount(() { supabase.auth.getSession().then(({ data: { session } }) { user session?.user ?? null; }); const { data: { subscription: authListener } } supabase.auth.onAuthStateChange( (_, session) { const currentUser session?.user; user currentUser ?? null; } ); return () { authListener?.unsubscribe(); }; });这段逻辑承担三个职责会话恢复页面刷新后通过getSession()立即拉取本地缓存的会话避免已登录用户被强制跳回登录页登录状态订阅onAuthStateChange监听登录、登出、令牌刷新等全部认证事件任何状态变化都会实时驱动 UI 切换资源清理组件卸载时调用authListener.unsubscribe()解除订阅这是 SvelteonMount返回清理函数的典型用法可避免内存泄漏。App.svelte根据user是否存在条件渲染Home已登录或Auth未登录。登录成功的用户对象随后被逐层下传给业务组件作为写入数据时user_id的来源。邮箱密码登录与第三方 OAuthsrc/lib/Auth.svelte 封装了两种登录形态。其一是邮箱/密码方式用一个参数区分登录与注册const handleLogin async (type) { const { data: { user }, error, } type LOGIN ? await supabase.auth.signInWithPassword({ email, password }) : await supabase.auth.signUp({ email, password }); if (error) { helperText { error: true, text: error.message }; } else if (!user !error) { helperText { error: false, text: An email has been sent to you for verification!, }; } };这段代码隐含了一个 Supabase Auth 的默认行为当项目开启了邮件确认时signUp成功后不会直接返回已登录用户而是先发送验证邮件——因此代码用!user !error分支提示用户查收邮件完成验证。其二是 GitHub / Google 等第三方 OAuth 登录const handleOAuthLogin async (provider: Provider) { let { error } await supabase.auth.signInWithOAuth({ provider }); if (error) console.log(Error: , error.message); };按钮分别以handleOAuthLogin(github)、handleOAuthLogin(google)调用。需要提醒的是使用第三方登录前必须先到 Dashboard 的Authentication Settings或对应 Provider 配置页中启用相应的第三方认证并填入应用的 OAuth 回调地址否则会登录失败——这也解释了为什么示例代码的注释强调你需要在 Authentication Settings 中启用你想使用的第三方认证。登出逻辑放在 src/lib/Home.svelte调用supabase.auth.signOut()即可成功后onAuthStateChange会将user置空、UI 自动切回登录页。Todo 数据层查询、新增、完成、删除查询列表src/lib/TodoList.svelte 在组件挂载时拉取当前用户可见的全部待办const fetchTodos async () { let { data, error } await supabase .from(todos) .select(*) .order(id, { ascending: true }); if (error) { console.log(error, error); } else { todos data; } };注意这里没有写任何WHERE user_id ...条件——能这样做的原因是 RLS 的select策略已经在数据库端把结果过滤为仅当前登录用户自己的行客户端无需也无法可靠地自行过滤。这也是理解本示例安全模型最重要的一点。新增 Todoconst addTodo async (taskText: string) { let task taskText.trim(); if (task.length) { let { data: todo, error } await supabase .from(todos) .insert({ task, user_id: user.id }) .select() .single(); if (error) { errorText error.message; } else { todos [...todos, todo]; newTaskText ; } } };插入时必须显式携带user_id: user.id也就是把当前登录用户的 UUID 写入行数据与 RLS 的insert with check (auth.uid() user_id)形成呼应。链式.select().single()让插入后直接返回新行并更新本地数组UI 无需二次刷新若 RLS 校验不通过例如恶意传入他人的user_iderror会携带数据库返回的权限错误信息并展示在页面顶部的 Alert 中。切换完成状态src/lib/Todo.svelte 处理单行完成状态切换const toggle async () { try { const { data, error } await supabase .from(todos) .update({ is_complete: !isCompleted }) .eq(id, todo.id) .select(is_complete) .single(); if (error) throw error; isCompleted data.is_complete; } catch (error) { console.log(error, error); } };.eq(id, todo.id)精确锁定目标行.update(...)只更新is_complete字段。由于 RLS 的update策略限定只能更新自己的行即便客户端拿到的是别人数据的id更新请求也会在数据库端被拒绝。删除 Todoconst deleteTodo async (id: number) { try { await supabase.from(todos).delete().eq(id, id); todos todos.filter((x) x.id ! id); } catch (error) { console.log(error, error); } };删除同样只依赖行id可见性控制完全交给 RLS 的delete策略。以上增删改查四条链路共同说明一个结论所有客户端过滤逻辑都只是用户体验优化真正的数据边界始终由数据库端 RLS 兜底。深入 Supabase 细节Postgres 行级安全RLSRLS 的工作机制本示例采用 Postgres 自带的高级授权能力 —— Row Level Security。其工作原理可概括为在 Supabase 创建 Postgres 数据库时平台会自动预置一个authschema 以及若干辅助函数如auth.uid()用户登录后GoTrueSupabase 的认证服务为其签发携带角色authenticated与其 UUID 的 JWTPostgREST 收到带 JWT 的请求后会注入对应的数据库角色与auth.uid()上下文表上的每一条策略都基于这些上下文做逐行判断从而实现精细到行的访问控制。建表与策略 SQL 逐行解读下方是示例中精简后的完整 schema与 examples/todo-list/nextjs-todo-list/supabase/migrations/20230712094349_init.sql 内容一致差异仅在于示例用了(select auth.uid())显式子查询写法语义相同create table todos ( id bigint generated by default as identity primary key, user_id uuid references auth.users not null, task text check (char_length(task) 3), is_complete boolean default false, inserted_at timestamp with time zone default timezone(utc::text, now()) not null ); alter table todos enable row level security; create policy Individuals can create todos. on todos for insert with check ((select auth.uid()) user_id); create policy Individuals can view their own todos. on todos for select using ((select auth.uid()) user_id); create policy Individuals can update their own todos. on todos for update using ((select auth.uid()) user_id); create policy Individuals can delete their own todos. on todos for delete using ((select auth.uid()) user_id);逐段拆解建表id使用bigint generated by default as identity自增主键现代 Postgres 推荐的 identity 列而非旧式 serialuser_id为uuid类型且通过外键references auth.users关联认证用户表并声明not null从根源上杜绝无主数据task通过check (char_length(task) 3)约束任务文本必须超过 3 个字符——这也是前端addTodo里做task.trim()与长度判断的数据库端镜像约束is_complete默认falseinserted_at默认取 UTC 当前时间timezone(utc::text, now())开启 RLSalter table todos enable row level security是承上启下的一行。注意如果不显式开启 RLS 且未赋予表级权限表默认对所有人开放而一旦开启 RLS 却没有创建任何策略则默认拒绝一切访问。Supabase 的建议是始终开启 RLS再由策略显式放行四条策略分别覆盖 insert/select/update/delete且全部围绕auth.uid()当前登录用户 UUID与行的user_id做等值判断insert 使用with check校验的是即将写入的新行是否满足user_id auth.uid()防止用户伪造他人user_id插入不属于自己的记录select / update / delete 使用using针对的是已存在的行判断该行是否属于当前用户。用using还是with check或二者兼用是编写 RLS 时最需要区分的概念update策略只写了using而未写with check意味着用户只能定位到自己拥有的行但配合前端update({ is_complete: ... })只改非关联字段的用法已足够若需要允许更新user_id等敏感字段则应补上with check。正是这条 SQL 让上一节中的所有前端调用天然安全任何用户查询到的行都必然是自己创建的插入、删除、更新都只在自己的数据范围内生效。本地运行完整流程回顾复制环境变量cp .env.example .env填入从 Dashboard API 设置中获取的 Project URL 与 anon/publishable key安装依赖npm install仓库为 pnpm workspace 结构单跑本示例亦可直接npm install启动npm run dev浏览器打开 Vite 输出的本地地址在 SQL Editor 执行 Todo List 快速启动 SQL或由 CLI 迁移导入 init.sql注册一个账号完成登录即可体验添加任务 → 勾选完成 → 删除任务 → 登出的完整闭环再注册第二个账号对比会发现两个账号的数据彼此完全隔离这正是 RLS 的实际效果验证。如需进一步掌握认证与实时同步机制可在仓库中继续研读相关文档与应用实现本示例侧重展示最小可运行的 RLS 多用户应用也正因为其精简非常适合作为理解 Supabase 鉴权与 Postgres 安全模型的第一份源码范例。【免费下载链接】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),仅供参考