跳至主要內容
Skip to content

會員註冊流程設計

會員註冊看起來很直覺:

text
填 email
填密碼
按註冊
建立帳號

但真正在產品裡,註冊流程常常是身份系統最容易長出複雜度的地方。

因為註冊不只是新增一筆 users

它還牽涉:

  • Email 是否真的由使用者控制。
  • 手機號碼是否要驗證。
  • 密碼如何儲存。
  • 已存在帳號怎麼處理。
  • 第三方登入如何合併。
  • 是否允許未驗證帳號先使用。
  • 邀請制和組織成員如何建立。
  • 註冊濫用、垃圾帳號、機器人怎麼擋。
  • 使用者中途放棄註冊怎麼處理。
  • 註冊後要不要自動登入。
  • 需要保存哪些 audit log。

所以這篇要把「會員註冊」當成完整流程來設計,而不是一支 POST /register

一、 一句話回答

如果面試官問:

會員註冊流程怎麼設計?

可以先回答:

我會把註冊拆成帳號建立、聯絡方式驗證、憑證綁定、初始 session、風控與稽核幾個部分。使用者輸入 email / phone / password 後,後端先做格式檢查、rate limit、風險判斷與密碼 hash,建立 pendingactive 帳號,再發送短效、單次使用、資料庫存 hash 的 verification token。Email 或手機驗證通過後才把對應欄位標記為 verified。若支援第三方登入,應以 provider subject 作為外部身份唯一鍵,不要只靠 email 自動合併帳號;若 email 衝突,應要求使用者登入既有帳號後再綁定。整個流程要避免帳號枚舉、重複註冊、token 暴力猜測,並記錄註冊、驗證、重寄、綁定與失敗事件。

短一點可以說:

text
註冊不是建立 user,
而是建立一個可驗證、可恢復、可稽核的身份生命週期起點。

二、 先決定註冊策略

不同產品的註冊流程會不一樣。

先問幾個問題:

問題影響
使用 email、手機,還是第三方登入作為主要入口?影響唯一鍵與驗證流程
是否一定要驗證 email 才能使用?影響帳號狀態
是否支援匿名試用後升級?需要 guest-to-user migration
是否有組織 / tenant / invitation?需要 invite token 與成員狀態
是否支援 Google / LINE / GitHub 登入?需要 external identity 與 account linking
是否有金融、醫療、政府高信任需求?可能需要手機、憑證或身份識別
是否允許同一 email 多帳號?影響唯一性和 UX
是否需要防垃圾註冊?需要 rate limit、captcha、風控

一般會員系統可以先用:

text
email + password + email verification

如果產品以手機為核心,可以用:

text
phone + OTP + optional password

如果是企業 SaaS,常見是:

text
invite link + email verification + org membership

如果是消費型產品,可能還會加:

text
Google / LINE / Apple 登入

三、 帳號狀態機

註冊流程不應該只有:

text
created = true

比較好的做法是設計帳號狀態。

常見狀態:

狀態意義
pending_verification已建立帳號,但 email / phone 未驗證
active可正常登入與使用
suspended因風險、濫用或安全事件暫停
locked因安全原因暫時鎖定
deleted使用者刪除或資料保留期後刪除
invited被邀請但尚未完成註冊

好處是:

  • 可以允許使用者先建立帳號但限制功能。
  • 可以清理長期未驗證帳號。
  • 可以支援邀請制。
  • 可以支援風控暫停。
  • 不會把所有狀態塞成一堆 boolean。

四、 Email 註冊流程

典型流程:

幾個重要決策:

1. 註冊後是否自動登入

可以有兩種設計。

設計 A:註冊後不自動登入

text
註冊成功 -> 驗證 email -> 使用者重新登入

優點:

  • 流程安全清楚。
  • session 邏輯簡單。
  • 適合高風險產品。

缺點:

  • 體驗多一步。

設計 B:註冊後建立受限 session

