☰
Python实战:从密码生成到模拟破解的安全闭环
2026/10/21 18:06:32 网站建设 项目流程

如果你去问十个开发同学,“你在各种网站和内部系统上,密码是怎么设置的?”我猜至少有一半人会沉默三秒,然后报出一个生日加名字的组合。这类密码在攻击者眼里基本等于白送。我自己也是从这种状态过来的,直到有一年给某个内部工具做安全加固,才真正意识到:与其天天盯着防火墙和漏洞补丁,不如先把“密码”这个东西本身的问题解决掉。所以我用 Python 内置的 random 模块和 hashlib 模块写了一套小工具,既能生成高强度的随机密码,又能从攻击者的视角去模拟破解这些密码。这篇文章是我整个项目的完整复盘,代码都在下面,直接复制就能跑。它适合两种人:一种是刚学 Python、想找一个贴近真实场景的小项目练手;另一种是开发者或技术管理者,想把“密码安全”从一句口号变成可以量化的工程实践。

这个项目的核心思路特别简单,就两步:先生成,再破解。生成是为了让密码本身足够硬,破解是为了检验到底有多硬。一套工具把“设计”和“验证”闭环串起来,你才会真正理解为什么有人专门写几十行代码去破解一个密码,而不仅仅是因为“网上说要复杂”。

1. 为什么密码强度是第一道防线

先聊一个常识性但总被忽略的问题:安全事件里,真正被盗号、被拖库的系统中,有多少是因为加密算法被正面攻破的?极少。绝大多数事故的薄弱点,都是弱口令。攻击者根本不需要去破解 AES 或者 RSA,只需要在登录框里试几个常见密码,比如生日、手机号、公司名加年份。你系统做得再牢固,密码是“zhangsan123”,那前面所有防御都白搭。

这就是为什么密码被称为第一道防线:它是用户和数据之间最简单、也最直接的认证凭证。密码本身不够随机、不够长,后面的身份管理和访问控制做得再好,都相当于给安全屋装了一把插销锁。

1.1 密码熵:衡量密码强度的数学尺子

密码强度在密码学里有一个量化指标,叫“熵”(Entropy),单位是比特(bit)。听起来复杂,其实就一个公式:熵 = 密码长度 × log2(字符集大小)。它表示攻击者用穷举方法猜中密码所需的平均尝试次数,换算成二进制信息的比特数。比特数越大,猜中需要尝试的次数就呈指数级上升。

我举个例子,4位纯数字密码,字符集是10个数字,长度是4,它的熵就是4 × log2(10) ≈ 13.3 bit,总共只有一万种组合,手工在登录框里试都能试完。而一个8位大小写字母加数字加符号的密码,字符集大约94个,熵是8 × log2(94) ≈ 52.4 bit,组合数量超过4.9×10^15。这中间的差距,不是“多了几个字符”这么简单,而是从“散步就能走完”到“光速也要飞几万年”的指数级鸿沟。

1.2 为什么必须放弃“自己脑补密码”

很多人觉得,自己想的密码已经很复杂了,比如“Lov3!@Family_2024”,看起来有大小写、有数字、有符号。但在攻击者的字典里,这类密码的变种早就被收录了成百上千次。人脑不擅长随机,我们容易把单词、日期、键盘相邻的按键组合拼在一起,这些模式恰恰是攻击者优先尝试的序列。

安全社区有一句被反复引用的共识:攻击者不需要专门破解你的密码,只需要找到一个比你更容易破解的密码。这句话听起来刺耳,但很真实。所以我会把密码生成完全交给程序,而不是凭感觉拍脑袋。程序不会“偏心”某个数字或单词,它对每一个字符的选择概率完全相同,这样才能产生真正均匀分布的随机密码。

1.3 生成与破解双通道的设计思路

