跳至主要內容
Skip to content

信箱驗證與手機驗證

信箱驗證與手機驗證看起來很像登入流程的一部分,但它們的核心目的通常不是「確認這次登入的人是本人」,而是「確認使用者能控制某個聯絡方式」。這個差異很重要,因為一旦把信箱驗證、手機 OTP 和 MFA 混在一起,系統設計就很容易把安全責任放錯地方。

一句話回答:信箱驗證與手機驗證主要是在驗證使用者是否控制某個 email 或 phone,常用於帳號啟用、通知管道確認、帳號恢復與風險驗證;設計時要使用高熵、短效、單次使用的 verification token 或 OTP,搭配重寄冷卻、嘗試次數限制、通用回應、防帳號枚舉、稽核紀錄與風險控管。Email 驗證通常不應被視為高強度登入因素,SMS OTP 也有 SIM swap 與電信通道風險。


一、 必考觀念

1. 驗證聯絡方式不等於登入

信箱驗證通常證明:

text
使用者目前可以控制這個 email inbox。

手機驗證通常證明:

text
使用者目前可以接收這個 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:

text
使用者註冊
-> server 建立 user,狀態為 pending
-> server 產生 verification token
-> 寄出驗證信
-> 使用者點擊連結
-> server 驗證 token
-> emailVerified = true
-> 帳號啟用或解除限制

2. Token 要怎麼設計?

Verification token 應該:

  • 高熵、不可預測。
  • 短效。
  • 單次使用。
  • 綁定用途。
  • 綁定 user 或 email。
  • 只存 hash,不存明文 token。

資料表可以這樣:

typescript
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:

text
https://example.com/verify-email?token=raw-token

資料庫存的是:

text
hash(raw-token)

這樣即使 token table 外洩,攻擊者也不能直接拿 token 完成驗證。

3. 為什麼 token 要單次使用?

如果 token 可以重複使用,風險會變高:

  • 使用者轉寄驗證信。
  • Email 被之後入侵。
  • Link 留在瀏覽器歷史紀錄。
  • Link 被 log 或 analytics 收集。
  • Token 被截圖或複製。

驗證成功後要立刻標記:

text
consumedAt = now

之後再使用同一 token,應該失敗。

4. 過期時間怎麼設?

沒有唯一答案,看用途:

用途建議方向
Email verification link幾小時到一天常見
Email OTP幾分鐘到十幾分鐘
Password reset token通常更短,且單次使用
高風險操作確認幾分鐘

越敏感的用途,有效時間越短。

但註冊驗證信如果太短,使用者體驗會很差,所以要搭配重寄流程。


三、 Email OTP 與驗證連結

1. Email OTP 流程

Email OTP 是寄一組短碼:

text
你的驗證碼是 839204,10 分鐘內有效。

使用者回到網站輸入:

http
POST /api/verify-email-otp
Content-Type: application/json

{
  "email": "user@example.com",
  "code": "839204"
}

優點:

  • 手機 App / 多裝置場景比較順。
  • 不一定要打開 link。
  • 可以在同一個畫面完成。

缺點:

  • 需要防猜碼。
  • 使用者容易輸錯。
  • Email 延遲會造成重寄問題。
  • Code 可能被釣魚頁騙走。
方式適合注意
驗證連結註冊啟用、信箱確認避免 token 出現在第三方 log
Email OTPApp 流程、同頁驗證、短時確認要限制嘗試次數
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 地址的確認碼」不等同於身份驗證流程。

工程上可以這樣理解:

text
Email verification:可以用來確認 email。
Email MFA:安全強度有限,不應當作高信任登入因素。

四、 手機驗證與 SMS OTP

1. SMS OTP 流程

text
使用者輸入手機號碼
-> server 標準化 phone number
-> 產生 OTP
-> 透過 SMS provider 寄出
-> 使用者輸入 OTP
-> server 驗證 OTP
-> phoneVerified = true

資料表可以和 email verification 共用概念:

typescript
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. 手機號碼要標準化

不要直接存使用者輸入:

text
0912-345-678
0912345678
+886912345678

應該轉成一致格式,例如 E.164:

text
+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。

常見策略:

text
第一次可立即寄
60 秒內不能重寄
短時間內最多 N 次
超過後要求稍後再試或額外驗證

前端可以倒數,但後端一定要執行限制。

2. 驗證碼要限制嘗試次數

例如 6 位數 OTP,若沒有嘗試限制就很危險。

應該限制:

text
同一 challenge 最多嘗試 5 次
同一 target 每小時最多建立 N 個 challenge
同一 IP 每分鐘最多 N 次驗證

錯誤太多時:

  • 讓 challenge 失效。
  • 要求重新發送。
  • 增加冷卻時間。
  • 高風險時要求更強驗證。

3. 重寄時要不要產生新 code?

看設計。

選項一:重寄同一個 code。

優點:

  • 使用者收到多封信或多封簡訊時,不容易混亂。

缺點:

  • code 暴露時間可能變長。

選項二:每次重寄產生新 code,舊 code 失效。

優點:

  • 安全邊界更清楚。

缺點:

  • 使用者可能輸入上一封簡訊的 code。

不管哪種,都要在 UI 文案上講清楚,例如「請使用最新一封簡訊中的驗證碼」。


六、 防帳號枚舉

1. 發送驗證信也可能洩漏帳號存在

忘記密碼或重寄驗證信常見錯誤:

text
此 email 尚未註冊

這會讓攻擊者枚舉帳號。

更安全的回應:

text
如果這個信箱已註冊,我們會寄出後續指示。

OWASP Forgot Password Cheat Sheet 也建議忘記密碼流程給出一致訊息與一致反應時間,避免揭露帳號是否存在。

2. 內外訊息要分開

外部:

json
{
  "message": "如果資料正確,我們會寄出驗證訊息。"
}

內部 log:

text
verification_email_requested
target_exists=true
rate_limited=false

這樣可以兼顧安全與營運除錯。

3. 註冊流程是否要提示 email 已存在?

這是產品取捨。

如果直接說:

text
這個 email 已經註冊

使用者體驗好,但有帳號枚舉風險。

替代方式:

text
如果這個 email 已經有帳號,我們會寄出登入或重設密碼的指示。

對高風險產品,應該更保守;對一般產品,可能會接受一定程度的可用性取捨。


七、 帳號狀態設計

1. Email verified 不一定等於 active

可以分清楚:

typescript
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 可用。
  • 高風險或濫用成本高。

策略二:可以登入,但限制功能。

適合:

  • 想降低註冊摩擦。
  • 產品需要先讓使用者體驗。

例如:

text
未驗證 email:可以瀏覽,但不能發文、不能付款、不能修改敏感資料。

這不是純技術問題,是風險與轉換率取捨。

3. 更換 email / phone 要重新驗證

使用者修改 email 或 phone 時,不能只更新欄位。

應該:

text
要求目前登入狀態
-> 高風險時要求重新驗證密碼或 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:

typescript
type: 'email_verification'

不能拿 email verification token 去 reset password。

2. Password reset 要更嚴格

忘記密碼連結通常等同於:

text
拿到這個 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。

簡化程式:

typescript
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}`,
  })
}

驗證:

typescript
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 流程

text
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、憑證或實名流程。

延伸閱讀