在线 TOTP 工具是怎么算验证码的?原理、实现与开源
最后更新:2026/9/19
平台:在线 TOTP 工具在线取码就三步:把平台给的密钥粘进输入框,页面上立刻跳出一个 6 位数字,三十秒后自动换成下一组。
这串密钥粘进来之后,会不会被发到哪个服务器上去?
密钥基本等同于密码:别人拿到它,就能算出跟你一模一样的验证码。这篇把这个工具的计算过程讲清楚——算在哪儿、怎么算的、代码为什么公开。
一、先回答最让人担心的那个问题:密钥去了哪儿
答案是:哪儿都没去,它一直待在你自己的浏览器里。
计算用的不是我们服务器的算力,而是浏览器自带的 Web Crypto(crypto.subtle)。页面加载完之后断网也能继续出码,因为后面的步骤一步都不走网络。
这个页面没有把密钥发出去的接口,服务端也不存它。所以我们收不到你的密钥,也就没法在你说“密钥找不回来”的时候帮你恢复——这既是优点,也是必须先说清的代价。
二、一串 Base32 密钥,是怎么变成 6 位数字的
平台让你“用验证器应用扫码”的时候,二维码里装的其实是一段这样的地址:
otpauth://totp/GitHub:octocat?secret=JBSWY3DPEHPK3PXP&issuer=GitHub
问号后面那个 secret,就是一串 Base32 编码的密钥。它变成屏幕上的 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 |
| npm | npmjs.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