只做生成器其实意义不大,因为没人知道生成的密码到底防不防得住攻击。所以我给这个项目多设计了一个“攻击者视角”:用一个离线模拟破解程序,去跑刚生成的密码。这个思路很像压力测试——你觉得自己写的接口很稳,那就上压测工具打一打,看它会不会挂;你觉得自己生成的密码很强,那就用字典攻击和暴力破解去撞一撞,看它多久会被攻破。

而且这个闭环设计还有一个额外的好处:它能反向帮助你理解存储端的防御。生成出的密码最终会存到服务器或本地数据库里,如果存储方式不对——比如明文存储、只做一次简单哈希——再强的密码也会因为存储泄露而报废。所以后面我还会用 hashlib 做加盐哈希存储,把整个链条补齐。

2. 用 random 模块生成安全密码:完整实现

这一节进入实战。我用 Python 内置的 random 模块封装了一个密码生成函数,它的完整代码是这样的:

import random import string CONFUSING_CHARS = set("O0oIl1|'`\" \\") # 易混淆字符 def generate_password(length=16, use_upper=True, use_lower=True, use_digits=True, use_special=True, exclude_confusing=True) -> str: pools = [] if use_upper: pools.append(string.ascii_uppercase) if use_lower: pools.append(string.ascii_lowercase) if use_digits: pools.append(string.digits) if use_special: pools.append(string.punctuation) if not pools: raise ValueError("至少需要启用一种字符类型") # 合并所有字符集,并统一去掉容易看错的字符 pool = "".join(pools) if exclude_confusing: pool = "".join(ch for ch in pool if ch not in CONFUSING_CHARS) if not pool: raise ValueError("排除易混淆字符后字符集为空,请调整参数") # random.SystemRandom 是基于系统熵源的密码学安全随机数 sys_random = random.SystemRandom() # 先从完整池子里随机取 length 个字符 password = [sys_random.choice(pool) for _ in range(length)] # 强制每种已启用的字符类型至少出现一次 for i, sub_pool in enumerate(pools): if not any(ch in sub_pool for ch in password): pos = sys_random.randrange(length) password[pos] = sys_random.choice(sub_pool) # 洗牌打乱,避免前面强制替换产生的规律分布 sys_random.shuffle(password) return "".join(password)

这段代码短,但里面藏了不少细节。

2.1 为什么用 random.SystemRandom 而不是直接 random.choice

这是很多人踩过的坑。直接使用random.choice,底层是梅森旋转算法(Mersenne Twister),它生成的是“伪随机数”,也就是说,只要知道了生成器的内部状态和种子,就能预测后续输出。这对模拟、跑数据来说足够用,但对密码生成来说是致命的。攻击者如果知道你生成密码的时间种子,理论上可以复现你生成的每一个字符。

所以这里我强调的是“密码学安全随机”:在 Python 中,random.SystemRandom直接从操作系统熵源读取随机性,比如 Linux 下的/dev/urandom、Windows 下的 CryptGenRandom。它不可预测,适合用于密码这类安全场景。实际上 Python 还提供了secrets模块,底层就是SystemRandom,我在真正的生产脚本里会优先用secrets.choice,它的语义更明确。但标题既然点名了 random 模块,我们就先把 random 的安全姿势讲清楚。

提示:学习阶段用random.SystemRandom没问题,但如果写正式工具,建议直接用import secrets,可读性和安全性都更清晰。

2.2 易混淆字符过滤与字符集强制覆盖

生成密码时有个很实际的问题:用户拿到密码后,需要手输进系统。如果密码里同时出现 “O” 和 “0”、“l” 和 “I”,很容易弄混,尤其在某些字体下几乎一模一样。很多密码生成器没有考虑这一点,生成出来的密码好看但不“好用”。所以我默认开启exclude_confusing,把这类视觉歧义字符全部剔除。

字符集强制覆盖则解决另一个问题:完全随机抽取时,运气差可能生成一个全是数字的密码,或者全是小写字母的密码。这种密码虽然也是随机产生的,但会被不少网站自带的“弱密码检测”直接判死。解决方式很简单:生成后主动检查每一类字符是否至少出现一次,缺哪类就在随机位置插入一个那一类的随机字符,最后再洗一次牌。

