OTP 簡訊實作的 6 個陷阱 — 從驗證碼設計到簡訊轟炸防護
OTP 大概是最多人做過、也最多人做得不夠好的功能。它看起來只是「產生六位數字然後發出去」,但一旦上了生產環境,會冒出一堆當初沒想到的問題。
以下六個,是我們協助客戶整合時最常遇到的。
陷阱一:把 API 回應當成「客戶收到了」
rc: 100 只代表平台收下請求。真正的送達要看 callback 回 DELIVRD。
這件事在 OTP 上特別要緊,因為使用者正在頁面上等。如果你的 UI 顯示「驗證碼已發送」,但實際上第一段 callback 回了 IM(號碼格式錯誤),使用者會盯著空白的收件匣等到超時,然後去客服罵人。
做法: 至少要處理第一段 callback。收到 -100 系列的失敗碼時,即時在前端提示使用者「號碼似乎有誤,請確認」而不是讓他乾等。狀態碼的完整對照在 DLR 狀態碼解讀。
陷阱二:驗證碼有效期設計不當
常見兩種極端:
- 太短(30 秒) — 使用者切換 app、收到電話、網絡稍慢就過期,重送率飆高,成本跟著上去。
- 太長(30 分鐘) — 攻擊窗口大幅拉長,暴力破解變得可行。
做法: 3–5 分鐘是常見的平衡點。同時要注意「有效期」和「可重送間隔」是兩回事 — 驗證碼可以有效 5 分鐘,但重送按鈕應該在 60 秒後才能按。
陷阱三:沒有做速率限制,被 SMS 轟炸
這是最貴的一個坑。
攻擊者寫一支腳本,用你的「發送驗證碼」端點對大量號碼連續發送。你的點數被燒光,號碼主人收到一堆莫名其妙的簡訊,你的品牌變成騷擾來源。
更精緻的版本叫 AIT(Artificially Inflated Traffic,人為灌量) — 攻擊者控制某些高費率地區的號碼段,透過你的 OTP 端點大量發送,從中分潤。你的帳單暴增,但沒有任何一個是真實用戶。
做法(分層防護):
- 同一號碼的頻率上限 — 例如每小時最多 5 次。
- 同一 IP / 同一 session 的上限 — 防單機腳本。
- 地區白名單 — 如果你的服務只做香港與台灣,就不要開放發往其他地區。這一條擋掉的 AIT 最多。
- 異常監控 — 某個號碼前綴的發送量突然暴增,應該要告警。
- 前置驗證 — 在發 OTP 之前先要求通過一個低摩擦的人機驗證。
第 3 點最容易被忽略,但成本效益最高。
陷阱四:OTP 和行銷簡訊走同一條路
行銷簡訊有可能因為內容審查、發送量、時段限制而被延遲或過濾。如果 OTP 跟它擠同一個佇列、同一條路由,使用者的登入體驗就會被行銷活動拖累。
做法: OTP 走獨立路由與獨立佇列。行銷發送再大量,都不影響驗證碼的延遲。這也是為什麼我們在服務設計上把兩者分開處理。
陷阱五:訊息內容寫得不好
幾個實際會影響體驗的細節:
- 驗證碼要放在訊息前段。 很多手機在通知列只顯示開頭幾十個字,把碼放後面等於逼使用者解鎖點開。
- 不要在 OTP 訊息裡放連結。 這會訓練使用者「收到驗證簡訊要點連結」,正好是釣魚攻擊想要的習慣。
- 加上品牌名稱。 使用者同時開著三個 app 註冊時,沒有品牌名的「你的驗證碼是 482910」根本分不出是哪一個。
- 加一句警語。 「本行不會致電索取此驗證碼」這類提示,對金融類服務很有效。
一個還可以的範本:
【你的品牌】驗證碼 482910,5 分鐘內有效。
本公司不會致電向你索取此驗證碼。
注意中文會讓整則訊息以 Unicode 編碼計費,長度上限比純英文短很多 — 這一點在香港 SMS 計費解析有詳細說明。
陷阱六:號碼格式沒有正規化
+852 9123 4567、852-91234567、09123 4567 — 使用者什麼都會輸入。
我們的 API 把地區號碼和號碼本體分成兩個欄位(country_code 與 mobile),而 country_code 要求 1–3 位數字、不含前置 0。如果你把使用者原始輸入直接塞進去,第一段 callback 會回 IM。
做法: 在寫入資料庫之前就正規化,不要在發送時才處理。存進去的應該永遠是乾淨的 country_code + mobile 兩欄,這樣不管之後從哪裡發送都一致。
檢查清單
上線前對一遍:
- 有處理第一段 callback,失敗碼會回饋到前端
- 驗證碼有效期 3–5 分鐘,重送間隔 60 秒
- 同號碼、同 IP 都有頻率上限
- 只開放你實際服務的地區
- OTP 與行銷簡訊路由分離
- 驗證碼在訊息前段、無連結、有品牌名
- 號碼在入庫時就已正規化
- 發送前檢查點數餘額
- Callback 處理是冪等的
實作細節與端點參數在 SMS API 整合指南。有整合上的問題,找我們談 — 我們的工程師處理過不少這類場景。