news 2026/10/14 12:33:53

TOTP动态口令生成器:Python与易语言双版本离线实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TOTP动态口令生成器:Python与易语言双版本离线实现

你在登录某个游戏平台时,除了账号密码,还要输一个6位数动态码——这就是动态口令令牌生成的一次性密码。这类动态码生成源码在中文技术社区里一直很热门,但大多数实现要么封装太重,要么依赖在线服务器,要么绑死硬件狗。这篇文章我给你一套可以直接复用的方案:Python和易语言双版本,离线可用,一键部署。两个版本都基于标准TOTP算法,不依赖网络,适合学习协议原理、做自动化登录测试、或者在封闭内网里自己写一个临时验证工具。

先说清楚一件事:这类动态码不是“随机生成”的,它背后是公开的RFC标准算法。你会看到网上有人把它包装成“破解某某令牌”的玄学,其实拆开看就是HMAC加上时间戳的数学运算。这篇就把TOTP的每个细节掰开揉碎,包括Base32密钥怎么标准化、HMAC摘要怎么截断、易语言里字节序怎么处理,还有我实际调试时踩过的那些坑。代码都是可运行的,你只需要把密钥换成自己的就能跑。

1. 动态码核心原理:不是“破解”,是标准算法

1.1 从HOTP到TOTP:一段密码学演化史

动态口令的历史不算长,但演变路径非常清晰。最早出现的是HOTP,全称是HMAC-Based One-Time Password,定义在RFC 4226里。它由一个递增计数器和一个共享密钥共同驱动,每次验证成功后计数器加一,从而生成不同的密码。在银行U盾、老式硬件令牌里,这种机制非常常见。它的缺点是客户端和服务端必须严格同步计数器,任何一方多算一次或少算一次,整个验证链就会错位。

TOTP就是为解决同步问题而来的,全称是Time-Based One-Time Password,定义在RFC 6238。它把HOTP的计数器换成了“当前Unix时间戳除以时间步长”的整数结果。常规实现默认30秒为一个步长,也就是说每30秒更换一次密码。只要客户端和服务端的时间基本一致,双方就能在同一个时间窗口内算出完全相同的6位数字。你现在看到的各种游戏平台安全令牌、身份验证器App,几乎清一色是TOTP。

这个“时间步长”的设计非常巧妙。它不需要额外的同步过程,因为时间本身就是天然同步的。哪怕设备之间的时间有几秒偏差,只要偏差没超过步长窗口的一半,验证依然能够通过。很多实现甚至允许前一个或后一个时间窗口的密码,进一步增加容错。

1.2 三个关键输入:密钥、计数器、摘要算法

TOTP算法表面上只看三个输入:密钥、计数器、摘要算法。但每个输入都有讲究。

密钥是最敏感的部分。它不是普通文本,而是经过Base32编码的二进制字节。Base32的字符集只有大写字母A-Z和数字2-7,一共32个字符,每个字符表示5位数据。这种方式比Base64更适合人类手工输入,也方便二维码编码。但代价是Base32编码后的字符串长度必须是8的倍数,否则解码器会直接报错。这也是新手最容易翻车的地方。

计数器则是当前时间戳整除步长的结果。以30秒步长为例,假设当前Unix时间戳是1750000000,那么计数器就是58333333。这个整数在参与HMAC运算前,必须转换成8字节的大端序无符号整数。所谓大端序,就是高位字节在前、低位字节在后。很多人把这里改成小端序,结果动态码怎么都对不上。

摘要算法方面,RFC 6238默认使用HMAC-SHA1。我知道很多人一听到SHA1就觉得不安全,但在TOTP这种一次性密码场景里,SHA1的128位安全强度已经完全够用。更重要的是兼容性,几乎所有的令牌硬件和服务端都支持SHA1。如果你的服务端明确支持SHA256,也可以把hashlib.sha1换成hashlib.sha256,算法流程完全一样,只是摘要长度不同。

1.3 一个计算实例:从时间戳到6位数字

为了让你彻底明白,我手动拆解一次完整计算过程。假设当前时间是2025年1月1日0点整,对应Unix时间戳是1735689600。步长30秒,计数器就是1735689600 // 30 = 57856320。