2.3 熵校验与密码强度报告

生成完密码之后,我建议顺手把熵也算出来。这不是形式主义,而是让生成对象变成可量化的报告。我在项目里加入了一个小的熵计算函数:

import math def password_entropy(length: int, pool_size: int) -> float: return round(length * math.log2(pool_size), 2)

比如生成一个14位密码、字符集约90个(排除混淆字符后),熵大约就是14 * log2(90) ≈ 90.7 bit。这个数字超过 80 bit,足以抵御目前任何现实条件的离线穷举攻击。经验上:个人普通系统密码至少 60 bit,高权限账号建议 80 bit 以上,数据库备份密钥、服务器私钥这类不建议依赖人类记忆的凭据,直接上 128 bit 以上。

3. hashlib 模块:密码存储的正确姿势

密码生成出来之后,紧接着的问题就是:密码怎么存?这片很多项目都踩过坑——直接用数据库明文存,或者只做一次 SHA256 哈希。明文存储就不用说了,数据库泄露等于密码裸奔;而“只做一次简单哈希”则是初学安全的人最容易掉进去的坑,原因我在下面详细拆。

3.1 哈希函数的三条关键特性

哈希函数(Hash Function)在 Python 里通过 hashlib 模块提供,常见的 SHA-256、SHA-512 都在里面。我用三个关键词总结它和安全存储相关的特性:

  • 确定性:同一个输入,每一次计算得到的哈希值完全一样。
  • 单向性:从哈希值反推原文,计算上不可行。
  • 雪崩效应:输入稍微改动一个字符,输出的哈希值就面目全非,且没有肉眼可辨的关联性。

我们可以来验证一下雪崩效应:

import hashlib a = hashlib.sha256(b"password").hexdigest() b = hashlib.sha256(b"passwore").hexdigest() print(a) print(b) print("两者是否相同:", a == b)

你会在终端里看到两串完全不同的 64 位十六进制字符串。“单向性”意味着即使哈希值泄露,攻击者也几乎不可能把它还原成原始密码。但注意,我这里用了“几乎”,因为如果密码本身太短、太常见,攻击者可以通过枚举常见密码来反查哈希,这就是后面会讲的字典攻击。

3.2 从明文到简单哈希:初学者的第一次升级

如果你现在还在用明文存密码,至少先升到简单哈希。但简单哈希并不是终点:

import hashlib, os def hash_without_salt(password: str) -> str: return hashlib.sha256(password.encode("utf-8")).hexdigest()

这样做的最大问题在于:相同的密码,会得到完全相同的哈希值。如果数据库泄露,攻击者把两个用户的哈希值一对比,就能知道这两个人用的是同一个密码;甚至可以拿一份预计算好的“常见密码→哈希值”对照表(专业术语叫彩虹表)去反向匹配,几个小时内就能把数据库里的弱口令筛一遍。

3.3 加盐哈希:打散整齐划一的目标

“加盐”(Salt)就是在原始密码后面拼上一段随机数据,然后再去算哈希。这个“盐”和密码一样,也是随机生成的。于是同一个密码,因为盐不同,最终得到的哈希值完全不同,攻击者手里那份彩虹表直接作废。

我把加盐哈希和密码校验封装成两个函数:

import hashlib import hmac import os def hash_password(password: str, salt: bytes = None, iterations: int = 100_000): if salt is None: salt = os.urandom(16) # 每位用户、每次注册都用独立的 16 字节随机盐 digest = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, iterations, dklen=32 ) return salt, digest def verify_password(password: str, salt: bytes, expected_digest: bytes, iterations: int = 100_000) -> bool: _, actual_digest = hash_password(password, salt, iterations) # 使用固定时间比较,避免通过耗时差异猜测密码长度 return hmac.compare_digest(actual_digest, expected_digest)