text
註冊成功 -> 建立 limited session -> 只能做驗證信箱、補資料等低風險操作

優點:

  • 體驗順。
  • 適合 SaaS onboarding。

缺點:

  • 權限模型要清楚。
  • API 必須檢查 email_verified_at 或 account status。

不建議:

text
未驗證 email 就給完整權限。

尤其不能讓未驗證帳號:

  • 邀請他人。
  • 發送大量訊息。
  • 存取敏感資料。
  • 建立付款或交易。
  • 使用需要可信聯絡方式的功能。

2. Verification token 怎麼設計

Email verification token 應該:

  • 高隨機。
  • 短效。
  • 單次使用。
  • 存 hash。
  • 綁定 user。
  • 綁定 purpose。
  • 可撤銷。
  • 重寄時可讓舊 token 失效。

資料表:

sql
create table verification_tokens (
  id uuid primary key,
  user_id uuid not null references users(id),
  token_hash text not null unique,
  purpose text not null,
  target text not null,
  expires_at timestamp not null,
  used_at timestamp,
  created_at timestamp not null
);

不要把 token 明文存 DB。

寄出去的是:

text
raw token

DB 存的是:

text
hash(raw token)

驗證時:

text
hash(input token) -> 查 DB -> 檢查 purpose / expiry / used_at

3. 驗證信連結安全

驗證連結要注意:

  • 使用 HTTPS。
  • 網域不要直接相信 Host header。
  • token 不要放到第三方 redirect。
  • 驗證頁設定合適的 referrer policy。
  • 驗證失敗不要透露太多細節。
  • 重寄驗證信要 rate limit。

OWASP Forgot Password Cheat Sheet 對 reset token 的建議也可以套用到驗證 token:token 要隨機、足夠長、妥善保存、單次使用並設定過期時間。

五、 Email 當識別符的陷阱

Email 很常被拿來當帳號唯一鍵,但它有很多坑。

OWASP Email Validation and Verification Cheat Sheet 也提醒:Email 在身份系統中若處理不當,可能導致帳號接管、使用者枚舉和身份混淆。

常見問題:

問題例子
大小寫Alice@example.com vs alice@example.com
Unicode / IDN視覺相似但不同字元
Gmail dot aliasa.b@gmail.com vs ab@gmail.com
plus aliasalice+test@example.com
provider 行為不同不是每個信箱都像 Gmail
email 可被回收使用者失去信箱控制權

建議:

  • 保存原始輸入 email 作為顯示。
  • 另存 normalized email 作為查詢與唯一性判斷。
  • normalization 規則要明確且一致。
  • 不要過度套用特定 provider 規則到所有網域。
  • email 變更要走安全流程。
  • 重要操作不要只依賴 email。

資料欄位可以是:

sql
email_original text,
email_normalized text unique,
email_verified_at timestamp

注意:

text
email verified 只代表使用者當時控制該信箱,
不代表真實身份被驗證。

六、 手機註冊流程

手機註冊通常是:

text
輸入 phone
-> 發 OTP
-> 使用者輸入 OTP
-> 建立或啟用帳號

流程:

OTP 設計重點:

  • 短效,例如 5 到 10 分鐘。
  • 重試限制。
  • 重寄限制。
  • 驗證成功後失效。
  • 不要明文存 OTP。
  • 不要在 log 裡記 OTP。
  • 防止同一手機被大量轟炸。
  • 防止同一 IP 大量打不同手機。

手機驗證的風險:

  • SIM swap。
  • 簡訊攔截。
  • 電信轉接。
  • 使用者換號。
  • 共享手機。
  • 虛擬號碼。

所以手機驗證方便,但不應該被誤認為高強度身份驗證。

七、 第三方登入註冊

第三方登入通常會變成「註冊 + 登入 + 綁定」混合流程。

使用 Google Login 舉例:

text
使用者點 Google Login
-> 完成 OIDC Authorization Code Flow
-> 後端驗證 id token
-> 取得 provider subject
-> 查 external_identities
-> 找到則登入
-> 找不到則建立新 user 或要求綁定既有帳號