接下来把计数器57856320转成8字节大端序,得到十六进制00 00 00 00 03 72 90 40。以这个字节序列作为消息,以Base32解码后的密钥字节作为HMAC密钥,使用HMAC-SHA1计算摘要,得到20字节的二进制数据。

动态截断从这里开始。取摘要最后一个字节的低4位,得到一个0到15之间的偏移量。假设偏移量是12,那么从摘要的第12个字节开始取4字节,得到一个32位整数。把这个整数的最高位置为0,也就是与0x7FFFFFFF做与运算。最后用这个数对1000000取模,得到6位动态码。如果结果不足6位,就在前面补零。

整个计算过程中,最反直觉的是动态截断。为什么不直接取摘要的前6位十六进制?因为历史原因,HOTP算法需要兼容不同厂商的硬件实现。动态截断是一种标准化的“后处理”步骤,保证所有实现产生同样的结果。

1.4 为什么说这不是“破解”

很多标题带“将军令”或“动态码”字样的源码,会让人觉得是在破解安全系统。实际上,TOTP算法是完全公开的,RFC标准文档可以随便下载,各种开源实现也遍地都是。所谓“生成动态码”,本质上就是实现一个标准的TOTP客户端,和你手机上安装的验证器App没有任何区别。

区别在哪里?区别在于用途。如果你写这个工具是为了学习算法、方便自己管理测试环境、或者在获得授权的前提下做安全实验,那完全合理。但如果你是拿它去绕过别人的登录保护、批量注册、撞库,那就是另一回事了。技术本身是中性的,用在哪、怎么用,才是要命的事。

注意:动态码属于身份认证信息,只建议用于学习算法、个人测试、自用工具或获得授权的开发环境。绝不可用于破解他人账号或绕过安全验证。

2. 双版本方案选型:为什么是Python和易语言

2.1 Python:适合做算法验证与自动化

Python版本是我个人最喜欢的算法原型工具。原因有三个:第一,标准库自带hmac、hashlib、base64、struct,整个TOTP核心逻辑不需要安装任何第三方包;第二,Python的字符串处理和文件IO非常方便,适合批量处理多个密钥;第三,跨平台,Windows、Linux、macOS都能跑,做自动化测试时可以无缝集成到脚本流程里。

我实际用Python版做的最多的事情,是在自动化登录脚本里动态获取验证码。以前遇到测试账号开启二次验证,每次都要手动打开手机看验证码,很烦。后来写了一个命令行版TOTP生成器,需要验证码的时候直接调用Python脚本,把输出填到登录表单里,整个过程不到一秒。

Python还可以快速做对比实验。比如我想验证某个平台的动态码算法是否用了SHA256,只需要在代码里改一行,再跟官方App显示的数字比对,就能得出结论。这种灵活性在别的语言里需要折腾半天。

2.2 易语言:适合做Windows交付工具

易语言版本则是为了解决“给别人用”的问题。Python脚本虽然好写,但目标电脑上不一定装了Python解释器。面对一个完全不懂技术的同事,你丢给他一个.py文件,他大概率不知道怎么跑。这时候,易语言编译出来的独立exe就非常友好,双击运行,不需要任何环境依赖。

易语言的另一个优势是中文编程。程序集、子程序、变量、如果真……这些保留字都是中文,对不熟悉英文编程的人门槛低很多。在Windows桌面环境下,易语言做小工具的开发效率很高,界面控件直接拖拽,事件处理也很直观。

我一般把Python版当作“算法验证机”,把易语言版当作“交付成品机”。一套逻辑两套皮,Python里跑通的数字和易语言里跑出来的数字完全一致,再拿去给同事用,心里很有底。

2.3 目录结构与模块划分

不管哪个语言,我都建议把代码组织成清晰的模块。Python版的目录结构可以是这样:

totp_generator/ ├── config.ini ├── main.py ├── totp_core.py └── start.bat

totp_core.py放纯算法函数,不依赖任何界面逻辑。main.py负责命令行或GUI交互。config.ini存放密钥和步长配置。start.bat负责一键启动。

易语言版则不需要那么多文件,因为编译器会把所有东西打包进一个exe。但建议在源码里也做模块化:密钥读取、算法计算、界面刷新分别写成独立的子程序,方便维护和后期改动。

