跳到主要内容

身份认证

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 为例:

  1. 在 GitHub 创建 OAuth App(Settings → Developer settings → OAuth Apps)
  2. 填写应用信息:
    • Homepage URL: https://your-app.com
    • Authorization callback URL: https://your-project.supabase.co/auth/v1/callback
  3. 获取 Client ID 和 Client Secret
  4. 在 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()

下一步

掌握身份认证后,你可以继续学习: