在线 TOTP 工具是怎么算验证码的?原理、实现与开源

最后更新:2026/9/19

平台:在线 TOTP 工具

在线取码就三步:把平台给的密钥粘进输入框,页面上立刻跳出一个 6 位数字,三十秒后自动换成下一组。

这串密钥粘进来之后,会不会被发到哪个服务器上去?

密钥基本等同于密码:别人拿到它,就能算出跟你一模一样的验证码。这篇把这个工具的计算过程讲清楚——算在哪儿、怎么算的、代码为什么公开。

一、先回答最让人担心的那个问题:密钥去了哪儿

答案是:哪儿都没去,它一直待在你自己的浏览器里。

计算用的不是我们服务器的算力,而是浏览器自带的 Web Crypto(crypto.subtle)。页面加载完之后断网也能继续出码,因为后面的步骤一步都不走网络。

这个页面没有把密钥发出去的接口,服务端也不存它。所以我们收不到你的密钥,也就没法在你说“密钥找不回来”的时候帮你恢复——这既是优点,也是必须先说清的代价。

二、一串 Base32 密钥,是怎么变成 6 位数字的

平台让你“用验证器应用扫码”的时候,二维码里装的其实是一段这样的地址:

otpauth://totp/GitHub:octocat?secret=JBSWY3DPEHPK3PXP&issuer=GitHub

问号后面那个 secret,就是一串 Base32 编码的密钥。它变成屏幕上的 6 位数,中间走四步:

在线 TOTP 工具的计算流程:Base32 密钥解码、按 30 秒划分时间窗口、对时间片做 HMAC-SHA1、动态截断后取模得到 6 位验证码

第一步,把 Base32 还原成原始字节。 Base32 只用 A-Z 和 2-7 这 32 个字符编码二进制数据,为的是让人能手写、能口述,也避开容易混淆的 0 和 O、1 和 I。粘贴进来的密钥会先做一次规整:转大写、去掉空格和补位符号,再解成字节。

第二步,算出当前是第几个时间窗口。 公式是 时间片 = (当前时间戳 - 1970 年起点) ÷ 30,向下取整。多数平台用 30 秒一个窗口,也有用 60 秒的。同一个密钥,两台设备在同一时刻会算出同一个结果——前提是两边的时间都对得上。

第三步,用密钥对这个时间片做一次 HMAC。 也就是 HMAC-SHA1(密钥, 时间片),得到 20 个字节。HMAC 的特点是只有拿着同一个密钥的人才算得出同样的摘要,又没法从摘要倒推回密钥。

第四步,动态截断并取 6 位。 取上一步结果最后一个字节的低 4 位当偏移量,从摘要里往前取 4 个字节,去掉最高位的符号位,得到一个 31 位整数;再对 10 的 6 次方取余,不足 6 位的前面补零——屏幕上那个数字就是它了。

带图的完整分解在这篇:TOTP 算法是怎么算出验证码的。6 位、8 位、30 秒、60 秒、SHA1、SHA256、SHA512 都是公开标准里规定好的选项,我们的工具把它们做成可调项,是为了对付少数平台用了非默认配置的情况。

三、为什么坚持在本地算,而不是丢给服务器

把密钥传到服务器上算,实现起来更简单,还能顺手替你存着。我们不这么干:密钥等同于密码,服务器一旦存了它,就多了一个可能被拖库的地方,也等于我们要替你担一份本来不必担的责任。

本地计算的代价也得说清楚:密钥存在浏览器的 localStorage 里,清空缓存、换个设备、用无痕窗口打开,它都会消失,我们这边没有副本可以找回。所以它适合临时救急、测试联调、或者“手机不在手边但现在就要这个码”的时刻,不适合当仓库。

所以网页版只管临时取码,长期保存交给小程序,第七节接着说。

四、我们把这套算法开源了

负责算码的那一小块被抽成了一个独立的开源包:

npm 包名2faguide-tools-online-totp-core
当前版本0.1.3
源码github.com/notbtc/free2fa-tools-online-totp-core
npmnpmjs.com/package/2faguide-tools-online-totp-core
依赖数量0
许可MIT,版权归 Free2FA

它按 RFC 4226 与 RFC 6238 实现,跑在 Web Crypto 之上,一共三个 ESM 文件、大约三百行代码,浏览器、Node、Workers、Deno、Bun 都能直接跑,不用给 crypto 打补丁,也不需要 Buffer。RFC 4226 附录 D 和 RFC 6238 附录 B 里的官方测试向量(覆盖 SHA1 / SHA256 / SHA512、6 位和 8 位)都作为断言跑在 CI 上。整个包里没有一行网络请求代码,这一点可以自己 grep 验证。

