題目與適用情境
為一個消費型 Web 應用程式設計自助電子郵件連結密碼重設流程。請求 API 接收電子郵件;若帳號 存在,系統就傳送一個包含不透明權杖的 HTTPS 連結。權杖從建立起 30 分鐘後到期。資料庫不得 儲存原始承載權杖。
設計必須防止未登入的呼叫者判斷某個電子郵件是否已註冊、對單一信箱反覆轟炸重設郵件、重放 已使用的連結、修改其他使用者的密碼,或在兩筆並行送出中繞過一次性限制。重設成功後,系統要 更新密碼、讓該帳號所有尚未使用的重設權杖失效、撤銷既有登入工作階段,並傳送安全通知。密碼 重設不能移除或取代已綁定的 MFA 驗證器。
這道題主要評估後端安全與一致性。完整回答要把威脅模型落實到 API 契約、權杖生命週期、資料庫 交易、郵件投遞、工作階段模型及正式環境證據。隨機權杖加到期時間只是起點;真正困難的是讓 所有可觀測路徑與並行路徑都遵守同一份安全契約。
面試官在評估什麼
第一個訊號是威脅建模。應徵者應識別帳號列舉、重設郵件轟炸、Host 標頭注入、URL 洩漏、權杖 猜測、資料庫洩漏、連結重放、用戶端竄改、並行消耗、遭竊工作階段及 MFA 繞過。每項威脅都要 對應具體控制,不能只籠統宣稱端點很安全。
第二個訊號是能否區分原始權杖與資料庫中的驗證值。郵件攜帶的是高熵承載秘密,資料庫只儲存 單向摘要,因此讀取權杖資料表不會直接取得可用連結。應徵者還應區分隨機權杖與人類密碼:使用者 選擇的密碼需要慢速密碼雜湊;均勻隨機的 256 位元權杖可以用快速密碼學摘要建立索引,不會因此 變得可猜。
第三個訊號是交易設計。應用程式先檢查 used_at、稍後才更新,會產生重放競爭。如果同一位 使用者有兩個不同的有效權杖,還要把帳號設成序列化點。高品質方案會鎖定使用者資料列,在一個 短交易內有條件地消耗目前權杖、修改密碼、撤銷同帳號其他權杖與工作階段,並寫入 outbox 事件。
第四個訊號是復原邊界判斷。忘記密碼不代表可以停用 MFA 或取代遺失的第二因素。更高保證等級 的帳號復原需要獨立的身分驗證流程。回答也應拒絕把安全問題當成唯一復原方式,不能透過電子郵件 傳送舊密碼或系統產生的新密碼。
最後還要看正式環境思維。如果時間差、限流行為、日誌、分析系統或客服工具仍會洩漏帳號是否 存在,統一文案就沒有真正解決列舉問題。郵件投遞故障不能提前改變帳號狀態,監控也必須在不 記錄承載權杖的前提下偵測濫用。
回答前要先確認的問題
- 這是取代密碼或完整帳號復原? 使用者仍能控制已驗證電子郵件、只是忘記密碼時,連結可
取代密碼。如果電子郵件、MFA 與復原碼都遺失,就需要更強的獨立復原流程。
- 帳號是否啟用 MFA? 密碼重設應保留已綁定因素。如果產品想用同一流程同時復原兩者,
身分保證與濫用審查要求會大幅改變。
- 哪些工作階段要撤銷? 本題要求撤銷全部工作階段。若希望發起重設的裝置保持登入,必須
證明該裝置原本可信,並把例外寫進契約。
- 可以同時存在多個連結嗎? 保留少量且有上限的連結,可以避免後寄出的郵件先抵達時讓合法
連結失效。無論哪個連結成功,都必須撤銷該使用者的其他所有連結。
- 帳號風險與投遞管道是什麼? 電子郵件可能適合低風險消費帳號,卻未必足以保護金融或受
監管存取。簡訊、復原碼、客服身分驗證與等待期有不同的接管及可用性風險。
- 登入已採用什麼密碼政策? 重設必須重用相同的長度、弱密碼封鎖清單與雜湊政策。另設一套
更弱的重設規則會形成驗證繞過。
- 濫用預算和郵件供應商限額是多少? 限流至少要考慮帳號、IP、裝置及供應商全域容量。只按
帳號硬鎖定,會讓攻擊者拒絕已知受害者的復原請求。
30 秒回答架構
「我會對所有電子郵件回傳相同的 202 狀態與文案,再在分層限流後非同步完成帳號查詢與投遞。 帳號存在時產生 32 個隨機位元組,把 base64url 權杖放入由固定 HTTPS 網域組成的郵件連結, 資料庫只儲存權杖摘要、使用者與 30 分鐘到期時間。GET 頁面不消耗權杖。最終 POST 會重新驗證 權杖並計算新密碼雜湊,接著鎖定使用者資料列,在同一交易內標記權杖已用、更新密碼、撤銷其他 重設權杖與全部工作階段,同時寫入通知與稽核 outbox。並行或重放請求會在條件更新時失敗。MFA 復原仍走獨立流程。」
分步深入解答
先定義兩個公開端點,並刻意保持回應契約簡單:
POST /password-reset-requests
{ "email": "person@example.com" }
202 Accepted
{ "message": "If an account matches, reset instructions will be sent." }
POST /password-resets
{ "token": "opaque-base64url-value", "new_password": "..." }無論帳號是否存在,請求端點都回傳相同狀態、文案結構與快取政策。它應先把工作交給有容量上限 的佇列再回傳,避免資料庫快速結束或同步 SMTP 呼叫形成明顯的時間旁路。固定休眠無法真正解決 問題:佇列飽和、只按帳號限流及不同例外路徑仍可能暴露行為,或被用來阻斷服務。
電子郵件只能依照產品已驗證的身分規則正規化。網域轉成小寫可能合理,但不能對所有網域套用 刪除句點或加號標籤等特定供應商規則,否則可能把兩個獨立帳號合併。限流應同時涵蓋正規化帳號 鍵、來源 IP 或網路、裝置或風險訊號,以及郵件供應商的全域預算。可疑流量可升級為驗證挑戰。 公開回應始終一致,而且只發起「忘記密碼」請求絕不能鎖定帳號或修改密碼。
對真實帳號,使用密碼學安全隨機數產生器產生 32 個位元組,也就是 256 位元;不帶填充的 base64url 編碼後為 43 個字元。這是本題的設計選擇,不是通用到期時間或編碼規定。資料庫儲存 SHA-256(raw_token),原始值只進入郵件連結。由於輸入均勻隨機且熵很高,快速單向摘要既能 建立索引,也不會讓權杖變得可猜;把摘要本身當成承載權杖送出時,伺服器會再次雜湊,因此無法 符合。人類選擇的密碼熵較低,必須另外使用慢速、加鹽的密碼雜湊。
以下是一份示意性的 PostgreSQL 資料表結構:
CREATE TABLE password_reset_tokens (
id uuid PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id),
token_digest bytea NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
used_at timestamptz,
revoked_at timestamptz
);
CREATE INDEX password_reset_tokens_active_user_idx
ON password_reset_tokens (user_id, expires_at)
WHERE used_at IS NULL AND revoked_at IS NULL;郵件 URL 必須使用已設定或允許清單內的來源,不能信任請求中的 Host 標頭。連結使用 HTTPS; 應用程式、Proxy、分析與錯誤日誌都要遮蔽權杖;重設頁不要載入第三方資源。設定 no-referrer 政策,防止導覽時洩漏查詢參數中的權杖。GET 可以顯示表單或提示連結無效,但不能消耗權杖, 因為郵件安全掃描器與連結預覽經常會在使用者之前開啟連結。
不能信任 GET 階段做過的驗證。遭竄改的用戶端可以直接呼叫最終端點,所以真正修改密碼的 POST 必須再次接收並驗證權杖。開始交易前,先計算權杖摘要、尋找未到期的候選紀錄、驗證新密碼並 計算慢速密碼雜湊。如此可讓昂貴雜湊與絕大多數無效請求留在帳號鎖外。此端點同樣需要限流。
最終狀態變更以使用者資料列作為序列化點:
candidate = find token by SHA-256(raw_token)
reject publicly if candidate is missing, expired, used, or revoked
new_hash = Argon2id(new_password, fresh_salt, tuned_parameters)
BEGIN
SELECT id FROM users WHERE id = candidate.user_id FOR UPDATE
UPDATE password_reset_tokens
SET used_at = now()
WHERE id = candidate.id
AND used_at IS NULL
AND revoked_at IS NULL
AND expires_at > now()
RETURNING user_id
if no row returned: ROLLBACK and reject
UPDATE users
SET password_hash = new_hash,
password_changed_at = now(),
auth_version = auth_version + 1
WHERE id = candidate.user_id
UPDATE password_reset_tokens
SET revoked_at = now()
WHERE user_id = candidate.user_id
AND id <> candidate.id
AND used_at IS NULL
AND revoked_at IS NULL
DELETE FROM sessions WHERE user_id = candidate.user_id
INSERT security_outbox(password_reset_succeeded, user_id, occurred_at)
COMMIT密碼雜湊在 BEGIN 前計算,但只有條件消耗成功才提交。對新系統,OWASP 目前列出的 Argon2id 最低設定之一是 19 MiB 記憶體、2 次迭代與 1 個平行度;應在不壓垮正常驗證容量的前提下壓測 並提高成本。雜湊值要攜帶演算法與參數,方便未來升級。Bcrypt 是帶有輸入長度限制的舊系統備援 方案,不能把它與 Argon2id 當成可任意互換的同義詞。
使用者資料列鎖可處理兩類競爭。兩筆請求送出同一權杖時,只有第一筆條件更新能寫入 used_at。 如果它們送出同一位使用者的兩個不同有效權杖,兩者可能都通過初始讀取並完成雜湊,但只有一筆 先持有使用者鎖。該交易會撤銷另一個權杖;第二筆請求取得鎖後,條件更新回傳零列。最終密碼只有 一個勝出值,所有失敗請求都會看到不可逆的終止狀態。
資料庫工作階段可透過伺服器端刪除立即失效;更新權杖則撤銷其伺服器端紀錄。對原本無狀態的 存取權杖,只有每次存取受保護資源都比較伺服器端 authversion 或 passwordchanged_at, 版本遞增才會立即生效。如果架構不做這項檢查,撤銷要等權杖自然到期。必須明確說明此限制, 不能把清除目前瀏覽器 Cookie 說成已撤銷攻擊者在其他裝置上的工作階段。
成功通知與安全稽核事件透過交易內 outbox 寫入,提交後才投遞。通知包含時間、帳號復原說明及 回報詐騙的入口,不包含舊密碼或新密碼。通知供應商故障應觸發重試與警示,不能回復已修改的 密碼。請求端也要讓權杖和郵件工作具備持久關聯,避免資料庫已提交、佇列卻失敗後悄悄遺失請求; outbox 或單一交易工作儲存可以封住這個邊界。
允許少量且有上限的未使用連結,不要每次請求都立即撤銷上一個。郵件可能延遲或順序顛倒;若 立即取代,攻擊者反覆發起請求就能讓受害者開啟的郵件持續到期。限制未完成紀錄數,達到上限時 撤銷或讓最舊紀錄到期;成功後再撤銷所有剩餘紀錄。終止與到期紀錄依稽核保存政策定期清理。
不透明伺服器端權杖通常比自包含 JWT 重設連結簡單。簽章 JWT 若要做到立即一次性使用、使用者 層級撤銷與密碼修改競爭,仍需要伺服器端狀態;既然狀態已存在,隨機權杖加摘要少了 claims 和 解析路徑。短 PIN 適合手動輸入,但熵遠低於 256 位元權杖,因此必須嚴格限制嘗試次數,並在 驗證成功後只建立權限受限的重設工作階段。
監控應涵蓋各風險維度的請求速率、佇列延遲、供應商錯誤、投遞結果、權杖驗證失敗、成功時權杖 年齡、重放嘗試與工作階段撤銷失敗。原始電子郵件、權杖、新密碼和完整重設 URL 不能進入指標 或日誌。警示既要辨識全域攻擊,也要辨識單一帳號郵件轟炸,同時不能讓公開回應變成帳號查詢 端點。
測試要把流程當成狀態機,不能只驗證 happy path。並行送出同一權杖,以及同一位使用者的兩個 不同權杖,都必須只有一筆成功。還要測試到期邊界、成功後重用、同帳號其他權杖撤銷、不存在的 帳號、佇列與郵件故障、Host 標頭竄改、Referer 洩漏、日誌遮蔽、限流維度、其他裝置工作階段 拒絕、通知重試及啟用 MFA 的帳號。安全掃描器 GET 連結時不應消耗權杖。
高品質示範回答
「我會把重設權杖視為暫時驗證器,並圍繞完整生命週期設計。請求端點一律回傳相同的 202 回應, 在帳號、網路、裝置與供應商維度做濫用控制後,非同步完成查詢與投遞。只輸入一個電子郵件, 不會鎖定或改變帳號。
帳號存在時,我會產生 32 個隨機位元組,把 base64url 值放進 HTTPS 連結,只儲存 SHA-256 摘要、 使用者 ID 與 30 分鐘到期時間。重設網域來自設定而非 Host,頁面不載入第三方資源,使用 no-referrer 政策,所有日誌管線都遮蔽權杖。GET 只顯示表單,因為郵件掃描器可能提早開啟連結。
最終 POST 會再次驗證權杖,在短交易前計算新密碼雜湊。交易內先鎖定使用者資料列,再有條件地 標記權杖已使用,更新密碼與驗證版本,撤銷同帳號其他重設權杖及伺服器端工作階段,並寫入 outbox。同權杖重放會敗在條件更新;使用者鎖加同帳號權杖撤銷,讓兩個不同權杖也只能有一個 勝出。
提交後,背景工作獨立傳送安全通知並重試。我會測試統一回應與時間差、權杖和 URL 洩漏、並行 消耗、到期、供應商故障、遠端工作階段拒絕及 MFA 帳號。密碼重設會保留 MFA;遺失所有驗證器 時,改走更高保證等級的獨立帳號復原流程。」
常見錯誤
- 回傳「電子郵件不存在」 → 攻擊者可列舉已註冊帳號 → **統一公開狀態與文案,並避免快速
結束形成時間旁路。**
- 儲存原始權杖 → 資料庫讀取者直接取得有效重設連結 → **只儲存單向摘要,除投遞與使用者
送出外全面遮蔽原始值。**
- 使用最終表單中的使用者 ID → 用戶端可替換目標帳號 → **使用者只能從伺服器端權杖紀錄
推導。**
- 在 GET 時消耗連結 → 郵件掃描器可能在使用者抵達前讓連結失效 → **只在修改密碼的 POST
中消耗。**
- 只在 GET 驗證、不在 POST 驗證 → 偽造用戶端可繞過畫面上的表單 → **在最終伺服器端交易
中重新驗證到期與終止狀態。**
- 先檢查、再無條件更新 → 並行請求可能同時通過檢查 → **在交易內做條件更新,並以使用者
資料列序列化不同權杖。**
- 每次請求都讓舊連結失效 → 攻擊者可持續讓受害者的最新郵件到期 → **保留有上限的集合,
只在一個成功後撤銷其他連結。**
- 用
Host組成連結 → 受污染的請求會傳送攻擊者控制的重設網域 → **使用已設定或允許清單
內的 HTTPS 來源。**
- 把權杖寫進日誌或分析系統 → 可觀測性平台變成憑證庫 → **遮蔽查詢參數,並禁止重設頁
載入第三方資源。**
- 重設後自動登入卻沒有工作階段政策 → 工作階段固定與遭竊工作階段行為不明 → **要求正常
登入,並明確撤銷伺服器端工作階段。**
- 連同密碼一起重設 MFA → 控制一個信箱就能擊穿更強因素 → MFA 復原使用獨立風險流程。
- 透過電子郵件傳送新密碼 → 密碼長期留在不安全管道,攻擊者還可鎖住受害者 → **傳送短期
連結,在有效證明送出前不改變帳號。**
追問與回答
追問 1:如果產品使用無狀態 JWT 存取權杖,如何撤銷工作階段?
先在伺服器端撤銷更新權杖。若要立即讓存取權杖失效,可在權杖中攜帶驗證版本或簽發時間,並讓 每次存取受保護資源都與伺服器端目前狀態比較。如果架構拒絕這次查詢或撤銷快取,舊存取權杖會 持續有效到到期;應清楚說明最長暴露時間,不能承諾立即登出。
追問 2:新請求是否應讓所有舊重設連結立即失效?
通常不應在成功前全部撤銷。郵件可能延遲或順序顛倒,知道電子郵件的攻擊者也可能不斷讓受害者 的最新連結失效。保留少量且有上限的未到期權杖,控制請求量,並在成功修改密碼的交易中撤銷 所有同帳號權杖。高風險產品可選擇嚴格取代,但必須接受相應的可用性代價。
追問 3:如果使用六位數驗證碼而不是 URL 權杖,怎麼改?
六位數驗證碼的熵遠低於 256 位元權杖。要把它綁定到特定帳號與權限受限的重設嘗試,伺服器端 限制嘗試次數並設定到期時間;驗證成功後只建立短期重設工作階段。該工作階段不能變成一般登入 工作階段,也不能允許用戶端選擇使用者 ID。
追問 4:新密碼應採用哪些規則?
重用一般登入的驗證器政策。如果產品採納目前 NIST 指引,單因素密碼最少 15 個字元;只在 MFA 中使用的密碼最少可為 8 個字元。至少允許 64 個字元,以封鎖清單拒絕常見或已洩漏值,不增加 任意字元組合或定期輪替規則。密碼雜湊成本與輸入規則應分開調整。
追問 5:如果使用者同時失去電子郵件與 MFA,怎麼辦?
這屬於完整帳號復原,不是本密碼重設端點。依帳號風險使用預先綁定的復原碼或聯絡人、仍可用 的驗證器、重新身分驗證、等待期及人工審查。復原地址本身的變更需要驗證與獨立通知。不能退回 到容易從公開資料猜出的安全問題,並把它當成唯一證明。
追問 6:如何證明帳號列舉真的受到控制?
對存在與不存在的帳號比較狀態碼、回應內容、回應標頭、快取行為、延遲分布、限流切換及下游副作用。 測試要涵蓋佇列飽和與供應商故障,不能只看安靜的本機環境。檢查 CDN、Proxy、應用程式、分析 與客服日誌是否出現識別資訊和完整 URL,並確認濫用儀表板只揭露彙總訊號,不會變成帳號存在性 查詢工具。