資料表:

sql
create table external_identities (
  id uuid primary key,
  user_id uuid not null references users(id),
  provider text not null,
  provider_subject text not null,
  email text,
  email_verified boolean,
  linked_at timestamp not null,
  unique (provider, provider_subject)
);

核心原則:

text
第三方身份唯一鍵是 provider + subject,
不是 email。

危險做法:

text
Google 回傳 email = alice@example.com
本地也有 alice@example.com
直接合併

比較好的流程:

text
如果 provider subject 已存在 -> 登入
如果 email 不存在 -> 建立新帳號並綁定
如果 email 已存在 -> 要求使用者登入既有帳號後再綁定

如果 provider 明確提供 email_verified=true,可以把它當成信箱已由 provider 驗證過,但仍要小心 account linking 的安全語意。

八、 帳號合併與衝突

註冊最難的地方之一是:

text
同一個人可能用不同方式進來。

例如:

  • 先用 email/password 註冊。
  • 後來用 Google 登入。
  • 再用 LINE 登入。
  • email 一樣但 provider 不同。

好的帳號合併流程:

text
使用者用 Google 登入
-> 系統發現 email 已存在本地帳號
-> 不直接合併
-> 顯示:此 email 已有帳號,請先登入原帳號以完成綁定
-> 使用者通過原帳號驗證
-> 建立 external identity link
-> 記錄 audit log
-> 發安全通知

解除綁定也要檢查:

text
解除後是否還有至少一種可用登入方式?

不要讓使用者解除最後一個登入方式,導致帳號無法登入。

九、 邀請制註冊

企業 SaaS 常見:

text
管理員邀請成員
-> 成員收到 invite link
-> 成員註冊或登入
-> 加入 organization

Invite token 設計:

  • 高隨機。
  • 短效。
  • 單次使用。
  • 綁定 email 或 role。
  • 可撤銷。
  • 記錄 inviter。
  • 記錄 accepted_at。

資料表:

sql
create table organization_invites (
  id uuid primary key,
  organization_id uuid not null,
  email_normalized text not null,
  role text not null,
  token_hash text not null unique,
  invited_by uuid not null,
  expires_at timestamp not null,
  accepted_at timestamp,
  revoked_at timestamp,
  created_at timestamp not null
);

接受邀請時要處理:

情境處理
email 尚未有帳號建立帳號並加入 org
email 已有帳號登入後加入 org
使用第三方登入檢查 provider email 是否匹配 invite email
invite 過期要求重新邀請
invite 被撤銷拒絕

不要只要拿到 invite token 就直接加入組織,尤其是高權限 role。

十、 防止帳號枚舉

註冊流程很容易洩漏:

text
這個 email 是否已經註冊。

例如:

text
此 email 已註冊。

這對一般產品可能可接受,但對金融、醫療、成人內容、政府服務等敏感產品可能是隱私風險。

可以依風險選擇。

一般產品

可以顯示:

text
此 email 已註冊,請登入或重設密碼。

體驗較好。

高隱私產品

可以顯示一致訊息:

text
如果此信箱可以註冊或已存在,我們會寄送後續指示。

並透過信件告知使用者下一步。

類似 OWASP Forgot Password Cheat Sheet 對忘記密碼的建議:對存在和不存在的帳號回傳一致訊息,避免帳號枚舉。

十一、 防濫用與風控

註冊會被拿來做:

  • 垃圾帳號。
  • 撞庫前置。
  • 大量佔用 email / phone。
  • 簡訊轟炸。
  • 試探帳號是否存在。
  • 邀請濫用。
  • 行銷優惠濫用。

防護手段:

手段用途
IP rate limit防同來源大量註冊
email / phone rate limit防轟炸
device fingerprint輔助識別濫用
CAPTCHA提高機器人成本
disposable email detection依產品需求判斷
risk score決定是否加驗證
invite-only降低公開濫用
email domain allowlist企業產品常見