我见过很多人喜欢“一个文件搞定全部功能”,刚开始确实爽,但一旦算法需要换摘要算法或者界面要加新功能,改起来灾难一样。模块化不是形式主义,是给自己留后路。

2.4 为什么不用现成开源库直接调

你可能会问,Python有pyotp这样的现成库,直接调一行代码就搞定,为什么还要自己写?我承认pyotp很优秀,但它的封装把很多关键细节隐藏了。如果你只是想拿到验证码,用库确实省事;但如果你想理解动态码的生成原理、想把它移植到易语言或C++,那就必须自己实现一遍。

自己实现还有一个额外好处:可以完全控制边界行为。比如默认步长、允许的时间偏移窗口、位数长度,这些在库里有默认值,但你的场景可能不同。自己写以后,想改哪里改哪里,不会被困在别人的设计决策里。

3. 核心实现细解:Base32密钥、HMAC-SHA1、动态截断

3.1 Base32标准化:最容易翻车的一步

几乎所有人第一次写TOTP都会在Base32解码的地方卡住。Base32编码的字符集是大写字母A-Z和数字2-7,但在实际使用中,密钥输入经常混着空格、小写、破折号,甚至有人会把数字0和1放进去。这些都会导致解码失败。

Python标准库的base64.b32decode要求输入长度必须是8的倍数,否则直接抛binascii.Error。所以标准的预处理流程是:去掉所有空格和破折号、统一转大写、再按长度补齐等号。

我给出一个稳定版函数:

import base64 def normalize_and_decode(secret: str) -> bytes: # 去掉空格、破折号,统一大写 cleaned = ''.join(secret.upper().split()) # 补齐 padding if len(cleaned) % 8 != 0: cleaned += '=' * (8 - len(cleaned) % 8) return base64.b32decode(cleaned)

注意,如果清理后的字符串里出现数字0或1,那一定是复制错了。正常Base32字母表只有A-Z和2-7,不会出现这两个讨厌的数字。遇到这种情况,先去检查原始密钥来源。

3.2 时间戳与步长计数器的转换

TOTP里的计数器是时间戳整除步长的整数结果。它的计算本身很简单,难点在于“字节序”。

Python的struct.pack('>Q', counter)可以一步生成8字节大端序无符号整数。如果你不喜欢struct,也可以用位运算手动组装:

def int_to_8bytes_big_endian(value: int) -> bytes: return bytes([ (value >> 56) & 0xff, (value >> 48) & 0xff, (value >> 40) & 0xff, (value >> 32) & 0xff, (value >> 24) & 0xff, (value >> 16) & 0xff, (value >> 8) & 0xff, value & 0xff, ])

移位操作看起来啰嗦,但能让你清楚地看到字节如何从高到低排列。易语言版本里没有直接的并行打包函数,我用的是这种手动移位思路,然后逐个字节放进字节集。

3.3 动态截断:HMAC摘要的“抽签”

HMAC-SHA1输出20字节摘要,但动态码不能直接显示整个摘要,必须经过RFC里定义的Dynamic Truncation过程。这个过程的步骤非常固定:

  1. 取摘要最后一个字节的低4位,得到偏移量offset(范围0到15)。
  2. 从摘要的第offset字节开始取4字节,得到一个32位二进制数。
  3. 将最高位置为0,即与0x7FFFFFFF做与运算。
  4. 对结果取模10**digits,得到最终的动态码。

这个“抽签”式的截断逻辑,是为了兼容早年的硬件令牌。硬件令牌的计算资源有限,不可能把20字节摘要全部参与十进制转换。动态截断能保证无论硬件还是软件实现,只要遵循同一个标准,就能得到一模一样的验证码。

Python实现如下:

import hmac import hashlib import struct import time def totp(secret_b32: str, step: int = 30, digits: int = 6) -> str: key = normalize_and_decode(secret_b32) counter = int(time.time()) // step msg = struct.pack('>Q', counter) digest = hmac.new(key, msg, hashlib.sha1).digest() offset = digest[-1] & 0x0F binary = struct.unpack('>I', digest[offset:offset+4])[0] & 0x7FFFFFFF code = binary % (10 ** digits) return str(code).zfill(digits) if __name__ == '__main__': # 公开测试密钥,自行替换 print(totp('JBSWY3DPEHPK3PXP'))

