OTP — 一次性密碼的原理與實作
前言
有時候我們登入服務時,螢幕會跳出一句:「你的驗證碼是 123456」。這串數字只能用一次,過了幾十秒就失效了。這種機制叫做 OTP(One-Time Password,一次性密碼)。
它看似簡單,卻是今天大多數二階段驗證(2FA)或多因子驗證(MFA)的基石。這篇文章,就來聊聊 OTP 的運作方式,最後會給一個 Go 的範例程式,讓你自己動手試試。
OTP 是什麼?
顧名思義,OTP 就是 只能使用一次的密碼。
它的目的很單純:防止密碼被竊取後還能重複使用。即便攻擊者截獲了一組 OTP,它在下一次登入時也已經過期,等於沒用了。
這樣的設計,有效降低了「重放攻擊」的風險。
OTP 的生成方式
常見的 OTP 有兩種:
HOTP (HMAC-based One-Time Password)
- 基於計數器,每次請求遞增一次。
- 適合事件觸發的情境(例如交易次數)。
TOTP (Time-based One-Time Password)
- 基於時間,每隔 30 秒或 60 秒換一組。
- 更適合登入驗證(Google Authenticator、Authy 常用的就是這種)。
OTP 通常會透過幾種方式傳遞給使用者:
- 硬體 Token(小小的驗證器)
- 手機 App(Google Authenticator、Microsoft Authenticator)
- 簡訊 / Email(雖然方便,但安全性相對較弱)
OTP 在登入流程中的位置
來看一張序列圖,幫助我們理解 OTP 在認證流程中的角色:
sequenceDiagram
participant User as 使用者
participant Server as 服務端
participant OTP as OTP 產生器 (App/簡訊/硬體)
User->>Server: 輸入帳號密碼
Server-->>User: 驗證密碼成功,要求輸入 OTP
User->>OTP: 查看 OTP (App/簡訊/硬體 Token)
OTP-->>User: 顯示 6 位數 OTP
User->>Server: 輸入 OTP
Server->>Server: 驗證 OTP 是否正確 (TOTP/HOTP)
Server-->>User: 登入成功
👉 可以看到 OTP 並不是單獨存在,而是作為 密碼驗證之後的第二步。
OTP 的好處
- 安全性更強:一次性使用,舊密碼完全無效。
- 降低密碼重用風險:即使其他服務外洩,也影響不到這裡。
- 很適合搭配 2FA / MFA:形成額外保護層。
OTP 的限制
- 仍可能被釣魚:如果使用者把 OTP 輸入到假的網站,攻擊者還是能即時盜用。
- 簡訊 OTP 風險高:可能遭遇攔截或 SIM 卡攻擊。
- 設備同步問題:時間或 counter 不一致時,會出現驗證失敗。
Go 程式實作
下面給兩個小範例,一個是 TOTP,一個是 HOTP。
範例一:TOTP(時間制 OTP)
1package main
2
3import (
4 "fmt"
5 "log"
6 "time"
7
8 "github.com/pquerna/otp"
9 "github.com/pquerna/otp/totp"
10)
11
12func main() {
13 // 建立一組新的 TOTP key
14 key, err := totp.Generate(totp.GenerateOpts{
15 Issuer: "ExampleCorp",
16 AccountName: "alice@example.com",
17 Period: 30,
18 Digits: otp.DigitsSix,
19 Algorithm: otp.AlgorithmSHA1,
20 })
21 if err != nil {
22 log.Fatal(err)
23 }
24
25 fmt.Println("Secret:", key.Secret())
26 fmt.Println("Provisioning URI:", key.URL())
27
28 // 產生當下的 OTP
29 code, _ := totp.GenerateCode(key.Secret(), time.Now())
30 fmt.Println("Current OTP:", code)
31
32 // 驗證使用者輸入
33 ok := totp.Validate(code, key.Secret())
34 fmt.Println("驗證結果:", ok)
35}
👉 key.URL()
可以直接用來產生 QR code,讓 Google Authenticator 掃描。
範例二:HOTP(計數制 OTP)
1package main
2
3import (
4 "fmt"
5 "log"
6
7 "github.com/pquerna/otp/hotp"
8)
9
10func main() {
11 secret := "JBSWY3DPEHPK3PXP"
12 counter := uint64(1)
13
14 // 產生 OTP
15 code, err := hotp.GenerateCode(secret, counter)
16 if err != nil {
17 log.Fatal(err)
18 }
19 fmt.Println("HOTP:", code)
20
21 // 驗證
22 ok := hotp.Validate(code, secret, counter)
23 fmt.Println("驗證結果:", ok)
24}
👉 實際應用中,伺服器需要保存並更新 counter
。
小結
OTP 的定位很清楚:
- 它不是取代密碼,而是 強化認證流程 的一環。
- TOTP 與 HOTP 各有應用場景,但 TOTP 在日常帳號登入上更常見。
- 雖然 OTP 提升了安全性,但仍要注意釣魚與簡訊風險。
對開發者來說,Go 已經有現成套件(pquerna/otp
),幾行程式就能完成 OTP 的產生與驗證。