注意不要過度阻擋。

例如 disposable email 不一定全都惡意,有些使用者是為了隱私。

資深設計應該把它當成風險訊號,不一定直接拒絕。

十二、 註冊後 onboarding

註冊完成後通常還有 onboarding。

例如:

  • 補姓名。
  • 補手機。
  • 選擇方案。
  • 建立 organization。
  • 邀請團隊成員。
  • 啟用 MFA。
  • 設定安全問題,不推薦作為主要安全手段。
  • 同意服務條款。

這些不一定都要在註冊第一步完成。

比較好的做法是:

text
註冊只收必要資料
後續依使用場景漸進式補資料

這叫 progressive profiling。

好處:

  • 降低註冊摩擦。
  • 減少不必要個資收集。
  • 讓使用者先看到產品價值。
  • 高風險功能前再要求補驗證。

十三、 資料模型

比較完整的註冊相關資料表:

sql
create table users (
  id uuid primary key,
  email_original text,
  email_normalized text unique,
  phone_e164 text unique,
  password_hash text,
  status text not null,
  email_verified_at timestamp,
  phone_verified_at timestamp,
  created_at timestamp not null,
  updated_at timestamp not null
);

create table verification_tokens (
  id uuid primary key,
  user_id uuid not null references users(id),
  purpose text not null,
  target text not null,
  token_hash text not null unique,
  expires_at timestamp not null,
  used_at timestamp,
  created_at timestamp not null
);

create table external_identities (
  id uuid primary key,
  user_id uuid not null references users(id),
  provider text not null,
  provider_subject text not null,
  email text,
  email_verified boolean,
  linked_at timestamp not null,
  unique (provider, provider_subject)
);

create table registration_events (
  id uuid primary key,
  user_id uuid,
  event_type text not null,
  result text not null,
  ip inet,
  user_agent text,
  risk_score integer,
  metadata jsonb,
  created_at timestamp not null
);

如果有邀請制,再加:

sql
create table organization_invites (
  id uuid primary key,
  organization_id uuid not null,
  email_normalized text not null,
  role text not null,
  token_hash text not null unique,
  invited_by uuid not null,
  expires_at timestamp not null,
  accepted_at timestamp,
  revoked_at timestamp,
  created_at timestamp not null
);

十四、 API 設計

常見 endpoint:

text
POST /auth/register
POST /auth/email/verify
POST /auth/email/resend-verification
POST /auth/phone/start
POST /auth/phone/verify
POST /auth/oauth/:provider/start
GET  /auth/oauth/:provider/callback
POST /auth/external-identities/link
DELETE /auth/external-identities/:id
POST /auth/invites/:token/accept
GET  /auth/me

註冊 API 概念:

ts
async function register(req: Request) {
  await rateLimit.consume({
    ip: req.ip,
    key: normalizeEmail(req.body.email),
    action: 'register'
  })

  const email = normalizeEmail(req.body.email)
  const passwordHash = await passwordHasher.hash(req.body.password)

  const user = await users.createPending({
    emailOriginal: req.body.email,
    emailNormalized: email,
    passwordHash
  })

  const token = await verificationTokens.create({
    userId: user.id,
    purpose: 'email_verification',
    target: email,
    ttlMinutes: 30
  })

  await mailer.sendVerificationEmail(user.emailOriginal, token.raw)
  await auditLog.record(user.id, 'registered')

  return { status: 'verification_required' }
}

驗證 API 概念:

ts
async function verifyEmail(req: Request) {
  const token = await verificationTokens.consume({
    rawToken: req.body.token,
    purpose: 'email_verification'
  })

  if (!token) {
    throw new InvalidTokenError()
  }

  await users.markEmailVerified(token.userId)
  await auditLog.record(token.userId, 'email_verified')

  return { status: 'verified' }
}

重點:

text
consume token 要是原子操作,
避免同一 token 被並發使用兩次。

十五、 註冊與登入的交界

