身份认证
Supabase 内置了完整的用户认证系统,支持多种登录方式。本章节介绍如何实现用户注册、登录、OAuth 社交登录等功能。
认证概述
Supabase 的认证服务基于 GoTrue,它是一个开源的身份认证服务器,支持:
- 邮箱密码登录
- 魔法链接(Magic Link,无密码登录)
- 手机号 OTP 验证
- OAuth 社交登录(Google、GitHub、Apple 等)
- 企业级 SSO
- 匿名登录
认证系统使用 JWT(JSON Web Token)管理会话,与行级安全策略(RLS)无缝集成。
用户表结构
Supabase 自动维护一个 auth.users 表存储用户信息:
select id, email, created_at, last_sign_in_at
from auth.users;
这个表由系统管理,不建议直接修改。你可以创建一个 public.profiles 表存储额外的用户信息:
create table profiles (
id uuid primary key references auth.users(id) on delete cascade,
name text,
avatar_url text,
created_at timestamptz default now()
);
-- 用户注册时自动创建 profile
create or replace function public.handle_new_user()
returns trigger as $$
begin
insert into public.profiles (id, name, avatar_url)
values (new.id, new.raw_user_meta_data->>'name', new.raw_user_meta_data->>'avatar_url');
return new;
end;
$$ language plpgsql security definer;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
邮箱密码登录
注册用户
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-anon-key'
)
// 注册新用户
const { data, error } = await supabase.auth.signUp({
email: '[email protected]',
password: 'secure-password',
options: {
data: {
name: '张三',
avatar_url: 'https://example.com/avatar.jpg'
}
}
})
if (error) {
console.error('注册失败:', error.message)
} else {
console.log('注册成功:', data.user)
}
注册后,Supabase 会发送确认邮件。用户需要点击邮件中的链接激活账号。
配置邮件确认
在 Authentication → Providers 页面可以配置:
- 是否需要邮箱确认
- 确认邮件的有效期
- 自定义邮件模板
如果不需要邮箱确认(仅用于开发环境),可以关闭 "Enable email confirmations"。
登录
const { data, error } = await supabase.auth.signInWithPassword({
email: '[email protected]',
password: 'secure-password'
})
if (error) {
console.error('登录失败:', error.message)
} else {
console.log('登录成功:', data.session)
// session 包含 access_token, refresh_token, user 等
}
登出
const { error } = await supabase.auth.signOut()
if (error) {
console.error('登出失败:', error.message)
}
获取当前用户
// 获取当前会话
const { data: { session } } = await supabase.auth.getSession()
console.log('当前会话:', session)
// 获取当前用户
const { data: { user } } = await supabase.auth.getUser()
console.log('当前用户:', user)
监听认证状态变化
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
console.log('认证事件:', event)
console.log('会话:', session)
if (event === 'SIGNED_IN') {
console.log('用户已登录')
}
if (event === 'SIGNED_OUT') {
console.log('用户已登出')
}
if (event === 'TOKEN_REFRESHED') {
console.log('令牌已刷新')
}
}
)
// 取消监听
subscription.unsubscribe()
魔法链接登录
魔法链接是一种无密码登录方式,用户只需输入邮箱,系统会发送一个包含登录链接的邮件。
发送魔法链接
const { error } = await supabase.auth.signInWithOtp({
email: '[email protected]',
options: {
emailRedirectTo: 'https://your-app.com/auth/callback'
}
})
if (error) {
console.error('发送失败:', error.message)
} else {
console.log('魔法链接已发送')
}
用户点击邮件中的链接后,会被重定向到你的应用,并自动完成登录。
处理回调
在回调页面处理登录:
// 解析 URL 中的 token
const { data: { session }, error } = await supabase.auth.getSessionFromUrl()
if (error) {
console.error('登录失败:', error.message)
} else {
console.log('登录成功:', session)
}
手机号登录
使用手机号和 OTP 验证码登录:
发送验证码
const { error } = await supabase.auth.signInWithOtp({
phone: '+8613800138000'
})
if (error) {
console.error('发送失败:', error.message)
} else {
console.log('验证码已发送')
}
验证登录
const { data, error } = await supabase.auth.verifyOtp({
phone: '+8613800138000',
token: '123456' // 用户收到的验证码
})
if (error) {
console.error('验证失败:', error.message)
} else {
console.log('登录成功:', data.session)
}
配置短信服务
在 Authentication → Providers → Phone 页面配置短信提供商:
- Twilio
- MessageBird
- TextLocal
- Vonage
OAuth 社交登录
Supabase 支持 20+ 种 OAuth 提供商,包括 Google、GitHub、Apple、Facebook 等。
配置 OAuth 提供商
以 GitHub 为例:
- 在 GitHub 创建 OAuth App(Settings → Developer settings → OAuth Apps)
- 填写应用信息:
- Homepage URL:
https://your-app.com - Authorization callback URL:
https://your-project.supabase.co/auth/v1/callback
- Homepage URL:
- 获取 Client ID 和 Client Secret
- 在 Supabase Dashboard → Authentication → Providers → GitHub 中填入
发起 OAuth 登录
// 登录并重定向
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
redirectTo: 'https://your-app.com/auth/callback'
}
})
// 或者使用 popup 方式
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'github',
options: {
skipBrowserRedirect: true
}
})
if (data.url) {
// 打开新窗口进行 OAuth
window.open(data.url, '_blank', 'width=600,height=600')
}
处理 OAuth 回调
// 在回调页面
const { data: { session }, error } = await supabase.auth.getSessionFromUrl()
if (error) {
console.error('OAuth 登录失败:', error.message)
} else {
console.log('登录成功:', session)
}
支持的 OAuth 提供商
// Google
await supabase.auth.signInWithOAuth({ provider: 'google' })
// Apple
await supabase.auth.signInWithOAuth({ provider: 'apple' })
// Facebook
await supabase.auth.signInWithOAuth({ provider: 'facebook' })
// Discord
await supabase.auth.signInWithOAuth({ provider: 'discord' })
// Twitter
await supabase.auth.signInWithOAuth({ provider: 'twitter' })
// GitLab
await supabase.auth.signInWithOAuth({ provider: 'gitlab' })
// Bitbucket
await supabase.auth.signInWithOAuth({ provider: 'bitbucket' })
// LinkedIn
await supabase.auth.signInWithOAuth({ provider: 'linkedin' })
// Slack
await supabase.auth.signInWithOAuth({ provider: 'slack' })
// Spotify
await supabase.auth.signInWithOAuth({ provider: 'spotify' })
// Twitch
await supabase.auth.signInWithOAuth({ provider: 'twitch' })
// Notion
await supabase.auth.signInWithOAuth({ provider: 'notion' })
匿名登录
匿名登录允许用户在不注册的情况下使用应用,后续可以转换为正式账号:
// 匿名登录
const { data, error } = await supabase.auth.signInAnonymously()
if (error) {
console.error('匿名登录失败:', error.message)
} else {
console.log('匿名用户:', data.user)
}
转换为正式账号:
// 为匿名用户设置邮箱密码
const { error } = await supabase.auth.updateUser({
email: '[email protected]',
password: 'secure-password'
})
密码管理
重置密码
// 发送重置密码邮件
const { error } = await supabase.auth.resetPasswordForEmail(
'[email protected]',
{
redirectTo: 'https://your-app.com/reset-password'
}
)
用户点击邮件链接后,在回调页面设置新密码:
// 更新密码
const { error } = await supabase.auth.updateUser({
password: 'new-secure-password'
})
修改密码
已登录用户修改密码:
const { error } = await supabase.auth.updateUser({
password: 'new-secure-password'
})
修改邮箱
const { error } = await supabase.auth.updateUser({
email: '[email protected]'
})
系统会发送确认邮件到新邮箱。
用户元数据
Supabase 允许存储自定义用户数据:
设置元数据
// 注册时设置
await supabase.auth.signUp({
email: '[email protected]',
password: 'password',
options: {
data: {
name: '张三',
age: 25,
preferences: {
theme: 'dark',
language: 'zh-CN'
}
}
}
})
// 更新元数据
await supabase.auth.updateUser({
data: {
name: '李四',
age: 26
}
})
读取元数据
const { data: { user } } = await supabase.auth.getUser()
console.log(user.user_metadata.name)
console.log(user.user_metadata.preferences.theme)
服务端认证
在服务端,你可以使用 service_role 密钥管理用户:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://your-project.supabase.co',
'your-service-role-key' // 注意:这是服务密钥
)
// 创建用户
const { data, error } = await supabase.auth.admin.createUser({
email: '[email protected]',
password: 'secure-password',
email_confirm: true // 自动确认邮箱
})
// 列出所有用户
const { data: { users } } = await supabase.auth.admin.listUsers()
// 根据 ID 获取用户
const { data: { user } } = await supabase.auth.admin.getUserById('user-uuid')
// 删除用户
await supabase.auth.admin.deleteUser('user-uuid')
Next.js 集成
在 Next.js 中,需要区分服务端和客户端的认证处理:
客户端
// lib/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
}
// components/AuthButton.tsx
'use client'
import { createClient } from '@/lib/supabase/client'
import { useEffect, useState } from 'react'
export function AuthButton() {
const [user, setUser] = useState(null)
const supabase = createClient()
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event, session) => {
setUser(session?.user ?? null)
}
)
return () => subscription.unsubscribe()
}, [])
const handleSignOut = async () => {
await supabase.auth.signOut()
}
if (user) {
return (
<button onClick={handleSignOut}>
登出
</button>
)
}
return <a href="/login">登录</a>
}
服务端
// app/actions/auth.ts
'use server'
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function signIn(formData: FormData) {
const supabase = await createClient()
const { error } = await supabase.auth.signInWithPassword({
email: formData.get('email') as string,
password: formData.get('password') as string
})
if (error) {
return { error: error.message }
}
revalidatePath('/', 'layout')
redirect('/')
}
export async function signOut() {
const supabase = await createClient()
await supabase.auth.signOut()
revalidatePath('/', 'layout')
redirect('/login')
}
中间件保护路由
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
export async function middleware(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
})
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll()
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value, options }) => {
request.cookies.set(name, value)
supabaseResponse.cookies.set(name, value, options)
})
},
},
}
)
const { data: { user } } = await supabase.auth.getUser()
// 未登录用户访问受保护页面
if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
const url = request.nextUrl.clone()
url.pathname = '/login'
return NextResponse.redirect(url)
}
return supabaseResponse
}
export const config = {
matcher: [
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
}
安全最佳实践
密码强度
建议在前端验证密码强度:
function validatePassword(password) {
const minLength = 8
const hasUpperCase = /[A-Z]/.test(password)
const hasLowerCase = /[a-z]/.test(password)
const hasNumbers = /\d/.test(password)
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password)
const errors = []
if (password.length < minLength) errors.push('密码至少 8 个字符')
if (!hasUpperCase) errors.push('密码需要包含大写字母')
if (!hasLowerCase) errors.push('密码需要包含小写字母')
if (!hasNumbers) errors.push('密码需要包含数字')
if (!hasSpecialChar) errors.push('密码需要包含特殊字符')
return errors
}
保护敏感路由
在 RLS 策略中使用 auth.uid() 检查用户身份:
-- 用户只能访问自己的数据
create policy "Users can access own data"
on profiles for all
using (auth.uid() = id);
刷新令牌
Supabase SDK 会自动刷新令牌,但你也可以手动刷新:
const { data, error } = await supabase.auth.refreshSession()
下一步
掌握身份认证后,你可以继续学习: