Supabase手机号认证 #
一、手机号认证概述 #
1.1 认证流程 #
text
手机号登录流程
├── 1. 用户输入手机号
├── 2. 系统发送短信验证码
├── 3. 用户输入验证码
├── 4. 验证成功后登录
└── 5. 创建/更新用户
1.2 手机号格式 #
text
手机号格式
├── E.164标准格式
├── 示例: +8613800138000
├── 包含国家代码
└── 不包含空格和特殊字符
二、配置SMS提供商 #
2.1 内置SMS提供商 #
text
Dashboard > Authentication > Providers > Phone
支持提供商
├── Twilio
├── MessageBird
├── TextLocal
├── Vonage
└── 自定义SMS网关
2.2 Twilio配置 #
text
1. 注册Twilio账号
2. 获取Account SID和Auth Token
3. 购买手机号
4. 在Supabase配置:
├── Account SID
├── Auth Token
└── From Number
2.3 自定义SMS网关 #
typescript
// 使用Edge Function发送短信
Deno.serve(async (req) => {
const { phone, code } = await req.json()
// 调用自定义SMS API
const response = await fetch('https://your-sms-provider.com/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer YOUR_API_KEY',
},
body: JSON.stringify({
phone,
message: `Your verification code is: ${code}`,
}),
})
return new Response(JSON.stringify({ success: true }))
})
三、发送验证码 #
3.1 基础用法 #
typescript
// 发送短信验证码
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+8613800138000',
})
3.2 带配置发送 #
typescript
// 带自定义配置
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+8613800138000',
options: {
shouldCreateUser: true,
channel: 'sms', // 或 'whatsapp'
},
})
3.3 发送验证码表单 #
tsx
import { useState } from 'react'
import { supabase } from '../lib/supabase'
export function PhoneLoginForm() {
const [phone, setPhone] = useState('')
const [loading, setLoading] = useState(false)
const [sent, setSent] = useState(false)
const [error, setError] = useState('')
const handleSendCode = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
// 格式化手机号
const formattedPhone = formatPhone(phone)
const { error } = await supabase.auth.signInWithOtp({
phone: formattedPhone,
})
setLoading(false)
if (error) {
setError(error.message)
} else {
setSent(true)
}
}
return (
<form onSubmit={handleSendCode}>
<h2>Sign in with phone</h2>
<input
type="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
placeholder="+86 138 0013 8000"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Sending...' : 'Send Code'}
</button>
{error && <p className="error">{error}</p>}
</form>
)
}
// 格式化手机号
function formatPhone(phone: string): string {
// 移除空格和特殊字符
let cleaned = phone.replace(/[\s\-\(\)]/g, '')
// 如果没有+号,添加中国区号
if (!cleaned.startsWith('+')) {
cleaned = '+86' + cleaned
}
return cleaned
}
四、验证验证码 #
4.1 验证OTP #
typescript
// 验证短信验证码
const { data, error } = await supabase.auth.verifyOtp({
phone: '+8613800138000',
token: '123456',
type: 'sms',
})
4.2 验证码输入组件 #
tsx
import { useState } from 'react'
import { supabase } from '../lib/supabase'
interface VerifyCodeProps {
phone: string
onVerified: () => void
}
export function VerifyCode({ phone, onVerified }: VerifyCodeProps) {
const [code, setCode] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError('')
const { data, error } = await supabase.auth.verifyOtp({
phone,
token: code,
type: 'sms',
})
setLoading(false)
if (error) {
setError(error.message)
} else {
onVerified()
}
}
return (
<form onSubmit={handleVerify}>
<h2>Enter verification code</h2>
<p>We sent a code to {phone}</p>
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value)}
placeholder="123456"
maxLength={6}
pattern="[0-9]{6}"
required
/>
<button type="submit" disabled={loading}>
{loading ? 'Verifying...' : 'Verify'}
</button>
{error && <p className="error">{error}</p>}
</form>
)
}
4.3 完整登录流程 #
tsx
export function PhoneAuth() {
const [phone, setPhone] = useState('')
const [step, setStep] = useState<'phone' | 'verify'>('phone')
if (step === 'verify') {
return (
<VerifyCode
phone={phone}
onVerified={() => {
window.location.href = '/dashboard'
}}
/>
)
}
return (
<PhoneLoginForm
onSent={(p) => {
setPhone(p)
setStep('verify')
}}
/>
)
}
五、WhatsApp验证 #
5.1 使用WhatsApp发送 #
typescript
// 通过WhatsApp发送验证码
const { data, error } = await supabase.auth.signInWithOtp({
phone: '+8613800138000',
options: {
channel: 'whatsapp',
},
})
5.2 WhatsApp配置 #
text
Dashboard > Authentication > Providers > Phone
WhatsApp配置
├── 启用WhatsApp
├── 配置Twilio WhatsApp
└── 或使用自定义提供商
六、更新手机号 #
6.1 更新手机号 #
typescript
// 更新用户手机号
const { data, error } = await supabase.auth.updateUser({
phone: '+8613900139000',
})
6.2 验证新手机号 #
typescript
// 验证新手机号
const { data, error } = await supabase.auth.verifyOtp({
phone: '+8613900139000',
token: '123456',
type: 'phone_change',
})
七、错误处理 #
7.1 常见错误 #
typescript
const errorMessages: Record<string, string> = {
'invalid_phone_number': '手机号格式不正确',
'invalid_otp': '验证码无效或已过期',
'rate_limit_exceeded': '请求过于频繁',
'sms_send_failed': '短信发送失败',
'phone_not_confirmed': '手机号未验证',
}
function getErrorMessage(error: any): string {
return errorMessages[error.message] || error.message
}
7.2 错误处理示例 #
typescript
async function sendVerificationCode(phone: string) {
try {
const { error } = await supabase.auth.signInWithOtp({ phone })
if (error) {
if (error.message === 'rate_limit_exceeded') {
return { success: false, message: '请等待60秒后再试' }
}
return { success: false, message: getErrorMessage(error) }
}
return { success: true }
} catch (err) {
return { success: false, message: '发送失败,请稍后重试' }
}
}
八、最佳实践 #
8.1 手机号验证 #
typescript
// 验证手机号格式
function validatePhone(phone: string): boolean {
// E.164格式验证
const e164Regex = /^\+[1-9]\d{1,14}$/
return e164Regex.test(phone)
}
// 中国手机号验证
function validateChinesePhone(phone: string): boolean {
const chineseRegex = /^(\+86)?1[3-9]\d{9}$/
return chineseRegex.test(phone)
}
8.2 验证码输入优化 #
tsx
// 自动提交6位验证码
function CodeInput({ onVerify }: { onVerify: (code: string) => void }) {
const [code, setCode] = useState('')
useEffect(() => {
if (code.length === 6) {
onVerify(code)
}
}, [code, onVerify])
return (
<input
type="text"
value={code}
onChange={(e) => setCode(e.target.value.replace(/\D/g, ''))}
maxLength={6}
placeholder="Enter 6-digit code"
/>
)
}
8.3 重发验证码 #
tsx
function ResendButton({ phone }: { phone: string }) {
const [countdown, setCountdown] = useState(0)
const [loading, setLoading] = useState(false)
useEffect(() => {
if (countdown > 0) {
const timer = setTimeout(() => setCountdown(countdown - 1), 1000)
return () => clearTimeout(timer)
}
}, [countdown])
const handleResend = async () => {
setLoading(true)
await supabase.auth.signInWithOtp({ phone })
setLoading(false)
setCountdown(60)
}
return (
<button
onClick={handleResend}
disabled={countdown > 0 || loading}
>
{countdown > 0
? `Resend in ${countdown}s`
: loading ? 'Sending...' : 'Resend Code'
}
</button>
)
}
九、成本考虑 #
9.1 SMS成本 #
text
短信成本估算
├── Twilio: ~$0.05-0.10/条
├── 国内短信: ~¥0.05/条
├── WhatsApp: 免费(需Twilio账号)
└── 建议: 开发环境使用WhatsApp
9.2 成本优化 #
typescript
// 限制发送频率
const rateLimiter = new Map<string, number>()
const COOLDOWN = 60000 // 60秒
function canSendSms(phone: string): boolean {
const lastSent = rateLimiter.get(phone)
if (lastSent && Date.now() - lastSent < COOLDOWN) {
return false
}
rateLimiter.set(phone, Date.now())
return true
}
十、总结 #
手机号认证要点:
| 操作 | 方法 |
|---|---|
| 发送验证码 | signInWithOtp({ phone }) |
| 验证验证码 | verifyOtp({ phone, token, type }) |
| 更新手机号 | updateUser({ phone }) |
| channel: ‘whatsapp’ |
下一步,让我们学习用户管理!
最后更新:2026-03-28