註冊完成後要不要直接登入?

可以依產品選。

低風險產品

text
email verified -> 建立 session -> 直接進入產品

體驗好。

高風險產品

text
email verified -> 要求使用者重新登入

流程保守。

有 MFA 的產品

text
email verified -> 建立 session -> 引導設定 MFA -> 完成後給完整功能

重點不是哪個一定對,而是:

text
session 的 assurance level 要和帳號狀態一致。

未驗證、剛註冊、剛綁定第三方登入,都可以是低權限狀態。

十六、 Email 變更流程

Email 變更其實是註冊流程的延伸。

危險做法:

text
使用者輸入新 email
-> 直接把 users.email 改掉

更好的流程:

text
使用者已登入
-> 重新驗證
-> 輸入新 email
-> 寄驗證信到新 email
-> 新 email 驗證成功
-> 更新 email
-> 通知舊 email
-> 記錄 audit log

為什麼要通知舊 email?

因為如果攻擊者拿到 session,他可能嘗試把帳號 email 改到自己的信箱。

舊 email 通知可以讓原使用者發現異常。

十七、 常見錯誤

1. 註冊後直接給完整權限

錯誤:

text
email 未驗證也能使用所有功能。

正確:

text
未驗證帳號只能使用低風險功能,敏感功能要驗證後才能用。

2. Verification token 明文存 DB

錯誤:

text
token = abc123 明文存資料庫。

正確:

text
寄 raw token,DB 存 hash。

3. Token 可重複使用

錯誤:

text
驗證連結點幾次都有效。

正確:

text
token 單次使用,使用後標記 used_at。

4. 只靠 email 合併第三方帳號

錯誤:

text
Google email 相同就合併本地帳號。

正確:

text
以 provider subject 識別第三方身份,email 衝突時要求原帳號重新驗證後再綁定。

5. 註冊錯誤訊息洩漏太多

錯誤:

text
此 email 已註冊且已啟用 Google 登入。

正確:

text
依產品風險決定是否顯示一致訊息,敏感產品應避免帳號枚舉。

6. 沒有限制重寄驗證信

錯誤:

text
任何人可以一直對同一 email 重寄驗證信。

正確:

text
依 IP、email、user 做 rate limit。

7. 忽略未完成註冊帳號清理

錯誤:

text
pending 帳號永久存在。

正確:

text
長期未驗證帳號應有清理或重新驗證策略。

十八、 面試追問題庫

Q1:註冊流程要做哪些事?

建立帳號、驗證 email 或手機、儲存密碼 hash、建立帳號狀態、發送 verification token、限制濫用、避免帳號枚舉、記錄 audit log,並決定註冊後是否建立 session。

Q2:Email verification token 怎麼設計?

高隨機、短效、單次使用、DB 存 hash、綁定 user 和 purpose,使用後失效,重寄要 rate limit,驗證失敗不要透露過多資訊。

Q3:註冊後要不要自動登入?

依產品風險決定。低風險產品可以 email verified 後直接建立 session;高風險產品可以要求重新登入;也可以建立 limited session,只允許完成驗證與 onboarding。

Q4:第三方登入和本地帳號 email 相同時怎麼處理?

不要只靠 email 自動合併。應以 provider + subject 作為第三方身份唯一鍵;若 email 已存在,要求使用者登入既有帳號並重新驗證後再綁定。

Q5:如何避免帳號枚舉?

在高隱私場景,註冊、忘記密碼、重寄驗證信都回傳一致訊息,並讓真正擁有該 email 的使用者透過信件取得下一步。同時控制回應時間與 rate limit。

Q6:手機註冊有什麼風險?

SMS OTP 可能遇到 SIM swap、攔截、虛擬號碼、共享手機和號碼回收,所以適合聯絡方式驗證或一般登入輔助,不應直接視為高強度身份驗證。

Q7:邀請制註冊要注意什麼?