这段代码只用标准库,在任何装有Python 3.6+的机器上都能离线运行。zfill(digits)保证输出是完整的6位,不会因为前导零被丢掉。

3.4 一个完整的Python GUI示例

核心算法搞定后,我们可以在外面包一层简单的tkinter界面,做成一个像模像样的离线小工具。代码不长:

import tkinter as tk import threading import pyperclip # 可选,用于剪贴板;如果不想引第三方库,可用 tkinter.clipboard_clear class TotpApp: def __init__(self, root): self.root = root self.root.title("离线动态码") self.label = tk.Label(root, text="------", font=("Arial", 48)) self.label.pack(pady=20) self.copy_btn = tk.Button(root, text="复制", command=self.copy_code) self.copy_btn.pack() self.timer_label = tk.Label(root, text="") self.timer_label.pack() self.update_loop() def update_loop(self): code = totp(load_secret()) self.label.config(text=code) self.timer_label.config(text="剩余秒数: " + str(30 - int(time.time()) % 30)) self.root.after(1000, self.update_loop) def copy_code(self): self.root.clipboard_clear() self.root.clipboard_append(self.label.cget("text"))

这里我没有把密钥写在前端页面里,而是通过load_secret()从config.ini读取。部署的时候,用户只需要改配置文件,不需要碰代码。

4. 易语言版实现与一键部署

4.1 易语言的窗口与定时器设计

易语言版我通常做一个300x200的小窗口,上面有一个大字标签显示动态码,一个“复制”按钮,一个时钟控件。时钟周期设置为1000毫秒,每秒刷新一次显示。这样用户看一眼就知道当前动态码,剩余时间会在标签下方显示。

界面控件对应事件非常简单:窗口创建完毕时读取配置文件,时钟周期事件里计算动态码并更新标签,按钮被点击时把当前动态码写入剪贴板。不需要搞复杂的状态机,因为TOTP的计算是无状态的——同一时刻,算法把密钥和时间戳作为输入,永远能算出同样的结果。

4.2 易语言核心逻辑与字节集操作

易语言的代码风格和Python差异很大,关键字是中文的,字节集索引从1开始。下面给出关键逻辑的示意性骨架:

.版本 2 .支持库 spec .程序集 程序集1 .子程序 _时钟1_周期事件 局部变量 计数器, 长整数型 局部变量 计数器字节集, 字节集 局部变量 HMAC字节集, 字节集 局部变量 偏移, 整数型 局部变量 二进制数, 整数型 局部变量 验证码, 文本型 计数器 = 取时间戳 () ÷ 30 计数器字节集 = 到大端字节集 (计数器) HMAC字节集 = 取HMAC_SHA1 (到字节集 (读取密钥()), 计数器字节集) 偏移 = 位与 (HMAC字节集 [取字节集长度 (HMAC字节集)], 15) + 1 ' 从偏移位置取4字节并转成32位整数,具体需要根据字节集索引调整 二进制数 = 取字节集4字节整数 (HMAC字节集, 偏移) 二进制数 = 位与 (二进制数, 2147483647) 验证码 = 到文本 (二进制数 % 1000000) ' 补0到6位,如果长度不足 ...

这段代码不是完整可运行版本,目的是展示映射关系。细心的读者会发现,Python里偏移量是从0开始的,易语言里字节集索引从1开始,所以要做加一处理。很多从Python移植到易语言的人就在这一步出错。

4.3 一键部署:从源码到双击运行

“一键部署”在这里有两层含义。对Python版,我会额外提供一个start.bat批处理脚本,双击后自动检查当前目录下的totp_core.py是否可导入,然后启动GUI。脚本内容大致如下:

@echo off cd /d %~dp0 where python >nul 2>nul if errorlevel 1 ( echo [ERROR] 未检测到Python,请先安装Python 3. pause exit /b ) python main.py pause

对易语言版,编译成exe后本身就够了。分发时只需把exe和config.ini放在同一个目录,用户双击exe就运行,不需要安装任何运行库。这就是“真·一键部署”。

