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 })
WhatsApp channel: ‘whatsapp’

下一步,让我们学习用户管理!

最后更新:2026-03-28