一个反例开场:某团队把"当前用户 id"存在前端 localStorage 里,每次请求前端自己带 ?userId=xxx 去过滤数据。结果攻击者改一下这个值,就把别人的数据全拉走了——因为后端没校验"这个 userId 是不是你"。Supabase 的解法是把身份放进 JWT、把授权放进 RLS,前端没法伪造。这一节讲这套"认证 + 授权"怎么咬合。
认证解决身份问题。Supabase Auth 支持邮箱密码、魔法链接、手机 OTP,以及 GitHub、Google 等第三方登录。登录成功后,客户端拿到一个 JWT,里面 sub 字段就是用户 uuid。
// 邮箱密码登录 const { data, error } = await supabase.auth.signInWithPassword({ email: 'a@b.com', password: 'secret', }) if (error) console.error('登录失败:', error.message) else console.log('用户 id:', data.session!.user.id) // 第三方登录(如 GitHub),会跳转到授权页 await supabase.auth.signInWithOAuth({ provider: 'github' })
登录态由 supabase-js 自动持久化(默认存 localStorage),后续请求自动带 JWT,你不用手动管理 token。
授权靠 RLS,不是靠应用判断。前面几节反复出现 auth.uid(),它就是 JWT 里的 sub 在数据库里的入口。一条策略的本质是:"当 JWT 里的用户满足某条件,才允许某操作"。
下面 SVG 画了 JWT 从登录到 RLS 判定的时序,看清"前端无法伪造身份"的原因:

除了"登录用户"和"匿名",你还可以在 JWT 里放自定义 role 或 app_role 声明,RLS 据此分档。比如区分普通用户和管理员:
-- 假设登录时把 app_role 写进 JWT(通过触发器或元数据) -- 管理员可见全部文章,普通用户只看已发布 create policy "管理全看" on public.articles for select using ( (auth.jwt() ->> 'app_role') = 'admin' ); create policy "公开已发布" on public.articles for select using ( published = true );
auth.jwt() 能取到整个 JWT 的声明,->> 取文本字段。注意:自定义声明要在登录流程里真正写入 JWT,否则 RLS 读不到。
JWT 有过期时间(默认一小时),但 supabase-js 会自动用 refresh token 换新的 access token,用户无感。你也可以监听状态变化做 UI 切换:
supabase.auth.onAuthStateChange((event, session) => { if (event === 'SIGNED_OUT') { console.log('用户已退出,清本地态') } if (event === 'TOKEN_REFRESHED') { console.log('token 已静默刷新') } })
背景:做一个用 GitHub 登录的工具站,登录后要自动建 profiles 行记录用户名。
操作过程:
create or replace function public.handle_new_user() returns trigger language plpgsql as $$ begin insert into public.profiles(id, username) values (new.id, coalesce(new.raw_user_meta->>'user_name', new.email)); return new; end; $$; create trigger on_auth_user_created after insert on auth.users for each row execute function public.handle_new_user();
signInWithOAuth({ provider: 'github' }),登录回调后 profiles 已自动存在。结果:用户点一下 GitHub 就完成注册,无需填表,资料行自动生成,后续 RLS 用 auth.uid() = id 直接生效。
解读:把"建资料"放进数据库触发器,而不是前端登录后补调一次接口,避免了"用户关页面导致资料缺失"的竞态。这是 Supabase 里很典型的"逻辑下沉到数据层"手法。
变式:若要将 GitHub 的 email 作为登录标识并允许改用户名,触发只负责首次插入,改用户名走前端的 update profiles,配合 1.1 里的"自己改自己"策略。
auth.uid() / auth.jwt() 判定,前端无法伪造。⚠️ 不要在 RLS 里信任客户端传来的任何字段当身份。身份只能来自 JWT 签名解析出的 auth.uid(),客户端传的 userId 参数一律忽略,否则回到本节开头的反例。
💡 我们建议把"用户注册后初始化资料"这类逻辑用 auth.users 的触发器实现,而不是前端补接口。数据层保证原子性,比应用层补操作稳得多。
下一节进入实时数据:怎么让客户端在数据变更的瞬间收到推送,而不是轮询。