會員註冊流程設計
會員註冊看起來很直覺:
填 email
填密碼
按註冊
建立帳號但真正在產品裡,註冊流程常常是身份系統最容易長出複雜度的地方。
因為註冊不只是新增一筆 users。
它還牽涉:
- Email 是否真的由使用者控制。
- 手機號碼是否要驗證。
- 密碼如何儲存。
- 已存在帳號怎麼處理。
- 第三方登入如何合併。
- 是否允許未驗證帳號先使用。
- 邀請制和組織成員如何建立。
- 註冊濫用、垃圾帳號、機器人怎麼擋。
- 使用者中途放棄註冊怎麼處理。
- 註冊後要不要自動登入。
- 需要保存哪些 audit log。
所以這篇要把「會員註冊」當成完整流程來設計,而不是一支 POST /register。
一、 一句話回答
如果面試官問:
會員註冊流程怎麼設計?
可以先回答:
我會把註冊拆成帳號建立、聯絡方式驗證、憑證綁定、初始 session、風控與稽核幾個部分。使用者輸入 email / phone / password 後,後端先做格式檢查、rate limit、風險判斷與密碼 hash,建立
pending或active帳號,再發送短效、單次使用、資料庫存 hash 的 verification token。Email 或手機驗證通過後才把對應欄位標記為 verified。若支援第三方登入,應以 provider subject 作為外部身份唯一鍵,不要只靠 email 自動合併帳號;若 email 衝突,應要求使用者登入既有帳號後再綁定。整個流程要避免帳號枚舉、重複註冊、token 暴力猜測,並記錄註冊、驗證、重寄、綁定與失敗事件。
短一點可以說:
註冊不是建立 user,
而是建立一個可驗證、可恢復、可稽核的身份生命週期起點。二、 先決定註冊策略
不同產品的註冊流程會不一樣。
先問幾個問題:
| 問題 | 影響 |
|---|---|
| 使用 email、手機,還是第三方登入作為主要入口? | 影響唯一鍵與驗證流程 |
| 是否一定要驗證 email 才能使用? | 影響帳號狀態 |
| 是否支援匿名試用後升級? | 需要 guest-to-user migration |
| 是否有組織 / tenant / invitation? | 需要 invite token 與成員狀態 |
| 是否支援 Google / LINE / GitHub 登入? | 需要 external identity 與 account linking |
| 是否有金融、醫療、政府高信任需求? | 可能需要手機、憑證或身份識別 |
| 是否允許同一 email 多帳號? | 影響唯一性和 UX |
| 是否需要防垃圾註冊? | 需要 rate limit、captcha、風控 |
一般會員系統可以先用:
email + password + email verification如果產品以手機為核心,可以用:
phone + OTP + optional password如果是企業 SaaS,常見是:
invite link + email verification + org membership如果是消費型產品,可能還會加:
Google / LINE / Apple 登入三、 帳號狀態機
註冊流程不應該只有:
created = true比較好的做法是設計帳號狀態。
常見狀態:
| 狀態 | 意義 |
|---|---|
pending_verification | 已建立帳號,但 email / phone 未驗證 |
active | 可正常登入與使用 |
suspended | 因風險、濫用或安全事件暫停 |
locked | 因安全原因暫時鎖定 |
deleted | 使用者刪除或資料保留期後刪除 |
invited | 被邀請但尚未完成註冊 |
好處是:
- 可以允許使用者先建立帳號但限制功能。
- 可以清理長期未驗證帳號。
- 可以支援邀請制。
- 可以支援風控暫停。
- 不會把所有狀態塞成一堆 boolean。
四、 Email 註冊流程
典型流程:
幾個重要決策:
1. 註冊後是否自動登入
可以有兩種設計。
設計 A:註冊後不自動登入
註冊成功 -> 驗證 email -> 使用者重新登入優點:
- 流程安全清楚。
- session 邏輯簡單。
- 適合高風險產品。
缺點:
- 體驗多一步。
設計 B:註冊後建立受限 session
註冊成功 -> 建立 limited session -> 只能做驗證信箱、補資料等低風險操作優點:
- 體驗順。
- 適合 SaaS onboarding。
缺點:
- 權限模型要清楚。
- API 必須檢查
email_verified_at或 account status。
不建議:
未驗證 email 就給完整權限。尤其不能讓未驗證帳號:
- 邀請他人。
- 發送大量訊息。
- 存取敏感資料。
- 建立付款或交易。
- 使用需要可信聯絡方式的功能。
2. Verification token 怎麼設計
Email verification token 應該:
- 高隨機。
- 短效。
- 單次使用。
- 存 hash。
- 綁定 user。
- 綁定 purpose。
- 可撤銷。
- 重寄時可讓舊 token 失效。
資料表:
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。
寄出去的是:
raw tokenDB 存的是:
hash(raw token)驗證時:
hash(input token) -> 查 DB -> 檢查 purpose / expiry / used_at3. 驗證信連結安全
驗證連結要注意:
- 使用 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 alias | a.b@gmail.com vs ab@gmail.com |
| plus alias | alice+test@example.com |
| provider 行為不同 | 不是每個信箱都像 Gmail |
| email 可被回收 | 使用者失去信箱控制權 |
建議:
- 保存原始輸入 email 作為顯示。
- 另存 normalized email 作為查詢與唯一性判斷。
- normalization 規則要明確且一致。
- 不要過度套用特定 provider 規則到所有網域。
- email 變更要走安全流程。
- 重要操作不要只依賴 email。
資料欄位可以是:
email_original text,
email_normalized text unique,
email_verified_at timestamp注意:
email verified 只代表使用者當時控制該信箱,
不代表真實身份被驗證。六、 手機註冊流程
手機註冊通常是:
輸入 phone
-> 發 OTP
-> 使用者輸入 OTP
-> 建立或啟用帳號流程:
OTP 設計重點:
- 短效,例如 5 到 10 分鐘。
- 重試限制。
- 重寄限制。
- 驗證成功後失效。
- 不要明文存 OTP。
- 不要在 log 裡記 OTP。
- 防止同一手機被大量轟炸。
- 防止同一 IP 大量打不同手機。
手機驗證的風險:
- SIM swap。
- 簡訊攔截。
- 電信轉接。
- 使用者換號。
- 共享手機。
- 虛擬號碼。
所以手機驗證方便,但不應該被誤認為高強度身份驗證。
七、 第三方登入註冊
第三方登入通常會變成「註冊 + 登入 + 綁定」混合流程。
使用 Google Login 舉例:
使用者點 Google Login
-> 完成 OIDC Authorization Code Flow
-> 後端驗證 id token
-> 取得 provider subject
-> 查 external_identities
-> 找到則登入
-> 找不到則建立新 user 或要求綁定既有帳號資料表:
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)
);核心原則:
第三方身份唯一鍵是 provider + subject,
不是 email。危險做法:
Google 回傳 email = alice@example.com
本地也有 alice@example.com
直接合併比較好的流程:
如果 provider subject 已存在 -> 登入
如果 email 不存在 -> 建立新帳號並綁定
如果 email 已存在 -> 要求使用者登入既有帳號後再綁定如果 provider 明確提供 email_verified=true,可以把它當成信箱已由 provider 驗證過,但仍要小心 account linking 的安全語意。
八、 帳號合併與衝突
註冊最難的地方之一是:
同一個人可能用不同方式進來。例如:
- 先用 email/password 註冊。
- 後來用 Google 登入。
- 再用 LINE 登入。
- email 一樣但 provider 不同。
好的帳號合併流程:
使用者用 Google 登入
-> 系統發現 email 已存在本地帳號
-> 不直接合併
-> 顯示:此 email 已有帳號,請先登入原帳號以完成綁定
-> 使用者通過原帳號驗證
-> 建立 external identity link
-> 記錄 audit log
-> 發安全通知解除綁定也要檢查:
解除後是否還有至少一種可用登入方式?不要讓使用者解除最後一個登入方式,導致帳號無法登入。
九、 邀請制註冊
企業 SaaS 常見:
管理員邀請成員
-> 成員收到 invite link
-> 成員註冊或登入
-> 加入 organizationInvite token 設計:
- 高隨機。
- 短效。
- 單次使用。
- 綁定 email 或 role。
- 可撤銷。
- 記錄 inviter。
- 記錄 accepted_at。
資料表:
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。
十、 防止帳號枚舉
註冊流程很容易洩漏:
這個 email 是否已經註冊。例如:
此 email 已註冊。這對一般產品可能可接受,但對金融、醫療、成人內容、政府服務等敏感產品可能是隱私風險。
可以依風險選擇。
一般產品
可以顯示:
此 email 已註冊,請登入或重設密碼。體驗較好。
高隱私產品
可以顯示一致訊息:
如果此信箱可以註冊或已存在,我們會寄送後續指示。並透過信件告知使用者下一步。
類似 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。
- 設定安全問題,不推薦作為主要安全手段。
- 同意服務條款。
這些不一定都要在註冊第一步完成。
比較好的做法是:
註冊只收必要資料
後續依使用場景漸進式補資料這叫 progressive profiling。
好處:
- 降低註冊摩擦。
- 減少不必要個資收集。
- 讓使用者先看到產品價值。
- 高風險功能前再要求補驗證。
十三、 資料模型
比較完整的註冊相關資料表:
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
);如果有邀請制,再加:
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:
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 概念:
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 概念:
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' }
}重點:
consume token 要是原子操作,
避免同一 token 被並發使用兩次。十五、 註冊與登入的交界
註冊完成後要不要直接登入?
可以依產品選。
低風險產品
email verified -> 建立 session -> 直接進入產品體驗好。
高風險產品
email verified -> 要求使用者重新登入流程保守。
有 MFA 的產品
email verified -> 建立 session -> 引導設定 MFA -> 完成後給完整功能重點不是哪個一定對,而是:
session 的 assurance level 要和帳號狀態一致。未驗證、剛註冊、剛綁定第三方登入,都可以是低權限狀態。
十六、 Email 變更流程
Email 變更其實是註冊流程的延伸。
危險做法:
使用者輸入新 email
-> 直接把 users.email 改掉更好的流程:
使用者已登入
-> 重新驗證
-> 輸入新 email
-> 寄驗證信到新 email
-> 新 email 驗證成功
-> 更新 email
-> 通知舊 email
-> 記錄 audit log為什麼要通知舊 email?
因為如果攻擊者拿到 session,他可能嘗試把帳號 email 改到自己的信箱。
舊 email 通知可以讓原使用者發現異常。
十七、 常見錯誤
1. 註冊後直接給完整權限
錯誤:
email 未驗證也能使用所有功能。正確:
未驗證帳號只能使用低風險功能,敏感功能要驗證後才能用。2. Verification token 明文存 DB
錯誤:
token = abc123 明文存資料庫。正確:
寄 raw token,DB 存 hash。3. Token 可重複使用
錯誤:
驗證連結點幾次都有效。正確:
token 單次使用,使用後標記 used_at。4. 只靠 email 合併第三方帳號
錯誤:
Google email 相同就合併本地帳號。正確:
以 provider subject 識別第三方身份,email 衝突時要求原帳號重新驗證後再綁定。5. 註冊錯誤訊息洩漏太多
錯誤:
此 email 已註冊且已啟用 Google 登入。正確:
依產品風險決定是否顯示一致訊息,敏感產品應避免帳號枚舉。6. 沒有限制重寄驗證信
錯誤:
任何人可以一直對同一 email 重寄驗證信。正確:
依 IP、email、user 做 rate limit。7. 忽略未完成註冊帳號清理
錯誤:
pending 帳號永久存在。正確:
長期未驗證帳號應有清理或重新驗證策略。十八、 面試追問題庫
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 註冊流程
好的答案:
- 使用者輸入 email/password。
- 後端做 input validation、rate limit、風險檢查。
- normalize email。
- 檢查是否已存在。
- 密碼用 Argon2id / bcrypt hash。
- 建立
pending_verificationuser。 - 建立短效單次 verification token,DB 存 hash。
- 寄送驗證信。
- 使用者點擊連結。
- 後端 consume token。
- 標記 email verified。
- 啟用帳號或建立 session。
- 記錄 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。
- 寄送驗證信。
- 記錄事件。
題目四:設計註冊狀態機
至少包含:
invited
pending_verification
active
suspended
locked
deleted並說明每個狀態能不能登入、能使用哪些功能、如何轉換。
二十、 資深視角
1. 註冊是身份生命週期的入口
初階答案通常只講:
insert user。資深答案會講:
user 如何被建立、驗證、啟用、限制、合併、恢復、稽核。這就是差距。
2. Email 是聯絡方式,不是真實身份
Email 很方便,但它不是自然人身份。
它只能證明:
某個時間點,使用者控制了這個信箱。所以金融、醫療、政府、高價值帳號不要只依賴 email。
3. Account linking 是接管高風險區
帳號合併和第三方登入綁定如果做錯,很容易造成 account takeover。
尤其是:
用 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 暴力猜測和垃圾註冊,並記錄註冊、驗證、重寄、綁定與失敗事件。