otpauth:// 二维码里到底存了什么:secret、algorithm、digits、period 逐参数详解

· 约 5 分钟 🔐 TOTP 验证码

扫一张 2FA 二维码只需要一秒,但那张图里编码的是一条完整的 URI,决定了之后每一次登录能不能过。当你需要手工迁移密钥、自建服务发二维码、或者排查「码就是对不上」时,都绕不开读懂这条 URI。

一条典型的完整 URI:

otpauth://totp/GitHub:zhangsan%40example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub&algorithm=SHA1&digits=6&period=30

结构总览

是否必需默认值
schemeotpauth必需
typetotp / hotp必需
labelGitHub:zhangsan建议
secretBase32 密钥必需
issuer发行方名称强烈建议
algorithmSHA1 / SHA256 / SHA512可选SHA1
digits6 / 7 / 8可选6
period秒数可选(仅 totp)30
counter整数仅 hotp 必需

真正必需的只有 secret。其余参数的默认值恰好覆盖了绝大多数真实服务,所以很多二维码里干脆不写——但这不代表你可以随便填,手工拼 URI 时写了和服务端不一致的值,码会永远对不上。

secret:唯一真正要备份的东西

secret 用 Base32 编码,字母表来自 RFC 4648:

A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 2 3 4 5 6 7

注意这 32 个字符里没有 0、1、8、9。这是刻意设计——排除掉和 O、I、B、g 视觉容易混淆的数字。

由此得到一条极其好用的自检规则:

密钥里只要出现 0、1、8、9 中任何一个,一定是抄错了。

纸质备份最高频的事故就是把 O 抄成 0、把 I 抄成 1。有了这条规则,不用登录冒险就能当场发现。

其他细节:

  • 服务端展示时常每 4 位加一个空格(JBSW Y3DP EHPK 3PXP),粘贴时多数实现会自动去空格,少数不会——保险起见自己去掉
  • 末尾可能有 = 填充,绝大多数验证器允许省略
  • 规范上是大写,多数实现会自动转换,但手工填时统一用大写不吃亏

algorithm:写了也可能被忽略

RFC 6238 允许 SHA-1SHA-256、SHA-512 三选一,默认 SHA-1。

问题在于生态:Google Authenticator 等主流实现长期硬编码 SHA-1,直接无视 URI 里的 algorithm 参数。后果是——服务端配了 SHA-256,验证器仍按 SHA-1 算,码永远对不上,而错误提示只会说「验证码错误」,排查起来非常折磨。

那 SHA-1 安全吗?在这个场景下不是短板

  • TOTP 用的是 HMAC-SHA-1,不是裸 SHA-1。SHA-1 已知的碰撞攻击威胁的是数字签名场景,对 HMAC 构造并不适用
  • 码只有 6 位、30 秒失效,服务端还有失败次数限制——真实防线在限流上,不在哈希强度上

实务结论:自建服务就用默认 SHA-1。真要用 SHA-256,必须在绑定页面显著提示用户选择支持的验证器,否则用户流失在这一步。

排查「码永远对不上」时,algorithm 不匹配是仅次于时间不同步的第二大原因,完整排查顺序见2FA 验证码总是对不上的三类坑

digits 和 period:改动是负收益

这两个参数最常被「安全加固」的名义改掉,但算一下就知道不划算。

digits 从 6 改到 8

  • 收益:暴力破解空间从 100 万扩到 1 亿
  • 但真实防线是服务端的失败次数限制(通常 5 次)。在 5 次上限下,100 万和 1 亿的差别趋近于零
  • 代价:部分验证器只显示前 6 位,直接不可用

period 从 30 改到 60

  • 收益:用户输入更从容
  • 代价:一个码的有效窗口翻倍,被钓鱼骗走后的可利用时间也翻倍
  • 同样存在验证器硬编码 30 秒忽略该参数的情况

真正提升安全性的做法是服务端限流、异常登录检测、以及给用户准备好恢复码,而不是把 6 位调成 8 位。除非有明确合规要求,三个可选参数全部保持默认。

totp 与 hotp

两者算法内核完全相同,差别只在计数器从哪来:

TOTPHOTP
规范RFC 6238RFC 4226
counter当前 Unix 时间 ÷ period一个真实的递增整数
码何时变每 period 秒自动每用一次
会过期吗不会,直到被用掉
URI 必需参数secretsecret + counter
典型故障时间不同步计数器失步

