信箱驗證與手機驗證
信箱驗證與手機驗證看起來很像登入流程的一部分,但它們的核心目的通常不是「確認這次登入的人是本人」,而是「確認使用者能控制某個聯絡方式」。這個差異很重要,因為一旦把信箱驗證、手機 OTP 和 MFA 混在一起,系統設計就很容易把安全責任放錯地方。
一句話回答:信箱驗證與手機驗證主要是在驗證使用者是否控制某個 email 或 phone,常用於帳號啟用、通知管道確認、帳號恢復與風險驗證;設計時要使用高熵、短效、單次使用的 verification token 或 OTP,搭配重寄冷卻、嘗試次數限制、通用回應、防帳號枚舉、稽核紀錄與風險控管。Email 驗證通常不應被視為高強度登入因素,SMS OTP 也有 SIM swap 與電信通道風險。
一、 必考觀念
1. 驗證聯絡方式不等於登入
信箱驗證通常證明:
使用者目前可以控制這個 email inbox。手機驗證通常證明:
使用者目前可以接收這個 phone number 的簡訊或語音。它們不直接證明:
- 使用者真實姓名。
- 使用者永遠控制這個信箱或手機。
- 這次登入一定是本人。
- 使用者有某個 API 權限。
所以在系統模型裡,email / phone verification 更接近 Identity Proofing 或 contact verification,而不是完整 Authorization。
延伸讀:身份驗證、授權與身份識別的差異。
2. 常見用途
| 用途 | 說明 |
|---|---|
| 註冊帳號啟用 | 確認 email 或 phone 可用,降低假帳號 |
| 通知管道確認 | 確保安全通知、帳單、提醒可以送達 |
| 忘記密碼 | 透過已驗證聯絡方式啟動恢復流程 |
| 風險驗證 | 新裝置、新地點、高風險操作時加一道確認 |
| MFA | 可作為較弱的第二因素,但要理解風險 |
| 帳號綁定 | 綁定或更換 email / phone 前確認控制權 |
同一種技術可以出現在不同流程,但安全語意不同。
例如 Email OTP 用於「驗證信箱」和用於「登入第二步」不是同一件事。
3. 驗證碼不是越長越安全就好
設計 OTP 要同時看:
- 位數。
- 有效時間。
- 嘗試次數。
- 重寄頻率。
- 是否單次使用。
- 是否綁定用途與帳號。
- 是否容易被釣魚或攔截。
例如 6 位數 OTP 只有 1,000,000 種組合,如果沒有嘗試次數限制,就可以被暴力猜。
所以 OTP 安全依賴的是整套流程,不只是位數。
二、 信箱驗證流程
1. 驗證連結流程
最常見做法是寄出 verification link:
使用者註冊
-> server 建立 user,狀態為 pending
-> server 產生 verification token
-> 寄出驗證信
-> 使用者點擊連結
-> server 驗證 token
-> emailVerified = true
-> 帳號啟用或解除限制2. Token 要怎麼設計?
Verification token 應該:
- 高熵、不可預測。
- 短效。
- 單次使用。
- 綁定用途。
- 綁定 user 或 email。
- 只存 hash,不存明文 token。
資料表可以這樣:
type VerificationToken = {
id: string
userId: string
type: 'email_verification' | 'phone_verification' | 'password_reset'
target: string
tokenHash: string
expiresAt: string
consumedAt?: string
attempts: number
createdAt: string
}寄給使用者的是明文 token:
https://example.com/verify-email?token=raw-token資料庫存的是:
hash(raw-token)這樣即使 token table 外洩,攻擊者也不能直接拿 token 完成驗證。
3. 為什麼 token 要單次使用?
如果 token 可以重複使用,風險會變高:
- 使用者轉寄驗證信。
- Email 被之後入侵。
- Link 留在瀏覽器歷史紀錄。
- Link 被 log 或 analytics 收集。
- Token 被截圖或複製。
驗證成功後要立刻標記:
consumedAt = now之後再使用同一 token,應該失敗。
4. 過期時間怎麼設?
沒有唯一答案,看用途:
| 用途 | 建議方向 |
|---|---|
| Email verification link | 幾小時到一天常見 |
| Email OTP | 幾分鐘到十幾分鐘 |
| Password reset token | 通常更短,且單次使用 |
| 高風險操作確認 | 幾分鐘 |
越敏感的用途,有效時間越短。
但註冊驗證信如果太短,使用者體驗會很差,所以要搭配重寄流程。
三、 Email OTP 與驗證連結
1. Email OTP 流程
Email OTP 是寄一組短碼:
你的驗證碼是 839204,10 分鐘內有效。使用者回到網站輸入:
POST /api/verify-email-otp
Content-Type: application/json
{
"email": "user@example.com",
"code": "839204"
}優點:
- 手機 App / 多裝置場景比較順。
- 不一定要打開 link。
- 可以在同一個畫面完成。
缺點:
- 需要防猜碼。
- 使用者容易輸錯。
- Email 延遲會造成重寄問題。
- Code 可能被釣魚頁騙走。
2. Link 和 OTP 怎麼選?
| 方式 | 適合 | 注意 |
|---|---|---|
| 驗證連結 | 註冊啟用、信箱確認 | 避免 token 出現在第三方 log |
| Email OTP | App 流程、同頁驗證、短時確認 | 要限制嘗試次數 |
| Magic link login | 無密碼登入 | 它已經是登入流程,風險高於單純信箱驗證 |
不要把所有 email link 都視為同一件事。
註冊信箱驗證 link 和 magic login link 的安全語意不同:
- 驗證信箱:確認聯絡方式。
- Magic login:直接建立登入狀態。
Magic login 需要更嚴格的 token 設計、裝置提示與 session 建立策略。
3. Email 不適合作為高強度 out-of-band authentication
NIST SP 800-63B 對 email 作為 out-of-band authentication 有明確限制,原因包含 email 本身常只靠密碼保護、可能在傳輸或中繼伺服器被攔截,也可能受到 DNS rerouting 等攻擊。不過 NIST 也區分了「用來驗證 email 地址的確認碼」不等同於身份驗證流程。
工程上可以這樣理解:
Email verification:可以用來確認 email。
Email MFA:安全強度有限,不應當作高信任登入因素。四、 手機驗證與 SMS OTP
1. SMS OTP 流程
使用者輸入手機號碼
-> server 標準化 phone number
-> 產生 OTP
-> 透過 SMS provider 寄出
-> 使用者輸入 OTP
-> server 驗證 OTP
-> phoneVerified = true資料表可以和 email verification 共用概念:
type OtpChallenge = {
id: string
userId?: string
type: 'phone_verification' | 'login_mfa' | 'password_reset'
target: string
codeHash: string
expiresAt: string
attempts: number
maxAttempts: number
consumedAt?: string
lastSentAt: string
}2. 手機號碼要標準化
不要直接存使用者輸入:
0912-345-678
0912345678
+886912345678應該轉成一致格式,例如 E.164:
+886912345678要注意:
- 國碼。
- 前導 0。
- 格式化字元。
- 同一號碼唯一性。
- 使用者換號。
- 號碼是否可接收 SMS。
3. SMS OTP 的風險
SMS OTP 比沒有第二因素好,但不是最高強度。
風險包含:
- SIM swap。
- 號碼移轉攻擊。
- 電信通道攔截。
- 手機遺失。
- 簡訊預覽外洩。
- 惡意 App 讀取簡訊。
- 釣魚頁即時轉送 OTP。
- SMS provider 故障或延遲。
NIST SP 800-63B 也把透過公眾電話網路 PSTN 的 out-of-band 驗證列為 restricted authenticator,並建議考慮 SIM swap、number porting 等風險指標。
4. SMS 適合什麼?
SMS 適合:
- 低到中風險場景。
- 驗證手機聯絡方式。
- 作為帳號恢復的一環。
- 使用者族群無法使用 TOTP / Passkey 時的過渡方案。
不建議單獨依賴 SMS:
- 金融高風險操作。
- 管理員帳號登入。
- 政府或醫療高信任身份。
- 高價值資產轉移。
高風險場景應該優先考慮 TOTP、Passkey、硬體安全金鑰、憑證或風險式驗證。
五、 重寄、過期與嘗試限制
1. 重寄要有冷卻時間
如果沒有冷卻時間,攻擊者可以:
- 大量寄信或簡訊造成成本。
- 騷擾別人的 email / phone。
- 對 SMS provider 造成費用攻擊。
- 製造大量 OTP challenge。
常見策略:
第一次可立即寄
60 秒內不能重寄
短時間內最多 N 次
超過後要求稍後再試或額外驗證前端可以倒數,但後端一定要執行限制。
2. 驗證碼要限制嘗試次數
例如 6 位數 OTP,若沒有嘗試限制就很危險。
應該限制:
同一 challenge 最多嘗試 5 次
同一 target 每小時最多建立 N 個 challenge
同一 IP 每分鐘最多 N 次驗證錯誤太多時:
- 讓 challenge 失效。
- 要求重新發送。
- 增加冷卻時間。
- 高風險時要求更強驗證。
3. 重寄時要不要產生新 code?
看設計。
選項一:重寄同一個 code。
優點:
- 使用者收到多封信或多封簡訊時,不容易混亂。
缺點:
- code 暴露時間可能變長。
選項二:每次重寄產生新 code,舊 code 失效。
優點:
- 安全邊界更清楚。
缺點:
- 使用者可能輸入上一封簡訊的 code。
不管哪種,都要在 UI 文案上講清楚,例如「請使用最新一封簡訊中的驗證碼」。
六、 防帳號枚舉
1. 發送驗證信也可能洩漏帳號存在
忘記密碼或重寄驗證信常見錯誤:
此 email 尚未註冊這會讓攻擊者枚舉帳號。
更安全的回應:
如果這個信箱已註冊,我們會寄出後續指示。OWASP Forgot Password Cheat Sheet 也建議忘記密碼流程給出一致訊息與一致反應時間,避免揭露帳號是否存在。
2. 內外訊息要分開
外部:
{
"message": "如果資料正確,我們會寄出驗證訊息。"
}內部 log:
verification_email_requested
target_exists=true
rate_limited=false這樣可以兼顧安全與營運除錯。
3. 註冊流程是否要提示 email 已存在?
這是產品取捨。
如果直接說:
這個 email 已經註冊使用者體驗好,但有帳號枚舉風險。
替代方式:
如果這個 email 已經有帳號,我們會寄出登入或重設密碼的指示。對高風險產品,應該更保守;對一般產品,可能會接受一定程度的可用性取捨。
七、 帳號狀態設計
1. Email verified 不一定等於 active
可以分清楚:
type User = {
id: string
email: string
emailVerified: boolean
phone?: string
phoneVerified: boolean
status: 'pending' | 'active' | 'disabled'
}狀態語意:
| 欄位 | 意義 |
|---|---|
| emailVerified | 是否確認 email 控制權 |
| phoneVerified | 是否確認 phone 控制權 |
| status | 帳號是否可使用 |
不要把所有東西都塞進 status,也不要只有一個 verified 讓人猜不出驗的是什麼。
2. 未驗證帳號能不能登入?
有兩種常見策略:
策略一:未驗證不能登入。
適合:
- 需要確保 email 可用。
- 高風險或濫用成本高。
策略二:可以登入,但限制功能。
適合:
- 想降低註冊摩擦。
- 產品需要先讓使用者體驗。
例如:
未驗證 email:可以瀏覽,但不能發文、不能付款、不能修改敏感資料。這不是純技術問題,是風險與轉換率取捨。
3. 更換 email / phone 要重新驗證
使用者修改 email 或 phone 時,不能只更新欄位。
應該:
要求目前登入狀態
-> 高風險時要求重新驗證密碼或 MFA
-> 寄驗證到新 email / phone
-> 驗證通過後才正式切換
-> 通知舊 email / phone
-> 寫 audit log這可以降低帳號被盜後直接換掉恢復管道的風險。
八、 和忘記密碼的差異
1. 信箱驗證 token 和 password reset token 不同
雖然技術相似,但語意不同:
| Token | 用途 | 風險 |
|---|---|---|
| Email verification | 驗證 email 控制權 | 通常中等 |
| Phone verification | 驗證手機控制權 | 通常中等 |
| Password reset | 取得重設密碼能力 | 高 |
| Magic login | 直接登入 | 高 |
不要把同一個 token 同時拿來做多個用途。
Token 應該綁定 type:
type: 'email_verification'不能拿 email verification token 去 reset password。
2. Password reset 要更嚴格
忘記密碼連結通常等同於:
拿到這個 token 的人,可以接管帳號。所以它要更嚴格:
- 更短效。
- 單次使用。
- 成功後撤銷其他 session。
- 不自動登入或謹慎自動登入。
- 通知使用者。
- 寫 audit log。
這會在後面忘記密碼專篇完整整理。
九、 追問題庫
Q1:信箱驗證算不算身份驗證?
通常不算完整身份驗證。它主要證明使用者能控制某個 email,屬於聯絡方式驗證或身份識別的一部分。若 email link 被設計成 magic login,才會進入登入流程,但那是另一種風險模型。
Q2:Email OTP 可以當 MFA 嗎?
可以在低風險場景作為額外確認,但不建議當成高強度 MFA。Email 本身常被密碼保護,可能被攔截、轉寄或釣魚,不能和硬體金鑰、Passkey、憑證等高強度方式相比。
Q3:SMS OTP 安全嗎?
比沒有第二因素好,但有 SIM swap、號碼移轉、攔截、簡訊預覽、釣魚即時轉送等風險。高風險場景不要只依賴 SMS OTP。
Q4:驗證碼應該幾分鐘過期?
沒有固定答案。Email / SMS OTP 常見是幾分鐘到十幾分鐘;高風險操作應更短。重點是搭配單次使用、重試限制、重寄冷卻與風險控管。
Q5:重寄驗證碼時舊碼要失效嗎?
兩種都可。新 code 使舊 code 失效比較清楚,但使用者可能輸入舊簡訊;重寄同一 code 體驗較好,但 code 暴露時間可能變長。重點是設計一致,並清楚提示使用者。
Q6:為什麼不要回覆「此 email 不存在」?
因為會造成帳號枚舉。忘記密碼、重寄驗證信等流程建議使用通用回應,例如「如果資料正確,我們會寄出指示」。
十、 實作題
題目:設計 Email Verification 流程
需求:
- 使用者註冊後寄驗證信。
- Token 24 小時內有效。
- Token 單次使用。
- 重寄有冷卻時間。
- 驗證成功後啟用 emailVerified。
簡化程式:
async function requestEmailVerification(user: User) {
await resendLimiter.check({
userId: user.id,
target: user.email,
})
const rawToken = randomToken()
const tokenHash = await hashToken(rawToken)
await verificationRepository.create({
userId: user.id,
type: 'email_verification',
target: user.email,
tokenHash,
expiresAt: addHours(new Date(), 24),
})
await mailer.sendEmailVerification({
to: user.email,
link: `https://example.com/verify-email?token=${rawToken}`,
})
}驗證:
async function verifyEmail(rawToken: string) {
const tokenHash = await hashToken(rawToken)
const token = await verificationRepository.findByHash(tokenHash)
if (!token || token.type !== 'email_verification') {
throw new InvalidTokenError()
}
if (token.consumedAt || token.expiresAt < new Date()) {
throw new InvalidTokenError()
}
await userRepository.markEmailVerified({
userId: token.userId,
email: token.target,
})
await verificationRepository.consume(token.id)
}注意:
rawToken只出現在 email link。- DB 只存
tokenHash。 - 驗證成功後 token 立即失效。
- 如果 user email 已經變更,要避免舊 token 驗證新 email。
題目延伸:設計 SMS OTP 流程
POST /phone-verifications
-> normalize phone
-> rate limit by phone + ip + user
-> create OTP challenge
-> send SMS
POST /phone-verifications/:id/verify
-> check challenge exists
-> check not expired
-> check attempts < maxAttempts
-> compare code hash
-> mark phoneVerified
-> consume challenge重點是 challenge id、code、target、attempts、expiresAt 都要有清楚邊界。
十一、 資深視角
1. 驗證流程會被濫用成成本攻擊
Email 和 SMS 都有成本:
- Email provider 信譽。
- SMS provider 費用。
- 寄送延遲。
- 客服成本。
- 使用者騷擾。
所以寄送 API 也要做安全設計,不只是驗證 API。
2. 聯絡方式是帳號恢復的核心資產
email / phone 常用於:
- 忘記密碼。
- 登入通知。
- 高風險操作確認。
- 裝置變更提醒。
所以修改 email / phone 的流程要比一般修改個人資料更嚴格。
3. 高風險場景要提高驗證強度
對一般論壇,email verification 可能足夠。
但對金融、醫療、政府服務:
- Email verification 不夠。
- SMS OTP 也可能不夠。
- 需要 MFA、Passkey、憑證或實名身份流程。
- 需要稽核紀錄與異常偵測。
驗證方式要配合資料敏感度與操作風險。
總結
| 問題 | 回答重點 |
|---|---|
| 信箱驗證 | 驗證使用者控制 email,不等於完整登入 |
| 手機驗證 | 驗證使用者控制 phone,有 SIM swap 等風險 |
| Verification token | 高熵、短效、單次使用、存 hash、綁定用途 |
| OTP | 要限制有效時間、嘗試次數、重寄頻率 |
| 重寄 | 需要冷卻時間與寄送限流 |
| 防枚舉 | 忘記密碼、重寄驗證信使用通用訊息 |
| 帳號狀態 | emailVerified、phoneVerified、status 分開設計 |
| 高風險 | Email / SMS 不應當作最高強度驗證 |
一句完整的面試回答可以是:
信箱驗證和手機驗證主要是在確認使用者能控制某個聯絡方式,不等於完整登入,也不等於授權。信箱驗證可以用 verification link 或 Email OTP,token 要高熵、短效、單次使用,資料庫只存 token hash,並綁定 user、target 和用途。手機驗證常用 SMS OTP,但要標準化 phone number,限制重寄頻率、驗證碼有效時間與嘗試次數,並理解 SMS 有 SIM swap、號碼移轉、攔截與釣魚風險。忘記密碼和重寄驗證信要避免帳號枚舉,對外使用通用訊息。修改 email 或 phone 屬於高風險操作,應要求重新驗證並通知舊聯絡方式。高信任場景不能只靠 Email 或 SMS,應搭配更強 MFA、Passkey、憑證或實名流程。