最简流程是:用totp.Generate生成密钥并保存key.Secret()(Base32字符串),用key.URL()生成otpauth:// URI供扫码;验证时调用totp.ValidateCustom,传入用户输入码、key.Secret()、time.Now().UTC()及WithSkew(2)等选项。

用 github.com/pquerna/otp/totp 生成和验证密钥最简流程
直接用 github.com/pquerna/otp/totp 是目前 Go 生态里最轻量、无依赖、符合 RFC 6238 的选择。它不强制绑定 Web 框架,也不需要数据库抽象层,适合嵌入已有服务。
常见错误是跳过密钥编码校验,导致 Base32 解码失败——totp.Generate 返回的 *totp.Key 已含正确编码格式,但手动构造密钥时必须用 base32.StdEncoding 编码原始字节,不能直接传字符串。
-
totp.Generate默认使用 SHA1 + 30 秒周期 + 6 位数字,符合主流认证器(如 Google Authenticator、Authy)兼容要求 - 密钥长度建议 ≥ 20 字节(
crypto/rand.Reader生成),短密钥易被暴力穷举 - 生成后务必保存
key.Secret()(Base32 字符串),不是key.URL()—— 后者含 OTP 参数,仅用于 QR 码生成
生成可扫码的 QR Code URL 要避开 URI 编码陷阱
QR 码内容本质是形如 otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Example 的 URI。问题常出在 issuer 和账户名含特殊字符(如 @、/、空格)时未做 url.PathEscape 或 url.QueryEscape。
正确做法是分别对 issuer 和 account 做路径编码,再拼入 URL:
立即学习“go语言免费学习笔记(深入)”;
issuerEscaped := url.PathEscape("MyApp")<br>accountEscaped := url.PathEscape("user@example.com")<br>uri := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30",<br> issuerEscaped, accountEscaped,<br> url.QueryEscape(key.Secret()),<br> issuerEscaped)
注意:& 是 HTML 实体,在生成纯文本 URI 时应写为 &;若输出到 HTML 页面展示 QR,则需额外转义 & 为 &,否则浏览器解析失败。
验证 TOTP 时必须容忍时间漂移,且禁止硬编码时间窗口
用户设备时钟偏差是常态。硬写 totp.Validate 不带偏移参数,等于只校验当前整 30 秒窗口,失败率极高。
必须用 totp.ValidateCustom 并设置合理滑动窗口:
- 窗口值建议设为 2(即 ±1 个周期,共 3 个连续时间片),覆盖约 ±30 秒偏差
- 时间源必须用
time.Now().UTC(),本地时区会导致验证错乱 - 不要复用同一个
time.Time变量多次调用 —— 验证逻辑应在单次请求内完成,避免跨秒边界
示例:
valid := totp.ValidateCustom(userInputCode, key.Secret(), time.Now().UTC(), totp.ValidateOpts{<br> Period: 30,<br> Skew: 2,<br> Digits: 6,<br> Algorithm: crypto.SHA1,<br>})
存储密钥时别把 Base32 当二进制存,也别明文落库
密钥本身是敏感凭据,但 key.Secret() 返回的是 Base32 字符串(如 "JBSWY3DPEHPK3PXP"),不是原始字节。存数据库时直接存该字符串即可,无需再 base64 或 hex 编码。
容易踩的坑:
- 误把
key.Secret()当作需要解码的密文 —— 它已是标准 Base32,验证时直接传给totp.ValidateCustom - 在 MySQL 中用
VARCHAR(255)足够(最长密钥约 40 字符),别用TEXT增加索引开销 - 若业务要求更高安全等级,可对密钥字符串做 AEAD 加密(如
golang.org/x/crypto/chacha20poly1305),但密钥加密密钥(KEK)管理成本陡增,多数场景没必要
真正关键的是:密钥一旦生成并交付用户,就不可更改;重置流程必须废弃旧密钥、生成新密钥,并强制用户重新绑定认证器。


















