字节笔记本
2026年8月29日
十行代码做 Supabase 邮箱 OTP 登录
邮箱加密码这套登录,用户常忘密码,你这边还要处理重置邮件。Supabase 的邮箱 OTP 更短:给邮箱发一个 6 位码,用户填回来,verifyOtp 就能换到会话。官方演示把发码和校验收在十行以内。
下面按能跑通的顺序写:改模板、发码、校验,再补生产环境里容易踩的坑。文档入口是 Passwordless email logins 和 Email Templates。
OTP 和 Magic Link 差在哪
signInWithOtp 这个名字容易误会。默认邮件模板里写的是 {{ .ConfirmationURL }},发出去的是一条一次性链接,也就是 Magic Link。用户点链接才登录。
邮箱 OTP 走同一套实现。差别只在邮件内容:模板里写 {{ .Token }},用户收到的就是 6 位数字,在你自己的输入框里填。
有些邮箱会预抓链接(Microsoft Defender Safe Links 这类),Magic Link 还没等用户点就已经被消费掉,页面上会报 token 过期。OTP 不依赖点击,这个问题就绕开了。移动端也不用处理深链回跳。
先改 Magic Link 邮件模板
邮箱登录默认是开着的。要发验证码而不是链接,去 Dashboard → Authentication → Email Templates,打开 Magic Link 那一封,改成类似:
<h2>登录验证码</h2>
<p>请输入下面这组 6 位数字,一小时内有效:</p>
<p>{{ .Token }}</p>本地 CLI 或自托管不走 Dashboard 编辑器,改 supabase/config.toml 里对应的 HTML 文件。Management API 也能改 mailer_templates_magic_link_content。
默认同一邮箱 60 秒内只能再要一次码,码本身默认 1 小时过期。过期时间在 Authentication → Sign In / Providers → Email → Email OTP expiration。超过 86400 秒(一天)官方不建议,而且只能走 Management API。这个过期值同时管 Magic Link、注册确认、找回密码、改邮箱、邀请邮件,改之前先看清楚。
发码:signInWithOtp
项目里已经有 createClient 的话,发码就是一次调用:
const { data, error } = await supabase.auth.signInWithOtp({
email,
options: {
shouldCreateUser: false,
},
})成功时 user 和 session 都是 null。这时没有登录态,只是邮件已经发出去。页面上提示用户去收件箱即可。
shouldCreateUser 默认是 true:这个邮箱还没注册,也会先建号再发码。只想给已有用户登录,把它设成 false。失败时错误文案不会明确告诉你「账号不存在」还是「只能用社交登录」,这是故意的,页面上不要自己补一句「该邮箱未注册」。
校验:verifyOtp
用户把 6 位码填回来之后:
const { data, error } = await supabase.auth.verifyOtp({
email,
token,
type: 'email',
})成功会拿到 session(access token、refresh token、user)。客户端库会按你创建 client 时的存储设置把会话留下来,后面 getUser() / getSession() 就能用。
type 必须对得上发码那次的用途。邮箱登录或注册用 'email'。找回密码是 'recovery',邀请是 'invite',改邮箱是 'email_change'。旧文档里的 'signup'、'magiclink' 已经弃用。
拼起来就是官方说的「不到十行」:
const { error: sendError } = await supabase.auth.signInWithOtp({
email,
options: { shouldCreateUser: false },
})
if (sendError) throw sendError
const { data, error: verifyError } = await supabase.auth.verifyOtp({
email,
token,
type: 'email',
})
if (verifyError) throw verifyError
const session = data.session服务端渲染可以把邮件里的 {{ .TokenHash }} 接到自己的路由上,用 verifyOtp({ token_hash, type: 'email' }) 换会话,再重定向回前端。这样校验发生在服务器,适合要先看登录态再吐页面的场景。写法见 Email Templates 里 Redirecting the user to a server-side endpoint。
上线前这几件事
内置发信只适合本地试。量一大就进垃圾箱,或者干脆发不出去。生产环境要接自己的 SMTP(Resend、AWS SES、邮管局都可以),并在 Dashboard 里关掉邮件追踪类改写链接的功能,否则模板里的地址会被中间层换掉。
公开接口建议开 CAPTCHA。signInWithOtp 的 options.captchaToken 把前端拿到的 token 带上。不接的话,别人拿你的 anon key 就能对一堆邮箱狂发验证码。
码猜错有次数限制,官方按暴力破解来防。页面上失败就提示「验证码不对或已过期」,让用户重新要一封。不要在接口里回显「还剩几次」。
PKCE 流程下,邮件里更常见的是带 token_hash 的链接,而不是让用户手输 6 位。Web 应用如果已经在用 PKCE,对照文档把模板和 verifyOtp 参数对齐,不要 Magic Link 和 OTP 两套混在同一封邮件里各用各的。
什么时候用
适合内部工具、控制台、不想养一套密码重置流程的产品。用户已经有邮箱,多一步填码通常比记住密码轻松。
不适合把登录完全交给即时通讯。邮箱有延迟,验证码进垃圾箱也常见。面向海外用户可以 Magic Link 和 OTP 并存,模板里同时放链接和数字;国内邮箱预抓没那么凶,OTP 更稳一点。
API 参考:signInWithOtp、verifyOtp。