4.4 配置文件与密钥管理

我不想让密钥硬编码在源码里,所以两个版本都使用config.ini保存配置。格式很简单:

[otp] secret = JBSWY3DPEHPK3PXP step = 30 digits = 6

启动时先读取配置文件,读不到就弹窗提示用户录入新密钥。这样换密钥不需要重新编译exe,也更安全。我在某个内部工具里就是这样部署的,运维同事只需要改一行配置,重启工具就能换账号密钥,不用再找开发改代码。

注意:不要把真实密钥直接粘贴到分享出来的源码或截图里。密钥一旦泄露,任何人都能算出你的动态码。写技术文章时,请用公开的测试密钥代替。

5. 实操避坑:时间不同步、Base32校验、密钥管理

5.1 时间偏移导致的验证失败

最常见的现象是“我的码和官方App显示的差一位”。原因很简单:本地计算机时间慢了,或者快了,而且偏差超过了一个步长窗口。TOTP是时间驱动的,如果本地时间和真实时间差太多,算出来的码自然就错。

解决办法有三个:

  • 开启系统自动时间同步,让Windows或macOS自己校准;
  • 在算法里增加“宽松窗口”,允许上一个或下一个时间步长的密码通过校验;
  • 在UI上显示“当前时间偏移量”,调试阶段非常管用。

我自己实际调试时,被这个问题坑过整整一个下午。当时我写了个易语言版本,怎么算都跟官方App对不上,最后打开系统时间一看,慢了2分钟。所以先把时间同步问题排查干净,再怀疑自己的代码。

5.2 Base32校验的最佳姿势

很多密钥在复制时会混入空格、破折号,有些二维码扫描工具还会把小写字母输出出来。统一的处理方式:去空格、转大写、去破折号、补齐=。如果解码后长度不对,优先检查原始密钥的字符集,是否混入了数字“1”或“0”。

这里还有一个很容易被忽略的细节:Base32的Padding规则。Python的base64.b32decode要求补齐到8的倍数,但如果你用的是一个格式良好的密钥,它本身可能就符合8的倍数,不需要额外补。最稳妥的做法是不管三七二十一,先算一下长度再决定是否补=。

5.3 易语言字节序的坑

易语言的“整数型”是32位有符号整数,如果你直接把时间戳整除30后的计数器赋值给整数型,一旦时间戳超过2147483647,就会变成负数,动态码全部错乱。我建议使用“长整数型”,并且在大端字节集转换时逐字节取位。

还有一个坑是易语言的位运算符。位与、位或、左移等操作符在字节集和整数之间转换时,要特别注意数据类型的位宽。我见过有人直接用整数型接收HMAC摘要的字节值,结果高位被截断,最后动态码差了十万八千里。

5.4 两个版本交叉验证

我在做这套源码时,会在同一台机器上同时运行Python版和易语言版,连续采样十次,要求九次完全一致。偶尔一次不一致,通常是因为正好卡在30秒边界——两个程序启动的瞬间不同,一个落在上一个窗口,一个落在下一个窗口。

处理边界问题的思路有两个:一是把显示刷新频率提高到每200毫秒一次,只在上一次计算剩余时间小于等于0时重新计算;二是在展示界面时多显示一个“剩余秒数”,让用户知道当前动态码已经用了多少秒,自然就理解为什么界面刷新前和刷新后会有短暂不一致。

6. 常见问题排查与合规使用提醒

6.1 问题速查表

问题现象大概率原因解决方法
动态码隔一段时间整体错位系统时间偏差过大开启自动时间同步,或加宽松窗口
Python版报binascii.ErrorBase32长度不对或字符非法标准化:去空格、转大写、补=
易语言版输出负数计数器用了整数型导致溢出改用长整数型
两个版本输出不一致大端/小端顺序不同用固定测试密钥逐位对比
复制到剪贴板多一个换行标签内容带了自动换行使用取文本后再置入剪贴板
配置文件读取失败编码格式不对统一使用ANSI或UTF-8无BOM编码

6.2 学习完你应该能回答的问题