本站自己也在用另一个小工具 2faguide-rehype-image-dimensions:构建期给文章里的图片补上真实宽高和懒加载属性,免得看文章时图片把文字顶来顶去。同样零依赖、零运行时 JS,同样 MIT。

开源的目的就一个:这套代码经得起看,也欢迎直接拿去用。

五、想用到自己的项目里,一行命令就行

要用就直接装:

npm install 2faguide-tools-online-totp-core
import { totp, generateSecret, buildOtpauth, parseOtpauth } from '2faguide-tools-online-totp-core';

// 直接算一个当前有效的 6 位码(返回字符串,所以开头的 0 不会被吃掉)
const code = await totp('JBSWY3DPEHPK3PXP');

// 生成一串新的随机密钥,默认 160 位
const fresh = generateSecret();

// 拼出可以塞进二维码的 otpauth 地址
buildOtpauth({ secret: fresh, name: 'alice', issuer: 'ACME' });

// 反过来,把一个 otpauth 地址解析成字段(格式不对返回 null,不会抛异常)
parseOtpauth('otpauth://totp/GitHub:octocat?secret=JBSWY3DPEHPK3PXP&issuer=GitHub');

除了上面几个,它还导出了 remainingSeconds(给倒计时环形进度条用)、hotp(按计数器的 HOTP,对应 RFC 4226)、base32Encode / base32Decode 这些零件。

一个细节要注意:Web Crypto 只在安全上下文里可用,也就是说页面得跑在 https 或者 localhost 上,用 http 直接开拿不到 crypto.subtle

六、这套东西也有做不到的事

做不到的事也一并说清楚:

  • 手机丢了、而且从没保存过密钥或二维码。这种情况下谁都取不回原来的码,只能走平台的备用码或者账号申诉。多数平台在开启两步验证的页面上会塞一组“备用码 / 恢复码”,没存的话现在去账号安全设置里找还来得及。
  • 想在没有密钥的情况下算出别人的验证码。TOTP 靠共享密钥算,没有密钥就算不出来,这套代码也不是破解工具。
  • 替你长期保管密钥。本地计算必然伴随“本地不负责任何备份”。

七、长期保存密钥,交给小程序更省心

如果你发现自己在不少地方都用到了 TOTP,建议把日常账号从网页版挪进微信小程序 free2fa二次验证码。它的几个功能正好补上网页版的短板:

绑定之后密钥自动加密备份到云端,换手机登录同一个微信就能全部恢复,不用去每个平台重新申请;误删的密钥先进回收站,想要彻底删还得再确认一次,手滑的成本很低;从其他验证器搬过来也不用一个个重绑,比如谷歌验证器的数据可以导出后在小程序里批量导入。

具体的绑定做法看这篇:三步绑定密钥并生成动态口令;如果是从别处迁过来,看这篇:谷歌验证器里的密钥怎么批量导入。要取码时更顺手,可以直接打开这个在线工具:网页版 2FA 验证器

八、关于在线 TOTP 工具的几个疑问

页面上出的码,和手机上验证器出的一样吗? 一样,只要密钥、时间、位数、周期、算法这五项一致。这也是为什么它能当作“网页版谷歌验证器”用——Google 官方虽然没出网页版,但 TOTP 是公开标准(RFC 6238),任何设备拿到同一串密钥都能算出同样的结果。

为什么有时候是 6 位,有时候是 8 位? 位数是平台开启两步验证时决定的,绝大多数是 6 位。Steam 这类平台会用 5 位字符的形式,少数企业系统配到 8 位。我们的工具这几档都支持。

代码是对的,但你们线上跑的真的是这份代码吗? 是同一个包。本站构建时直接依赖它,你看到的这个页面就是用它算的。

换台电脑,之前算过的密钥会跟着走吗? 不会。密钥存在当时那台电脑的浏览器里,换个地方就是空的。

想试试,打开在线 2FA 验证器页面,粘一串测试密钥进去就能看到效果。

有使用问题,或者有建议反馈,可以加客服微信,微信号:free2fa

free2fa二次验证码小程序码,微信扫码即可打开使用

Free2FA 二次验证码微信小程序码

在微信中打开 Free2FA 小程序

微信扫码即可使用,无需下载 App

更多打开方式 →