Invite token 要短效、單次、可撤銷,綁定 email、organization、role 和 inviter。接受邀請時要檢查 token 狀態、帳號身份與 email 是否匹配。

十九、 實作題

題目一:設計 Email 註冊流程

好的答案:

  1. 使用者輸入 email/password。
  2. 後端做 input validation、rate limit、風險檢查。
  3. normalize email。
  4. 檢查是否已存在。
  5. 密碼用 Argon2id / bcrypt hash。
  6. 建立 pending_verification user。
  7. 建立短效單次 verification token,DB 存 hash。
  8. 寄送驗證信。
  9. 使用者點擊連結。
  10. 後端 consume token。
  11. 標記 email verified。
  12. 啟用帳號或建立 session。
  13. 記錄 audit log。

題目二:設計第三方登入首次註冊

好的答案:

  • 使用 Authorization Code Flow。
  • 後端驗證 id token。
  • 取 provider subject。
  • provider + subject 查 external identity。
  • 不存在則看 email 是否已有本地帳號。
  • email 不存在則建立 user 並綁定。
  • email 已存在則要求登入既有帳號再綁定。
  • 記錄 audit log。
  • 發安全通知。

題目三:設計重寄驗證信

好的答案:

  • 不透露帳號是否存在。
  • 對 IP、email、user 做 rate limit。
  • 產生新 token。
  • 舊 token 可失效。
  • token 短效且存 hash。
  • 寄送驗證信。
  • 記錄事件。

題目四:設計註冊狀態機

至少包含:

text
invited
pending_verification
active
suspended
locked
deleted

並說明每個狀態能不能登入、能使用哪些功能、如何轉換。

二十、 資深視角

1. 註冊是身份生命週期的入口

初階答案通常只講:

text
insert user。

資深答案會講:

text
user 如何被建立、驗證、啟用、限制、合併、恢復、稽核。

這就是差距。

2. Email 是聯絡方式,不是真實身份

Email 很方便,但它不是自然人身份。

它只能證明:

text
某個時間點,使用者控制了這個信箱。

所以金融、醫療、政府、高價值帳號不要只依賴 email。

3. Account linking 是接管高風險區

帳號合併和第三方登入綁定如果做錯,很容易造成 account takeover。

尤其是:

text
用 email 自動合併

這是非常常見的坑。

4. 註冊流程要和風控合作

註冊是攻擊者大量建立帳號的入口。

好的設計會把註冊事件餵給風控:

  • IP。
  • device。
  • email domain。
  • phone country。
  • 行為速度。
  • 是否命中濫用規則。

但不要一刀切阻擋所有可疑訊號,而是根據風險提高驗證或限制功能。


總結

面向設計重點
帳號狀態pending、active、suspended、deleted
Email 驗證token 短效、單次、存 hash、rate limit
手機驗證OTP 短效、限制重試、注意 SIM swap
密碼慢 hash、salt、必要時 pepper
第三方登入以 provider subject 為唯一鍵
帳號合併email 衝突要重新驗證後綁定
邀請制invite token 短效、單次、綁定 email / role
防枚舉敏感產品回傳一致訊息
防濫用IP / email / phone rate limit、風險評分
稽核註冊、驗證、重寄、綁定、失敗都記錄

一句完整的面試回答可以是:

會員註冊流程不只是建立 user,而是身份生命週期的入口。我會先決定註冊入口是 email、手機、第三方登入或邀請制;帳號建立時先做輸入驗證、rate limit、風險檢查和密碼 hash,建立 pending_verification 狀態,再發短效、單次使用、DB 存 hash 的 verification token。Email 或手機驗證通過後才標記 verified 並啟用完整功能。若支援第三方登入,應用 provider + subject 作為外部身份唯一鍵,不靠 email 直接合併;email 衝突時要求使用者登入既有帳號後再綁定。整個流程要避免帳號枚舉、重寄濫用、OTP 暴力猜測和垃圾註冊,並記錄註冊、驗證、重寄、綁定與失敗事件。

延伸閱讀