代码里有个值得注意的点:校验时重新用相同的盐和新输入的密码计算一遍哈希,再去对比数据库里保存的哈希。整个过程不需要保存用户的原始密码,只需要保存盐和最终哈希,就能完成登录验证。这也意味着:即使数据库泄露,攻击者拿到的也只是不可逆的哈希,而用户真实密码不会直接暴露。

3.4 从 sha256 到 PBKDF2 慢哈希

上面代码里的hashlib.pbkdf2_hmac是一个“慢哈希”算法,它和普通 SHA256 的区别在于:PBKDF2 会把“加盐+哈希”反复迭代很多次(代码里的iterations=100_000),每次登录验证都要跑十万次计算。对正常用户来说,认证一次多花几十毫秒到几百毫秒,完全无感;但对攻击者来说,这意味着每秒尝试次数从“数百万次”骤降到“几千次”,代价直接拉高了几个数量级。

Python 的 hashlib 模块直接内置了这个算法,不需要装任何第三方包。如果你想要更现代的选择,还可以看 scrypt、argon2 这类内存硬算法,但作为一个学习项目,hashlib.pbkdf2_hmac已经是很大的进步了。

注意:加盐解决了“所有人密码统一哈希”和“彩虹表”的问题,慢哈希解决的是“攻击者尝试速度太快”的问题。两者必须同时用,缺一不可。

4. 模拟破解:从攻击者视角检验密码强度

生成密码、加盐存储,这两步做完,工具已经能解决“如何安全地保存密码”的问题。但我要再进一步,把攻击者的“作业抄一遍”:一个离线破解程序,拿着密码哈希文件,会怎么尝试还原你的密码?我在项目里写了两种最常见的攻击方式:字典攻击和暴力破解。

4.1 字典攻击模拟:不加盐时为何一打一个准

字典攻击的思路很简单:攻击者准备一个大型“常见密码列表”,把列表里的每个口令按同样的哈希算法算一遍,然后逐个和目标哈希比对。如果用户的密码恰好命中了列表中的某个口令,攻击者就能在极短时间内“破解”出来。

我把这个过程模拟了一下,使用一组虚构的小型常见口令列表:

import hashlib wordlist = ["123456", "password", "admin", "P@ssw0rd", "qwerty", "abc123"] def dict_attack_no_salt(target_hash: str, wordlist) -> str | None: for word in wordlist: if hashlib.sha256(word.encode("utf-8")).hexdigest() == target_hash: return word return None

实测下来,如果目标密码是列表里那个P@ssw0rd,扫描六条记录就能直接命中。真实场景中,攻击者的字典不是六条,而是几百GB的泄露密码库,里面包含了数百亿条真实泄露的口令。只要用户用了其中的任何一个,无论这个密码看起来多“花哨”,都难逃一劫。

4.2 暴力破解与耗时估算

如果字典找不到,攻击者进入第二种打法:暴力尝试所有可能组合。这个思路在代码上很简单,就是嵌套循环生成字符集的所有排列,但对密码“长度”非常敏感。我写了一个简化版演示:

import itertools import string import time def brute_force_demo(length: int = 4, charset: str = string.digits): start = time.time() total = len(charset) ** length checked = 0 for combo in itertools.product(charset, repeat=length): guess = "".join(combo) checked += 1 # 演示场景:命中最后一个组合 if checked == total: cost = time.time() - start return guess, cost return "", 0.0

在只含数字的 4 位密码上,这个程序能在几十毫秒内跑完一万种组合。得到的基准速率可以用来做数量级推演。方便起见,我按“每秒钟可尝试 10 万次”为基线,估算常见密码规模:

密码规格组合数量(约)单 CPU 破解时间(约)等级
6 位纯数字10^610 秒极弱
8 位小写字母2.1×10^1124 天弱
8 位大小写+数字2.2×10^1470 年中等
8 位全字符6.1×10^151900 年中上
12 位全字符4.8×10^231.5 亿年强
16 位全字符3.7×10^31约 10^19 年极强

