本文件为通用复用模板,适用于任何需要「邮箱+密码注册 / 登录 / 找回密码 / 登出 + 角色权限(RBAC)」的项目。多个项目直接复制使用,按下方「占位符清单」替换为各自配置即可。
文档目的:把「注册(邮箱+密码+验证码)/ 登录(邮箱+密码)/ 找回密码(邮箱+新密码+验证码)/ 退出登录」四个流程,按 Supabase Auth 从头到尾讲清楚,含建项目、后台配置、前端调用、数据模型与后续扩展。
说明:文中
supabase.auth.*代码片段为 示意(JavaScript / @supabase/supabase-js),用于说明每一步调什么方法;实际接入时按你的前端框架(小程序 / Web / React Native)调用同名方法即可。范围:核心做邮箱+密码。手机号、微信(含小程序)留作第九章的扩展位,数据模型已为此预留;权限 / RBAC 见第十一章。
| 占位符 | 含义 | 出现在 |
|---|---|---|
| https://xxxx.supabase.co | 你的 Project URL | 第 1 章、所有前端初始化 |
| your-anon-key | 项目 anon / public key(前端用) | 前端初始化 |
| SERVICE_ROLE_KEY | service_role key(仅服务端) | 第 11 章分配管理员 |
| https://your-domain.com | 你的业务域名 | 第 2、4、6 章回调 / 重定向 |
| your-project | 示例项目名 | 第 1 章 |
| 微信 AppID / AppSecret | 微信开放平台凭证 | 第 9 章(仅接微信时需要) |
| SMTP 发信配置 | 自定义发信域名 | 第 2.4 节(生产必做) |
用 Supabase(路径 A)后,认证的核心脏活全部由 Supabase 托管,你几乎不写后端:
| 能力 | 谁负责 |
|---|---|
| 密码哈希(Argon2/bcrypt) | Supabase |
| 双 Token 签发 / 刷新(Access + Refresh) | Supabase |
| 刷新令牌轮换(防重放) | Supabase(默认开启) |
| 退出登录 / 全端吊销 | Supabase |
| 发送验证码邮件、限流、防枚举 | Supabase |
| 数据行级安全(RLS) | Supabase(你配策略) |
| 邮箱+密码注册/登录/找回/登出 | supabase.auth.* 原生方法 |
| 微信小程序登录 | 你写一个 Edge Function(第九章) |
结论:你要做的 = 前端调方法 + 后台开配置 + 配邮件模板;唯一要写后端代码的是将来微信小程序那块。
your-project,按实际项目命名)。https://xxxx.supabase.conpm install @supabase/supabase-js
import { createClient } from '@supabase/supabase-js'
export const supabase = createClient(
'https://xxxx.supabase.co', // Project URL
'your-anon-key' // anon / public key
)
进入项目后台 Authentication 区:
{{ .ConfirmationURL }}(魔法链接)替换为 {{ .Token }}(6 位验证码)。signUp / resetPasswordForEmail 发出的就是数字码,契合你「邮箱验证码」的需求。OTP / expir);发送限流保持 Supabase 默认或按需收紧。weak_password 错误。Supabase 已托管两张核心表,你不用建:
auth.users:账号主体(id, email, encrypted_password, email_confirmed_at, last_sign_in_at …)。auth.identities:每种登录方式的身份(邮箱 / 将来微信 openid …)。这是「一个用户挂多种登录方式」的原生支撑,对应我们之前 users + auth_identities 的设计思路。profiles 表(扩展字段用)邮箱密码之外想存昵称、头像等,建一张 profiles,用 RLS 锁住「只能看自己的」:
create table public.profiles (
id uuid primary key references auth.users(id) on delete cascade,
full_name text,
avatar_url text,
created_at timestamptz default now()
);
alter table public.profiles enable row level security;
create policy "用户只能访问自己的档案"
on public.profiles for all
using (auth.uid() = id)
with check (auth.uid() = id);
新用户注册确认后自动在 profiles 插一行,用 trigger:
create or replace function public.handle_new_user()
returns trigger language plpgsql security definer set search_path = public as $$
begin
insert into public.profiles (id) values (new.id);
return new;
end;
$$;
create trigger on_auth_user_created
after insert on auth.users
for each row execute function public.handle_new_user();
步骤:
email + password。signUp —— Supabase 建用户(此时 email_confirmed_at 为空)并按模板向邮箱发 6 位验证码。
const { data, error } = await supabase.auth.signUp({
email,
password,
options: { emailRedirectTo: 'https://your-domain.com/welcome' }
})
if (error) {
// user_already_exists → 邮箱已注册,提示去登录
// weak_password → 密码太弱
}
verifyOtp(注意 type: 'email')校验码,成功即标记邮箱已确认并返回 session:
const { data, error } = await supabase.auth.verifyOtp({
email,
token: '123456', // 用户输入的 6 位码
type: 'email'
})
// data.session 即为登录态(access + refresh)
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'SIGNED_IN') { /* 跳首页 */ }
})
要点:
email_not_confirmed。signUp 报 user_already_exists(即我们之前 register 的查重逻辑,Supabase 原生提供)。步骤(全原生,零自定义):
const { data, error } = await supabase.auth.signInWithPassword({
email,
password
})
if (error) {
// invalid_credentials → 统一报「邮箱或密码错误」(Supabase 已做防枚举)
// email_not_confirmed → 提示先完成邮箱验证
} else {
// data.session 可用,data.user 为用户信息
}
要点:
session 含 access_token(短期)+ refresh_token(长期);客户端 SDK 自动管理刷新。步骤:
resetPasswordForEmail —— 按第二章模板发验证码邮件:
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: 'https://your-domain.com/reset'
})
// 返回统一响应,不暴露该邮箱是否注册(内置防枚举)
verifyOtp(type: 'recovery')拿到临时 session:
const { data, error } = await supabase.auth.verifyOtp({
email,
token: '123456',
type: 'recovery'
})
updateUser 设置新密码:
const { error } = await supabase.auth.updateUser({ password: newPassword })
要点:
otp_expired → 验证码过期,引导重新发送。resetPasswordForEmail 已内置,无需你额外处理。步骤(全原生):
// 当前设备登出(吊销本机 refresh token + 清 session)
await supabase.auth.signOut()
// 全端登出(吊销该用户所有 session,对应「踢下线」)
await supabase.auth.signOut({ scope: 'global' })
// 保留当前、登出其他设备
await supabase.auth.signOut({ scope: 'others' })
要点:
scope 三档:global(默认,全端)/ local(仅当前)/ others(除当前外)。REFRESH_TOKEN_REUSE_INTERVAL 可配短暂容错窗口。signInWithPassword、resetPasswordForEmail 返回统一响应。auth.uid(),别用 service_role 跑普通查询。anon key 前端可用;service_role key 仅可信服务端,绝不进客户端包。数据模型(auth.identities)已原生支持「一个用户多种登录方式」,扩展时不重构。
supabase.auth.signInWithOtp({ phone: '+86138...' })supabase.auth.verifyOtp({ phone, token, type: 'sms' })EXTERNAL_WECHAT_*)。supabase.auth.signInWithOAuth({ provider: 'wechat' }),零自定义代码。小程序用 wx.login() 拿 code,不是标准 OAuth 重定向,需自定义 Edge Function(或 Node 后端):
① 小程序 wx.login() 拿到 code
② 调你的 Edge Function:
code → 调微信接口换 openid / unionid
→ 用 Supabase Admin API 查/建用户
→ 由 Supabase Auth 正式签发 session(返回 access_token + refresh_token)
③ 小程序客户端拿到 session,后续与邮箱登录完全一致
supabase-mp-js(微信小程序专用客户端)可直接用。service_role key 与微信 AppSecret 绝不能放前端。supabase.auth.linkIdentity 或 Admin API 把它挂到同一邮箱用户下,实现「邮箱 + 微信」互通。profiles 之外若有其他业务表,记得逐表配 auth.uid() 策略。Supabase 没有内置角色系统,RBAC 是你用「角色字段 + RLS 策略」自己搭的。下面分两档,按你当前的「管理员 / 普通用户」需求,先用 11.3 最简方案即可,将来真有多角色多权限再上 11.4。
Supabase 的用户元数据有两种,授权用的角色只能放对的地方:
| 字段 | 谁能改 | 能否用于授权 |
|---|---|---|
| raw_user_meta_data(即 user_metadata) | 用户自己(updateUser 能改) | ❌ 绝对不行 |
| raw_app_meta_data(即 app_metadata) | 仅服务端(Admin API / SQL) | ✅ 可以 |
致命坑:若把 role: 'admin' 存到 user_metadata,任何用户调一次 supabase.auth.updateUser({ data: { role: 'admin' } }) 就能把自己提权成管理员——这是经典提权漏洞。所以角色必须放 app_metadata,且只允许服务端写。
另一原则:客户端判断角色只用于 UX(隐藏按钮、跳转),真正的权限强制必须写在 RLS 里——客户端检查能被绕过(改请求、调 API),RLS 不能。
零额外表,角色烤进 JWT,RLS 直接读,无数据库查询开销。
步骤 1 · 写 RLS 策略,读 JWT 里的角色
默认角色用 coalesce(..., 'user') 兜底:新用户无需预先写角色,只有被显式提升为 admin 的才是管理员(app_metadata 为 null 时 ->> 'role' 返回 null,自动兜底为普通用户)。
-- 管理员:所有行全权限
create policy "管理员全权限" on public.articles
for all to authenticated
using ( coalesce((select auth.jwt() -> 'app_metadata' ->> 'role'), 'user') = 'admin' );
-- 普通用户:只能碰自己的行(owner 检查)
create policy "用户管自己的" on public.articles
for all to authenticated
using ( (select auth.uid()) = author_id );
auth.jwt()返回的 JWT 默认就包含app_metadata,所以无需额外 Hook 即可读取。(select auth.jwt() ...)包一层子查询是官方推荐写法,能让每条语句复用、避免重复求值。
步骤 2 · 分配管理员(只能从服务端改 app_metadata,绝不从客户端)
三种方式任选:
Authentication → Users → 找到该用户 → 编辑其 app_metadata JSON,加 "role":"admin"。service_role key(绝不可进前端):
const supabaseAdmin = createClient(URL, SERVICE_ROLE_KEY) // 仅服务端运行
await supabaseAdmin.auth.admin.updateUserById(userId, {
app_metadata: { role: 'admin' }
})
update auth.users
set raw_app_meta_data = jsonb_build_object('role', 'admin')
where id = '目标用户uuid';
步骤 3 · 客户端只做 UX(可选,非安全边界)
用 jwt-decode 解 access_token 拿角色控制按钮显隐;不要把它当权限校验——真正拦截在 RLS。
import { jwtDecode } from 'jwt-decode'
supabase.auth.onAuthStateChange((event, session) => {
if (session) {
const role = jwtDecode(session.access_token).app_metadata?.role
// role === 'admin' 才显示「管理后台」入口(仅 UI 层)
}
})
当你需要「编辑能发帖、财务能看账单、运维能删数据」这类细粒度权限时,上标准模型。
步骤 1 · 建角色与权限表
create type public.app_role as enum ('admin', 'editor', 'viewer');
create type public.app_permission as enum ('articles.read', 'articles.write', 'articles.delete');
create table public.user_roles (
user_id uuid references auth.users(id) on delete cascade,
role public.app_role,
primary key (user_id, role)
);
create table public.role_permissions (
role public.app_role,
permission public.app_permission,
primary key (role, permission)
);
-- 示例:admin 拥有全部,editor 仅有读写
insert into public.role_permissions values
('admin', 'articles.read'), ('admin', 'articles.write'), ('admin', 'articles.delete'),
('editor', 'articles.read'), ('editor', 'articles.write');
步骤 2 · 用 Auth Hook 把角色烤入 JWT(避免每次查库)
在 Authentication → Hooks 启用 custom_access_token_hook,指向下面这个函数。它在新令牌签发时把角色写进 JWT 的 user_role claim:
create or replace function public.custom_access_token_hook(event jsonb)
returns jsonb language plpgsql stable security definer set search_path = '' as $$
declare
claims jsonb;
role public.app_role;
begin
select role into role
from public.user_roles
where user_id = (event ->> 'user_id')::uuid
limit 1;
claims := event -> 'claims';
claims := jsonb_set(claims, '{user_role}', to_jsonb(coalesce(role, 'viewer')));
return jsonb_set(event, '{claims}', claims);
end;
$$;
示例假设一个用户单一角色;若需「一个用户多角色」,把
user_role存为数组,并在下一步authorize()中用?|(包含)判断。
步骤 3 · 写 authorize() 函数(官方推荐写法)
SECURITY DEFINER 防止 RLS 递归;set search_path = '' 防止路径注入:
create or replace function public.authorize(requested_permission public.app_permission)
returns boolean language plpgsql stable security definer set search_path = '' as $$
declare
user_role public.app_role;
begin
select (auth.jwt() ->> 'user_role')::public.app_role into user_role;
return exists (
select 1 from public.role_permissions
where role = user_role and permission = requested_permission
);
end;
$$;
步骤 4 · 在 RLS 里一行调用
create policy "授权才可删" on public.articles
for delete to authenticated
using ( (select authorize('articles.delete')) );
create policy "授权才可写" on public.articles
for insert to authenticated
with check ( (select authorize('articles.write')) );
app_metadata / user_roles 里改了角色,不会立刻生效——要等 Access Token 刷新(默认约 1 小时)新 JWT 才带新角色。user_roles 表(DB 实时读取),封禁立刻生效;或主动 supabase.auth.refreshSession() 强制刷新令牌,让前端马上感知新角色。app_metadata.role + RLS 读 JWT,默认 user、手动升 admin,零额外表。authorize() 函数,不提前建。app_metadata,从服务端写。用 Supabase 后,四个流程的核心逻辑全由 supabase.auth.* 原生方法覆盖;你的工作量集中在「建项目 + 后台开 Email OTP + 改邮件模板为发码 + 配 RLS」。唯一需要写后端代码的是将来微信小程序那块 Edge Function,而账户模型(多 identity 合一)Supabase 原生支持,无需重构。权限 / RBAC 见第十一章:角色放 app_metadata(绝不放可被用户自改的 user_metadata)+ RLS 强制,admin/user 二分用最简方案即可,多角色多权限再上完整 RBAC 表 + authorize() 函数。