学完这个项目,你可以用下面这份清单自测:

  • 能否不用任何第三方库,写一个离线TOTP生成函数?
  • 能否把Python核心函数改造成支持SHA256?
  • 能否在易语言中实现同样的动态截断逻辑?
  • 能否说出Base32和Base64在密钥表示上的区别?
  • 能否解释为什么动态码能防重放攻击?

如果这些都能回答,说明你已经彻底掌握了动态口令的底层原理。以后看到任何“XX令牌生成器”的源码,对你来说都是同一套TOTP的不同外壳,不会被眼花缭乱的界面迷惑。

6.3 一定要想清楚的安全边界

动态码生成器本质上是一把“临时钥匙”。你自己写一个用来学习、自用、测试完全没问题,但要是别人拿它窃取账号、批量撞库,那就是违法行为。作为技术人员,至少应该做到两点:不传播可用于非法破解的成品源码,不在代码里留可被滥用的后门。

我见过有些项目打着“动态码生成”的旗号,实际上把用户输入的密钥偷偷上传到服务器收集,这种源码本身就是恶意软件。自己写一遍,至少能看清算法细节,知道哪些参数会在什么时候被读取,不容易被别人的“成品工具”算计。


最后分享一个我自己的小习惯:在密钥配置文件里额外保存一个“偏移量”字段,比如当前系统时间比真实时间快3秒,就在配置里记个+3。这个字段在算法计算时加进去,能解决大部分因为电脑时间不准导致的不一致问题。另外,做GUI版本的时候,把“复制到剪贴板”按钮放在最大最顺手的位置,因为动态码30秒后就失效了,用户没有时间去找菜单。这两个细节看起来小,实际使用时能省下不少麻烦。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 12:33:40

Keras回调函数实战:模型保存、早停与学习率调整全解析

1. 回调函数到底是什么,为什么训练离不开它用Keras做模型训练,model.fit一跑起来,控制台就开始刷loss和accuracy。看起来训练是自动的,但实际上一堆问题不会自己解决:模型保存下来了吗?学习率还合适吗&…

作者头像 李华
网站建设 2026/10/14 12:33:06

OpenClaw:水产活体状态映射与动态履约系统解析

1. 项目概述:一只“小龙虾”背后的产业真相,远比你想象的更硬核最近刷到“OpenClaw”这个代号,很多人第一反应是——又一个蹭热点的营销梗?但我在某食品供应链实验室参与过三轮夏季水产流通压力测试后发现,这根本不是什…

作者头像 李华
网站建设 2026/10/14 12:32:54

Java应用Windows打包实战:Launch4j原理与优化

1. 为什么Java应用需要专门的Windows打包方案 Java应用跨平台特性带来的一个副作用就是用户体验问题。当我们在Windows环境下双击一个jar文件时,通常会看到命令行窗口一闪而过,或者需要用户手动配置Java环境变量。这种体验对于普通Windows用户来说极不友…

作者头像 李华
网站建设 2026/10/14 12:32:23

Kettle ETL工具入门:从环境配置到数据同步实战

简介:这份PDF资料面向数据集成与ETL初学者及需要搭建数据管道的数据工程师,系统讲解开源ETL工具Kettle(Pentaho Data Integration,PDI)的下载、安装与基础配置方法,帮助读者在Windows、Linux或macOS环境下快…

作者头像 李华
网站建设 2026/10/14 12:31:55

Spring Boot+Vue航空订票系统设计与高并发解决方案

1. 项目背景与核心价值 航空订票管理系统作为现代民航业的核心业务支撑平台,其Web化转型已成为行业标配。这个毕设项目抓住了三个关键趋势:首先是传统航空票务系统向B/S架构迁移的技术浪潮,其次是高校计算机专业对实战型毕业设计的迫切需求&a…

作者头像 李华
网站建设 2026/10/14 12:31:54

Meta PAP协议:为AI Agent建立统一个人协议层

1. Meta的PAP协议到底想解决什么问题这几年但凡你稍微留意过AI圈,一定见过各式各样的“Agent框架”——AutoGPT、LangChain、MetaGPT,再到各种定制化智能体,隔三差五就出一个新玩法。我自己也前后折腾了不少,框架换了一拨又一拨&a…

作者头像 李华