现实中的攻击者不会只用一台 CPU 去跑,他们会用多显卡并行,十几张显卡可以把计算速度提升 1000~5000 倍。所以“8 位大小写+数字”这套组合,在 GPU 集群面前可能是几天到几周就能跑完的量级。这样一对比,为什么密码圈普遍推荐至少 12 位、最好 16 位,原因就很清晰了:位数每增加一位,破解时间就翻一个数量级。

4.3 加盐后的破解成本变化

我在第 3 节强调过,加盐不是为了让你密码变得更难猜,而是为了破坏“一次暴力计算、批量验证全体用户”的规模效应。这一点在模拟里看得很明显。

如果攻击者手里有一份哈希文件,里面是 10 万个用户的不加盐 SHA256 哈希,那么他可以一次性把所有哈希读入内存,每尝试一个候选密码,就同时和 10 万个哈希比对一遍。这时候破解整个数据库的性能开销和在破解单个用户时基本一样。但有了随机盐后,每个用户的哈希都经过不同的盐运算,攻击者每尝试一个候选密码,都必须针对某一个用户单独计算一次,无法批量复用。也就是说:破解 10 万个用户的时间,接近破解一个用户时间的 10 万倍。

关于“盐到底能不能防字典攻击”,这里也需要说准确:盐不能防针对单个用户的定向字典攻击。攻击者拿到了某个用户的盐和哈希,仍然可以对这一个用户跑字典。但配合 PBKDF2 这种慢哈希,单用户遍历十万条口令的计算成本也会高到让人不想继续。我把这两种情况都封装了出来:

def salted_dict_crack(salt, target_digest, wordlist, iterations=100_000): for word in wordlist: _, d = hash_password(word, salt, iterations) if hmac.compare_digest(d, target_digest): return word return None

如果你把iterations从100_000降到1,这个函数跑字典很快,弱口令一秒内命中;但如果你是攻击者,面对一个正常配置了高迭代的服务,只为了试一个候选口令就要算十万次 PBKDF2,整本字典跑完的时间会拉到数十倍甚至数千倍。不上 GPU 集群,基本没人愿意逐个用户去磨。

4.4 关于破解效率的几点现实认知

在做模拟破解的时候,有几点现实情况必须心里有数。第一,真实的破解工具一定会用多进程、多线程或 GPU 并行计算,单机 Python 程序的计时只能给一个下限参考。第二,攻击者的字典攻击优先级永远高于暴力破解,因为成本低、命中率高;如果你的密码不在常见口令列表里,暴力破解才轮到考虑。第三,很多所谓“破解成功”并不是攻击者真拿到了哈希慢慢算,而是用户在别的平台也用了同一个密码,其他平台泄露后直接拿过来撞库。这就是为什么我会建议,哪怕密码生成得再强,也不要跨平台重复使用同一个密码。一个密码管理器,加上一条“每个平台随机生成一个不同密码”的铁律,往往比单纯把密码定成十六位更有效。

5. 完整项目源码与一键演示

前面几段把各个函数拆开讲了,这一步把它们放进一个完整项目中。我列一下项目结构:不依赖任何第三方库,Python 3.8+ 直接运行。

5.1 项目结构与函数说明

整个工具我合并成了一个文件,内部按职责分成几个类方法:

