除了使用信箱驗證註冊以外,今天要介紹透過 Supabase Auth 與 Twilio SMS 服務,建立手機號碼登入系統,讓使用者無需記住密碼,只要輸入手機號碼接收驗證碼即可快速登入。
Twilio 是通訊 API 服務商,提供穩定的 SMS 發送服務,而且有免費額度可做開發測試,也可以使用預設 OTP 來測試,無需實際發送 SMS。
註冊完成後,需要進行基本設定:
Twilio 需要一個專用號碼來發送 SMS:
ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+1234567890
在 Supabase Dashboard 中設定手機登入:
進入 Authentication 設定
啟用 Phone 登入方式
在 Supabase 中設定 Twilio 憑證:
填入 Twilio 憑證:
Twilio Account SID: ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Twilio Auth Token: your_auth_token_here
Twilio Message Service SID: your_sender_phone_number_here
設定 SMS 模板(可選):
Your verification code is: {{ .Code }}
在您的專案中設定環境變數:
# .env.local
NEXT_PUBLIC_SUPABASE_URL=your_supabase_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
建立手機登入表單元件:
// components/phone-login-form.tsx
export default function PhoneLoginForm() {
// 發送驗證碼
const sendOTP = async (e: React.FormEvent) => {
try {
const { error } = await supabase.auth.signInWithOtp({
phone: phone,
});
if (error) {
setMessage(`錯誤:${error.message}`);
} else {
setMessage("驗證碼已發送到您的手機");
// 切換到 OTP 驗證步驟
setStep("otp");
}
} catch (error) {
setMessage("發送驗證碼時發生錯誤");
} finally {
setLoading(false);
}
};
// 驗證 OTP 並登入
const verifyOTP = async (e: React.FormEvent) => {
try {
const { data, error } = await supabase.auth.verifyOtp({
phone: phone,
token: otp,
type: "sms",
});
if (error) {
setMessage(`驗證失敗:${error.message}`);
} else {
setMessage("登入成功!");
// 登入成功後的處理
window.location.href = "/home";
}
} catch (error) {
setMessage("驗證時發生錯誤");
} finally {
setLoading(false);
}
};
// 重新發送驗證碼
const resendOTP = async () => {
setLoading(true);
try {
const { error } = await supabase.auth.signInWithOtp({
phone: phone,
});
if (error) {
setMessage(`重發失敗:${error.message}`);
} else {
setMessage("驗證碼已重新發送");
}
} catch (error) {
setMessage("重發驗證碼時發生錯誤");
} finally {
setLoading(false);
}
};
return <form>...</form>;
}
加入手機號碼格式驗證:
// utils/phone-validation.ts
export function validatePhoneNumber(phone: string): boolean {
// 基本的國際手機號碼格式檢查
const phoneRegex = /^\+[1-9]\d{1,14}$/;
return phoneRegex.test(phone);
}
export function formatPhoneNumber(phone: string): string {
// 移除所有非數字字符,保留 + 號
return phone.replace(/[^\d+]/g, "");
}
// 常見國家的手機號碼格式
export const phoneFormats = {
TW: { code: "+886", example: "+886912345678", length: 13 },
US: { code: "+1", example: "+1234567890", length: 12 },
CN: { code: "+86", example: "+8613812345678", length: 14 },
JP: { code: "+81", example: "+819012345678", length: 13 },
};
改善錯誤處理和使用者回饋:
// 在 PhoneLoginForm 中加入錯誤處理
const handleError = (error: any) => {
console.error("Phone login error:", error);
// 根據不同錯誤類型提供錯誤訊息
if (error.message?.includes("Invalid phone number")) {
setMessage("手機號碼格式不正確,請確認包含國碼");
} else if (error.message?.includes("SMS not sent")) {
setMessage("簡訊發送失敗,請稍後再試");
} else if (error.message?.includes("Invalid token")) {
setMessage("驗證碼不正確,請重新輸入");
} else if (error.message?.includes("Token expired")) {
setMessage("驗證碼已過期,請重新發送");
} else {
setMessage("發生未知錯誤,請稍後再試");
}
};
實作安全性:
// 限制重發驗證碼的頻率
const [canResend, setCanResend] = useState(true);
const [countdown, setCountdown] = useState(0);
const resendOTP = async () => {
if (!canResend) return;
setLoading(true);
setCanResend(false);
setCountdown(60); // 60 秒倒數
// 倒數計時器
const timer = setInterval(() => {
setCountdown((prev) => {
if (prev <= 1) {
clearInterval(timer);
setCanResend(true);
return 0;
}
return prev - 1;
});
}, 1000);
try {
const { error } = await supabase.auth.signInWithOtp({
phone: phone,
});
if (error) {
setMessage(`重發失敗:${error.message}`);
} else {
setMessage("驗證碼已重新發送");
}
} catch (error) {
setMessage("重發驗證碼時發生錯誤");
} finally {
setLoading(false);
}
};
測試手機登入功能:
# 啟動開發伺服器
pnpm run dev
# 訪問手機登入頁面
# http://localhost:3000/auth/phone-login
測試:
手機號碼登入為用程式提供了更便利的使用者體驗,同時保持了高度的安全性。
... to be continued
有任何想討論歡迎留言,或需要指正的地方請鞭大力一點,歡迎訂閱、按讚加分享,分享給想要提升開發效率的朋友