HOTP 的经典翻车:你在 App 上多点了几下生成码却没用,客户端 counter 跑到了服务端前面,之后全部对不上。服务端一般开一个前瞻窗口(往后试 10 个)容错,超出就得重新绑定。

现在还能遇到 HOTP 的地方:少数硬件令牌、老式银行动态口令牌、部分企业 VPN——共同点是设备没有电池驱动的时钟,用不了时间做计数器。新系统一律选 TOTP。

label 和 issuer 为什么都要写

看起来重复,但两个都得写,且必须一致。这是历史包袱:

  1. 早期规范只有 label,约定用 Issuer:Account 的冒号写法
  2. 后来发现冒号在 URI 里要编码、解析容易出错,才补了独立的 issuer 查询参数
  3. 为兼容新旧解析器,规范建议两处都写

写法要点:

  • @ 编码成 %40,冒号可以写成 %3A(多数实现也容忍原样的 :
  • 两处的发行方名称大小写要完全一致,否则验证器里会出现「GitHub」和「Github」两个看起来一样的条目
  • label 的账号部分用邮箱或用户名——同一个服务有多个账号时,这是你唯一的区分依据
  • 不要用中文,部分老验证器解析会乱码

手工拼 URI 前先本地验一遍

自己拼的 URI 别直接做成二维码发给用户,先用TOTP 工具本地跑一遍:粘入 secret,按 URI 里的参数逐项设置,看算出的码是否和服务端预期一致。

参数错和时间不同步的症状容易混淆,区分方法很简单:

时间不同步时,码会周期性地偶尔对上;参数不匹配时,码永远对不上。

验证通过后,再用二维码生成把完整 URI 编成图。生成的图片用完即删,别留在会自动同步到云端的目录——那张图是明文密钥,等同于把 2FA 交出去。

密钥的长期保管策略、换机迁移的正确顺序,见手机丢了 / 换新机 2FA 怎么迁移

❓ 常见问题

otpauth:// URI 的完整结构是什么?哪些参数是必需的?

结构是 otpauth://TYPE/LABEL?PARAMETERS,其中只有 secret 是真正必需的逐段:(1) scheme = otpauth,固定;(2) TYPE = totp(基于时间)或 hotp(基于计数器),决定后面必需的参数;(3) LABEL = 账号标识,推荐写成「发行方:账号名」,冒号在 URI 里要编码成 %3A;(4) 查询参数——secret 必需,issuer 强烈建议,algorithm / digits / period 可选且都有默认值。最小可用形式:otpauth://totp/GitHub:zhangsan?secret=JBSWY3DPEHPK3PXP&issuer=GitHub。默认值:algorithm=SHA1、digits=6、period=30——这三个默认值覆盖了绝大多数真实服务,所以很多二维码里根本不写。注意:省略不等于可以随便填,你手工拼 URI 时如果写了和服务端不同的值,码会永远对不上。

secret 必须是 Base32 吗?为什么我抄的密钥填进去报错?

必须是 Base32,且字母表只有 A-Z 和 2-7 这 32 个字符规范:RFC 4648 的 Base32 字母表 = 大写 A-Z(26 个)+ 数字 2-7(6 个),共 32 个。因此:(1) 不存在 0、1、8、9 这四个数字——它们被刻意排除,因为和 O、I、B、g 视觉上容易混;(2) 不存在小写字母(多数实现会自动转大写,但规范上是大写);(3) 末尾可能有 = 号填充,多数验证器允许省略。常见抄错:(1) 把 O 抄成 0、把 I 或 l 抄成 1 —— 这是纸质备份最高频的事故,占我见过的手抄失败的大半;(2) 把 8 当成 B;(3) 服务端展示时每 4 位加了空格,粘贴时空格没去掉——多数实现会容错,少数不会。自检方法:密钥里只要出现 0 / 1 / 8 / 9 中任何一个,一定是抄错了,不用怀疑。

algorithm 写 SHA-256 更安全,为什么很多验证器不认?

因为生态默认值是 SHA-1,而多数验证器直接忽略这个参数现状:(1) RFC 6238 允许 SHA-1 / SHA-256 / SHA-512;(2) 但 Google Authenticator 等主流实现长期硬编码 SHA-1,直接无视 URI 里的 algorithm 参数;(3) 结果是服务端配 SHA-256、验证器仍按 SHA-1 算,码永远对不上,而且报错信息只会说「验证码错误」,极难排查。安全性上要不要慌:不用。TOTP 用的是 HMAC-SHA-1,不是裸 SHA-1——SHA-1 的碰撞攻击威胁的是数字签名场景,对 HMAC 构造不适用;加上 6 位码 30 秒即失效、服务端有失败次数限制,SHA-1 在这里不是短板。实务:(1) 自建服务就用默认 SHA-1,兼容性最好;(2) 确实要用 SHA-256,必须在文档里显著提示用户选择支持的验证器;(3) 排查「码永远对不上」时,algorithm 不匹配是仅次于时间不同步的第二大原因。

digits 和 period 能改吗?改了有什么代价?

能改,但每一项都在拿兼容性换边际收益digits:(1) 默认 6,规范允许 6-8;(2) 改成 8 位把暴力破解空间从 100 万扩到 1 亿,但真实防线本来就是服务端的失败次数限制(通常 5 次),扩大空间的收益接近于零;(3) 代价是部分验证器只显示前 6 位,直接不可用。period:(1) 默认 30 秒,常见改法是 60 秒;(2) 改长 = 用户更从容,但一个码的有效窗口也更长,被钓走后可利用时间翻倍;(3) 改短(如 15 秒)= 用户经常来不及输入,体验很差;(4) 同样存在验证器硬编码 30 秒直接忽略该参数的情况。结论:(1) 除非有明确的合规要求,三个可选参数全部保持默认;(2) 真正提升安全性的做法是加上服务端限流、异常登录检测、以及给用户准备好恢复码,而不是把码从 6 位调到 8 位。

TOTP 和 HOTP 有什么区别?什么时候会遇到 HOTP?

TOTP 用时间做计数器,HOTP 用一个真实的递增计数器,二者算法内核相同HOTP(RFC 4226):(1) 码 = HMAC(密钥, counter),counter 是个整数;(2) 每用一次,客户端和服务端各自把 counter 加 1;(3) 码不会过期,直到被用掉或被跳过。TOTP(RFC 6238):(1) counter = 当前 Unix 时间 ÷ period 取整;(2) 所以码每 period 秒自动变一次,天然过期。HOTP 的现实问题计数器会失步——你在 App 上多点了几下生成码但没用,客户端 counter 跑到了前面,服务端还在原地,之后就全对不上。服务端一般会开一个「前瞻窗口」(look-ahead,比如往后试 10 个)来容错,超出窗口就得重新绑定。哪里还能遇到:少数硬件令牌、部分银行的老式动态口令牌、一些企业 VPN。URI 差异:hotp 类型必须带 counter 参数指明起始值,totp 则不需要。选型:新系统一律用 TOTP,HOTP 只在硬件受限(令牌没有电池驱动的时钟)时才有意义。

LABEL 和 issuer 都写了发行方,重复吗?

看起来重复,但两个都要写,且必须写成一致历史原因:(1) 早期规范只有 LABEL,约定用「Issuer:Account」的冒号写法;(2) 后来发现冒号在 URI 里需要编码、解析容易出错,才补了独立的 issuer 查询参数;(3) 为了兼容新旧解析器,规范建议两处都写正确写法:otpauth://totp/GitHub:zhangsan%40example.com?secret=XXX&issuer=GitHub —— LABEL 里的冒号写成 %3A 或直接用冒号(多数实现都容忍),@ 符号要编码成 %40。不一致的后果:验证器列表里会出现「GitHub」和「Github」两个看起来一样的条目,或者显示成「GitHub - GitHub:zhangsan」这种重复串。实务建议:(1) issuer 用服务的正式名称,大小写固定;(2) LABEL 的账号部分用邮箱或用户名,让你在验证器里一眼能认出是哪个账号——同一个服务有多个账号时这一点尤其重要;(3) 不要用中文,部分老验证器的解析会乱码。

🔐 打开 TOTP 验证码 2FA 动态口令 · 多账号保存 · 兼容 Google Authenticator · 倒计时 · otpauth 二维码 · 本地计算不上传

📖 同一工具的其他教程

🔗 相关阅读

全部教程 →