import hashlib import hmac import itertools import math import os import random import string import time class PasswordTool: CONFUSING_CHARS = set("O0oIl1|'`\" \\") @staticmethod def generate_password(length=16, use_upper=True, use_lower=True, use_digits=True, use_special=True, exclude_confusing=True) -> str: pools = [] if use_upper: pools.append(string.ascii_uppercase) if use_lower: pools.append(string.ascii_lowercase) if use_digits: pools.append(string.digits) if use_special: pools.append(string.punctuation) if not pools: raise ValueError("至少需要启用一种字符类型") pool = "".join(pools) if exclude_confusing: pool = "".join(ch for ch in pool if ch not in PasswordTool.CONFUSING_CHARS) if not pool: raise ValueError("排除易混淆字符后字符集为空") sys_random = random.SystemRandom() password = [sys_random.choice(pool) for _ in range(length)] for i, sub_pool in enumerate(pools): if not any(ch in sub_pool for ch in password): pos = sys_random.randrange(length) password[pos] = sys_random.choice(sub_pool) sys_random.shuffle(password) return "".join(password) @staticmethod def password_entropy(length, pool_size): return round(length * math.log2(pool_size), 2) @staticmethod def hash_password(password, salt=None, iterations=100_000): if salt is None: salt = os.urandom(16) digest = hashlib.pbkdf2_hmac( "sha256", password.encode("utf-8"), salt, iterations, dklen=32, ) return salt, digest @staticmethod def verify_password(password, salt, expected_digest, iterations=100_000): _, actual_digest = PasswordTool.hash_password(password, salt, iterations) return hmac.compare_digest(actual_digest, expected_digest) @staticmethod def demo_dict_attack_no_salt(target_hash, wordlist): for word in wordlist: if hashlib.sha256(word.encode("utf-8")).hexdigest() == target_hash: return word return None @staticmethod def demo_dict_attack_with_salt(salt, target_digest, wordlist, iterations=100_000): for word in wordlist: _, d = PasswordTool.hash_password(word, salt, iterations) if hmac.compare_digest(d, target_digest): return word return None @staticmethod def brute_force_demo(length=4, charset=string.digits): total = len(charset) ** length start = time.time() for idx, combo in enumerate(itertools.product(charset, repeat=length), start=1): guess = "".join(combo) if idx == total: return guess, time.time() - start return "", 0.0 def main(): print("=== 1. 生成安全密码 ===") pwd = PasswordTool.generate_password(length=16) print("密码:", pwd) print("熵值:约", PasswordTool.password_entropy(16, 90), "bit") print("\n=== 2. 加盐哈希存储与校验 ===") salt, digest = PasswordTool.hash_password(pwd) print("盐(hex):", salt.hex()) print("哈希(hex):", digest.hex()) print("正确密码校验:", PasswordTool.verify_password(pwd, salt, digest)) print("错误密码校验:", PasswordTool.verify_password(pwd + "x", salt, digest)) print("\n=== 3. 字典攻击演示 ===") wordlist = ["123456", "password", "admin", "P@ssw0rd", "qwerty", "abc123"] weak_pwd = "P@ssw0rd" weak_no_salt = hashlib.sha256(weak_pwd.encode("utf-8")).hexdigest() print("不加盐字典攻击目标命中:", PasswordTool.demo_dict_attack_no_salt(weak_no_salt, wordlist)) weak_salt, weak_digest = PasswordTool.hash_password(weak_pwd) print("加盐后同字典攻击结果:", PasswordTool.demo_dict_attack_with_salt(weak_salt, weak_digest, wordlist)) print("\n=== 4. 暴力破解流程演示 ===") guess, cost = PasswordTool.brute_force_demo(4, string.digits) print(f"4位纯数字演示命中: {guess},耗时 {cost:.4f} 秒") if __name__ == "__main__": main()

这个整合版本更倾向“演示”:它不是为了破解某个真实系统,而是为了在本地环境下把生成、存储、校验、攻击四个环节一次性跑通,让你对每个环节产生的中间结果有直观的认知。

5.2 运行流程与典型输出

运行这段代码,你会依次看到四段输出:新生成的 16 位密码和它对应的熵值;加盐哈希和正确/错误密码校验结果;一个不加盐字典攻击命中的案例,以及加盐后同字典攻击失败的结果;最后是 4 位纯数字暴力破解的时间统计。整个脚本在普通笔记本上跑完,通常只需要几秒到几十秒,取决于你的机器性能和 PBKDF2 迭代次数。

我建议你拿到代码后,先不要急着改参数,原样跑一遍;然后把length改成 8、12 分别跑一次,观察熵值变化;再把iterations改成 1000 和 100000 各跑一次,直观感受慢哈希在不同迭代次数下的耗时差。这个过程比看十篇文章都更能建立“指数级”的感觉。

6. 踩过的坑与工程化建议

