Supabase魔法链接 #
一、魔法链接概述 #
1.1 什么是魔法链接 #
text
魔法链接登录流程
├── 1. 用户输入邮箱地址
├── 2. 系统发送包含链接的邮件
├── 3. 用户点击邮件中的链接
├── 4. 自动完成登录
└── 5. 无需记忆密码
1.2 魔法链接优势 #
| 优势 | 说明 |
|---|---|
| 无密码 | 用户无需记忆密码 |
| 安全 | 链接一次性使用,有效期短 |
| 便捷 | 一键登录 |
| 减少支持 | 无密码重置问题 |
二、发送魔法链接 #
2.1 基础用法 #
typescript
// 发送魔法链接
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
})
2.2 带配置发送 #
typescript
// 带自定义配置
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
emailRedirectTo: 'https://example.com/auth/callback',
data: {
full_name: 'John Doe',
},
},
})
2.3 魔法链接表单 #
tsx
import { useState } from 'react'
import { supabase } from '../lib/supabase'
export function MagicLinkForm() {
const [email, setEmail] = useState('')
const [loading, setLoading] = useState(false)
const [sent, setSent] = useState(false)
const [error, setError] = useState('')
const handleSendMagicLink = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: window.location.origin + '/auth/callback',
},
})
setLoading(false)
if (error) {
setError(error.message)
} else {
setSent(true)
}
}
if (sent) {
return (
<div className="success">
<h2>Check your email</h2>
<p>We sent a magic link to {email}</p>
<button onClick={() => setSent(false)}>
Resend
</button>
</div>
)
}
return (
<form onSubmit={handleSendMagicLink}>
<h2>Sign in with magic link</h2>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Magic Link'}
</button>
{error && <p className="error">{error}</p>}
</form>
)
}
三、处理魔法链接回调 #
3.1 自动处理 #
typescript
// Supabase客户端自动处理魔法链接
// 只需确保redirectTo页面正确配置
const supabase = createClient(url, key, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true, // 自动检测URL中的token
},
})
3.2 手动处理回调 #
typescript
// auth/callback.tsx
import { useEffect } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
import { supabase } from '../lib/supabase'
export function MagicLinkCallback() {
const [searchParams] = useSearchParams()
const navigate = useNavigate()
useEffect(() => {
const handleCallback = async () => {
// 获取URL中的token
const token = searchParams.get('token')
const type = searchParams.get('type')
if (token && type === 'magiclink') {
const { error } = await supabase.auth.verifyOtp({
token_hash: token,
type: 'magiclink',
})
if (error) {
console.error('Magic link error:', error)
navigate('/login?error=invalid_link')
} else {
navigate('/dashboard')
}
}
}
handleCallback()
}, [searchParams, navigate])
return <div>Logging in...</div>
}
四、配置魔法链接 #
4.1 Dashboard配置 #
text
Dashboard > Authentication > Providers > Email
魔法链接配置
├── Enable Magic Link
│ └── 启用魔法链接功能
│
├── Secure email change
│ └── 更改邮箱时发送确认邮件
│
└── Token expiry
└── 链接有效期(默认24小时)
4.2 邮件模板配置 #
text
Dashboard > Authentication > Email Templates
Magic Link模板
├── Subject: Your Magic Link
├── Content:
│ <h2>Magic Link</h2>
│ <p>Click the link below to sign in:</p>
│ <a href="{{ .ConfirmationURL }}">
│ Sign in to {{ .ProjectName }}
│ </a>
│ <p>Link expires in 24 hours.</p>
└── 可用变量:
├── {{ .ConfirmationURL }} - 魔法链接URL
├── {{ .Token }} - Token值
├── {{ .TokenHash }} - Token哈希
├── {{ .Email }} - 用户邮箱
└── {{ .ProjectName }} - 项目名称
4.3 自定义邮件模板 #
html
<!-- 自定义魔法链接邮件模板 -->
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; }
.button {
background: #3ECF8E;
color: white;
padding: 12px 24px;
text-decoration: none;
border-radius: 8px;
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to {{ .ProjectName }}</h1>
<p>Click the button below to sign in:</p>
<a href="{{ .ConfirmationURL }}" class="button">
Sign In
</a>
<p>Or copy this link:</p>
<code>{{ .ConfirmationURL }}</code>
<p><small>This link expires in 24 hours.</small></p>
</div>
</body>
</html>
五、PKCE流程 #
5.1 什么是PKCE #
text
PKCE (Proof Key for Code Exchange)
├── 增强OAuth安全性
├── 防止授权码拦截攻击
├── 适用于移动端和SPA
└── 推荐用于魔法链接
5.2 使用PKCE #
typescript
// 使用PKCE流程
const { data, error } = await supabase.auth.signInWithOtp({
email: 'user@example.com',
options: {
shouldCreateUser: true,
emailRedirectTo: 'https://example.com/auth/callback',
},
})
六、魔法链接vs密码登录 #
6.1 对比 #
| 特性 | 魔法链接 | 密码登录 |
|---|---|---|
| 用户体验 | 一键登录 | 需输入密码 |
| 安全性 | 无密码泄露风险 | 密码可能泄露 |
| 依赖 | 需要邮箱访问 | 仅需邮箱 |
| 登录速度 | 需等待邮件 | 即时登录 |
| 离线使用 | 不支持 | 支持 |
6.2 选择建议 #
text
推荐使用魔法链接的场景
├── 安全性要求高
├── 用户经常忘记密码
├── 移动端应用
└── 希望简化登录流程
推荐使用密码登录的场景
├── 需要离线登录
├── 企业内网应用
├── 邮件系统不可靠
└── 用户习惯密码登录
七、结合使用 #
7.1 提供多种登录方式 #
tsx
export function LoginPage() {
const [method, setMethod] = useState<'password' | 'magic'>('magic')
return (
<div>
<div className="tabs">
<button
onClick={() => setMethod('magic')}
className={method === 'magic' ? 'active' : ''}
>
Magic Link
</button>
<button
onClick={() => setMethod('password')}
className={method === 'password' ? 'active' : ''}
>
Password
</button>
</div>
{method === 'magic' ? (
<MagicLinkForm />
) : (
<PasswordForm />
)}
</div>
)
}
八、重新发送魔法链接 #
8.1 限制频率 #
typescript
// 防止滥用
let lastSentTime = 0
const COOLDOWN = 60000 // 60秒冷却
async function sendMagicLink(email: string) {
const now = Date.now()
if (now - lastSentTime < COOLDOWN) {
throw new Error('Please wait before requesting another link')
}
const { error } = await supabase.auth.signInWithOtp({ email })
lastSentTime = now
return { error }
}
8.2 带冷却的发送按钮 #
tsx
import { useState, useEffect } from 'react'
export function ResendButton() {
const [countdown, setCountdown] = useState(0)
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
return () => clearTimeout(timer)
}
}, [countdown])
const handleResend = async () => {
await supabase.auth.signInWithOtp({ email })
setCountdown(60)
}
return (
<button
onClick={handleResend}
disabled={countdown > 0}
>
{countdown > 0 ? `Resend in ${countdown}s` : 'Resend Magic Link'}
</button>
)
}
九、错误处理 #
9.1 常见错误 #
typescript
const errorMessages: Record<string, string> = {
'invalid_otp': '链接已过期或无效',
'user_not_found': '用户不存在',
'email_not_confirmed': '请先验证邮箱',
'rate_limit_exceeded': '请求过于频繁,请稍后再试',
}
async function handleMagicLink(email: string) {
const { error } = await supabase.auth.signInWithOtp({ email })
if (error) {
return errorMessages[error.message] || error.message
}
return null
}
十、最佳实践 #
10.1 安全建议 #
text
魔法链接安全建议
├── 设置合理的过期时间
├── 限制发送频率
├── 使用PKCE流程
├── 记录登录日志
└── 监控异常行为
10.2 用户体验优化 #
typescript
// 保存登录前的状态
async function sendMagicLinkWithState(email: string) {
// 保存当前路径
sessionStorage.setItem('redirectAfterLogin', window.location.pathname)
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: window.location.origin + '/auth/callback',
},
})
return { error }
}
// 登录后恢复状态
function afterLogin() {
const redirect = sessionStorage.getItem('redirectAfterLogin') || '/dashboard'
sessionStorage.removeItem('redirectAfterLogin')
window.location.href = redirect
}
十一、总结 #
魔法链接要点:
| 操作 | 方法 |
|---|---|
| 发送链接 | signInWithOtp({ email }) |
| 验证链接 | verifyOtp({ token, type }) |
| 配置回调 | emailRedirectTo |
| 自定义模板 | Dashboard > Email Templates |
下一步,让我们学习手机号认证!
最后更新:2026-03-28