我運行的網站在註冊時,要求使用者證明他們擁有該電子郵件地址,然後又再要求一次。
首先是一個六位數代碼,透過電子郵件寄出並輸入。只有在輸入正確後,帳戶才會被建立。然後在建立時,驗證服務會發送一封帶有連結的確認信。
兩封信。一個地址。同一個問題被問了兩次。
這看起來很嚴謹,但實際上造成了資料分歧,而且已經把真實使用者置於系統無法合理處理的位置。
無人設計的狀態
有人註冊了。他們收到代碼,正確輸入後帳戶被建立。應用程式立即讓他們登入,這是系統設計的行為。
他們從未點擊第二個連結。他們為什麼要點擊?他們剛從收件匣輸入代碼,現在正看著已登入的頁面。在他們看來,工作已經完成了。
所以紀錄顯示:帳戶存在、密碼已設定、工作階段有效、已登入——而 email_confirmed_at 為空。
這筆紀錄描述的不是半完成的註冊,而是一次已完成的註冊,加上一個無人需要、也未被執行的剩餘標記。但一個名為 email confirmed 且值為 no 的標記,終究會被某些東西讀取。任何支援工具、任何稽核、任何依確認狀態設定的未來功能,都會看到這個使用者並判定他們從未證明過地址。
但他們確實證明了。六週前。另一個表格中有一個已使用的代碼,記錄了他們的名字。
兩筆紀錄,一個事實
這是我花了很長時間才理解的重構。
我一直把第二次檢查視為備援——雙重保險,無害。但它不是備援。它是同一事實被記錄在第二個地方,而同一事實的兩筆紀錄終將產生歧異。
事實是「此人控制此地址」。我的 OTP 表格知道這件事,以已使用的代碼形式記錄。驗證服務也知道,以時間戳記形式記錄。沒有任何機制讓兩者同步,因為它們從未被設計為同一件事——一個是應用程式邏輯,另一個是平台預設設定且未被關閉。
當使用者滿足其中一個而非另一個時,事實就有了兩個值。而當事實有兩個值時,每個下游消費者都必須選擇相信哪一個,通常在不知情的情況下做出選擇。
那個未在執行任何事物的檢查
有另一種版本,第二次檢查是真正的檢查,第一次只是方便。這至少是合理的。
但事實並非如此。該連結沒有執行任何功能。
使用者在確認信有可能被開啟之前就已經登入,因為應用程式在建立帳戶後立即呼叫登入。登入並未查詢該標記。產品中沒有任何東西查詢該標記。無論連結是否被點擊,帳戶的功能都完全相同。
所以第二次檢查無法阻擋任何事、無法閘控任何事,也無法以使用者會注意到的方式失敗。它唯一可觀察到的效果是設定一個欄位,而該欄位與同一事實的另一筆紀錄相矛盾。
這是值得應用的測試:如果一個檢查無法拒絕任何事物,它就不是檢查。它只是一條帶有意見的日誌。
為什麼它原本是開啟的
沒有人選擇這樣做。確認信是平台預設設定,而且是合理的預設——對於註冊流程為「建立帳戶,我們會寄送連結給你」的應用程式而言。這是它被設計的流程。
我們已經用自己的流程取代了該流程,卻從未關閉舊的流程。原始程式碼甚至在註解中說明了這一點:
// Create the user account with auto-confirm (since we verified email via OTP)
Enter fullscreen mode Exit fullscreen mode
意圖被記錄了。它所指的設定卻從未被更改。所以這個註解描述了一個不存在的系統,直接位於造成問題的那一行程式碼上方,持續了數月。
我對那個註解抱有相當的同情。這就是當你建立替代方案時,合理地假設被取代的東西已經讓位,卻發現事實並非如此的結果。
修正,以及已經損壞的資料列
關閉平台確認只需要一個設定。新註冊現在只會收到一封電子郵件,只包含一個代碼,並以單一連貫的事實紀錄登入。
那位被困的使用者需要另一個決定,而這個決定比看起來更有趣。選項有:
- 讓他們保持未驗證標記,這是錯誤的。
- 將標記設定為現在,這也是錯誤的——它表示他們在今天證明了擁有權,但他們並沒有。
- 將它設定為他們實際輸入代碼的時刻。
我選擇了第三個。他們的確認時間戳現在是帳戶建立前三十三秒,這看起來像資料錯誤,但實際上是目前最準確的陳述:他們證明了擁有該地址,然後帳戶才被建立。
我寧願要一個看起來奇怪但為真的時間戳,也不願要一個看起來整潔但為假的時間戳。任何調查該資料列的人都會發現對應的已使用代碼就在另一個表格中,順序會變得合理。
下次我會更早詢問的事
當你用自己的流程取代平台流程時,工作並不是在你的流程運作時就完成了。而是在平台的版本被關閉時才算完成。
在此之前,你擁有的不是一個已驗證的電子郵件地址。你有兩個關於已驗證電子郵件地址的聲明,由不同的系統維護,純粹靠巧合而一致——而這種巧合一直持續到使用者做出完全合理的行為,例如只讀一次收件匣而不是兩次。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.