做这个项目的过程中,我踩过不少坑。很多问题不是代码写不出来,而是写出来之后才发现思路错了。挑几个有代表性的分享下,希望你能避开。

6.1 随机数选型不当:看似随机,实则可预测

我第一次写密码生成器时,直接用random.choice,跑起来很正常,生成的密码也五花八门,看着没什么毛病。但后来在测试时发现,如果同时开多个进程,因为种子问题,短时间内生成的密码会出现大量重复。虽然 random 模块每次都默认使用系统时间做种子,但在某些环境下,种子相似度极高,导致前几个随机数高度相近。这在密码学场景里是不可接受的。解决方案就是前面说过的random.SystemRandom或secrets模块。结论很简单:涉及安全性的随机数,永远不要依赖算法可预测的伪随机数生成器。

6.2 盐的管理失误:硬编码盐与重复盐

另一个让我记忆深刻的坑,是把“盐”当成一个固定字符串写在配置里。比如salt = "my-secret-salt",这样所有用户都用同一个盐,等于把盐的随机性废掉了。加盐的意义在于“每个用户、每次注册都不同”,盐并不需要保密,它只需要保证唯一性,让攻击者的批量预计算失效。正确的做法是每次调用os.urandom(16)生成新盐,并把盐明文存在数据库里。它的作用不是隐藏,而是打散规律。

6.3 操作层面的三个教训

第一个教训:日志里打过明文密码。调试时为了排查问题,随手把生成的密码print到控制台,结果日志归档后泄露到某个内部共享目录。虽然只是测试环境,但这个习惯非常危险。后来我给自己定了条规定:所有涉及密码明文的地方,只能在交互式终端时刻短暂显示,绝不能写进日志、文件或监控系统。

第二个教训:暴力破解演示忘了设上限。有次拿着真实的 8 位密码去跑暴力破解模拟,脚本没做停机条件,结果跑了一个中午,CPU 直接把风扇吹得嗡嗡响。后来模拟破解程序里都加了“范围上限”和“超时时间”,只做规模可控的演示,不做无限遍历。

第三个教训:混淆了“密码生成”和“密码存储”的边界。项目初期我花了很大精力把生成密码做得很复杂,结果后来发现别的系统丢数据往往不是因为字典碰巧命中,而是因为存储端连基本加盐都没做。所以后来我把这个项目的重心调整为:生成端保证熵值足够,存储端保证加盐慢哈希,两层一起约束,才是完整的密码安全方案。

拿实测数据说点心里话

这个项目做完之后,我自己做了一个小测试:用同一个字符集,生成一个 8 位密码和一个 16 位密码,然后分别跑一遍“估算破解时间”的对比。结果差异非常直观,8 位全字符密码在 GPU 集群面前已经存在被攻破的可能,而 16 位全字符密码的估算时间高到基本没有现实意义。从那以后,我所有需要脑记的账号,都统一走密码管理器随机生成;真正需要人工输入的场景,才用生成器生成一个偏长但可读性较好的口令。

如果你也想动手玩一下,我建议不要只跑代码,而是把它们拆成三个小练习:第一,改length和字符集参数,观察熵值变化;第二,把iterations调低到 1000,再跑一次加盐字典攻击,感受慢哈希的成本影响;第三,自己往 wordlist 里加几条常见的弱口令,然后用你刚生成的密码去测试命中情况。当你真的站在“攻击者”的位置上操作一遍,会发现在密码安全这个话题上,直观的体验比任何文档都更有说服力。

如果再往后扩展,这个项目还能继续加几个方向:把生成的密码按域名/用途整理导出为 CSV;接入secrets模块替代random.SystemRandom;用argparse做成命令行工具;甚至加一个简单的“多用户密码库”管理界面。密码安全不是一个单点问题,而是一个持续迭代的工程实践——我从这个项目里学到最多的一句话是:相对于设计一套牢不可破的加密算法,真正困难的是让每一个普通人都能在日常操作中少犯几个低级错误。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询