「口令要够复杂」「ID 用 UUID 就行」这两句话流传得最广,也误导得最多。前者让很多人把 8 位的 P@ssw0rd! 当成强口令,后者让很多系统把 UUID 直接当成密码重置链接里的凭证。这两类问题都能用一个统一的尺子量出来:这串字符里到底有多少不可预测的信息。
一、强度看熵,不看符号花样
熵是衡量「猜中它平均需要试多少次」的指标,单位是比特。对于每一位都从同一个字符池里独立随机取的口令,它的计算非常直接:
熵 = 长度 × log2(字符池大小)
随机密码生成四类字符全开时的字符池是 88 个:26 个大写、26 个小写、10 个数字、26 个特殊符号。log2(88) 约等于 6.46,所以每多一位口令就多约 6.46 比特。16 位就是约 103 比特,页面上「熵值」那一行显示的就是这个数。
有了公式就能算出「加长度」和「加字符类型」哪个更划算,答案会让很多人意外:
- 16 位、四类字符全开:字符池 88,约 103 比特。
- 16 位、只用大小写字母和数字:字符池 62,
log2(62)约 5.95,约 95 比特。少了 8 比特。 - 18 位、只用大小写字母和数字:约 107 比特,反而比上面那个带符号的 16 位口令更强。
也就是说,加两位普通字符的收益,超过把整个符号集加进来的收益。原因是字符池对熵的贡献是对数的,从 62 扩到 88 只让每一位多 0.51 比特,而多一位直接多 5.95 比特。这就是「长度优先」的可计算依据,不是一句口号。
还有两个参数值得说清楚。勾选「排除相似字符」会去掉 0、O、1、l、I 五个字符,字符池从 88 降到 83,16 位口令的熵从 103 比特掉到 102 比特,损失可以忽略;换来的是手抄和口述时不会认错,需要打印或电话告知口令时应该勾上。
至于强度条,请把它当成配置提示而不是安全结论。它的算法是长度乘 3(上限 40 分)加上字符类型数乘 15,再加字符池大小乘 0.5(上限 20 分)。这意味着一个只有 4 位但四类字符全开的口令也会拿到 92 分并显示「非常强」,而它的真实熵只有 25 比特,本机几秒钟就能穷举完。判断强弱请只看熵值那一行:日常网站账号至少 70 比特,密码管理器主密码、服务器凭证、加密文件口令建议 100 比特以上。
二、真正把口令毁掉的是可预测性
上面的公式有一个前提:每一位都是真随机取的。人编的口令不满足这个前提,所以「按长度算出来的熵」对它完全不适用。
Tom19900312! 有 12 位、包含四类字符,按公式算 77 比特,但它的实际强度接近于零。攻击者不会逐字符穷举,而是用规则化的字典攻击:常见英文名与拼音姓名表、连续八位的生日、手机号段、公司名与产品名,再叠加几条固定变换规则,比如首字母大写、末尾加感叹号或年份、a 换成 @、o 换成 0、i 换成 1、s 换成 $。这些替换在密码破解工具里都是内置规则,加上它们只让候选集大几倍,而不是大几百万倍。
同理必须避开的还有键盘序。qwerty、1qaz2wsx、zxcvbnm、1q2w3e4r 看起来毫无规律,实际上是所有字典里的头部条目,因为太多人用过同一个手法。
所以口令只有两种正确来源:由工具随机生成,或者用足够多的随机词组成的长口令(四到六个完全随机挑选的词,不是自己造的句子)。凡是「我想一个自己能记住又很复杂的」,基本都落在字典攻击的射程内。
这里要说清楚一个实现边界:本站的随机密码生成在浏览器端调用的是 Math.random()。它是速度优先的伪随机数生成器,不是密码学安全的随机源,理论上输出序列具有可推断性。浏览器里真正的安全随机接口是 crypto.getRandomValues,它由操作系统的熵池支撑,输出不可预测,这也是密码管理器和 Web 加密库使用的接口。因此实际建议是:本工具生成的口令用于普通网站账号、临时测试账号、需要快速凑一个不重复口令的场景足够;而密码管理器主密码、根账号口令、加密密钥、长期有效的服务凭证,请用密码管理器的生成器或本机命令行工具产出。
三、UUID 的版本差异与隐私影响
UUID生成器提供 v1、v3、v4、v5 四个版本,可以切换大小写、是否带花括号、是否保留连字符,单次生成 1 到 1000 个,并导出为文本、JSON 或 CSV。选版本之前先知道它们的生成逻辑差在哪。
- v4 是完全随机。除了固定的版本位和变体位,其余全是随机内容,标准实现有 122 个随机比特。它不携带任何关于生成时间、生成机器的信息,是对外暴露 ID 时的默认选择。
- v1 基于时间戳与节点标识。标准 v1 的节点段直接使用网卡 MAC 地址,这带来两个隐私后果:任何拿到这个 ID 的人都能算出它的生成时刻(精度到 100 纳秒级),并且能得到生成机器的网卡标识,从而把不同时间、不同业务里生成的 ID 关联到同一台机器上。历史上有安全事件正是靠文档里的 v1 UUID 反推出作者机器信息的。
- 本站的 v1 是浏览器端的简化实现,节点段用随机十六进制填充。浏览器里的 JavaScript 拿不到网卡 MAC,所以它不会泄露 MAC,但时间戳部分仍然能看出生成时刻,批量生成时也能看出先后顺序。如果你的系统会对 UUID 做严格的版本与格式校验,v1 请用后端库(Node 的
uuid、Python 的uuid)生成。 - v3 与 v5 在本站是简化实现,会回落到随机结果。真正的 v3 和 v5 是「命名空间 + 名字」经 MD5 或 SHA-1 哈希得到的,特点是可复现:同一个命名空间加同一个名字,永远得到同一个 UUID,适合把外部系统的稳定标识映射成 UUID。这个可复现性本站的实现不具备,需要它请用后端库。
选择上可以简化成一句:对外暴露、写进 URL、发给第三方的用 v4;只在库内做主键、需要按时间大致有序的可以考虑 v1 或数据库自带的有序 ID,但要接受它暴露时间信息。
四、UUID 不是安全令牌
这是最常见也最危险的误用:把 UUID 放进密码重置链接、邀请链接、文件下载地址,然后认为「别人猜不到就等于安全」。
UUID 的设计目标是唯一,不是不可猜。这两个目标对随机源的要求完全不同:保证唯一只需要碰撞概率足够低,用一个普通伪随机数就能做到;保证不可猜需要攻击者即使看到大量历史输出也无法推断下一个,这要求密码学安全的随机源。本站 v4 用的是 Math.random(),明确不满足后者。v1 更不能用于这个场景,它的时间戳部分是可预测的,知道大致生成时间就能把搜索空间压缩到很小。
安全令牌的正确做法是三条一起做:
- 用密码学安全随机源生成足够长的随机串,服务端至少 128 比特随机内容,通常表示为 32 位十六进制或 22 位以上的 URL-safe Base64。
- 服务端存储并校验,令牌本身不承载权限信息,权限查库确定;一次性令牌用过即失效。
- 设置过期时间,密码重置链接通常 15 到 30 分钟,且重置成功后立刻作废同一账号的其他未用令牌。
顺带一句:把令牌放在 URL 里意味着它会进入浏览器历史、Referer 头和服务端访问日志,能放在请求体或头部就不要放在路径和查询串上。
五、口令进数据库之前必须过一遍慢哈希
明文存口令的后果不只限于你的系统。用户在别处复用了同一个口令,你这一次泄露会变成他所有账号的失守,这就是撞库攻击的燃料来源。所以口令入库前必须变成不可逆的形式。
但「不可逆」这个条件远远不够。用 MD5、SHA-256 这类通用哈希存口令仍然是错的,即使加了盐。原因在于它们的设计目标是快:现代显卡每秒能算数十亿次 SHA-256,拿到泄露的哈希表后,几十亿条常见口令跑一遍只是几分钟的事。加盐能让预计算的彩虹表失效、让两个用相同口令的用户存出不同摘要,但挡不住针对单个用户的逐个尝试。
正确选择是专门为口令设计的慢哈希:bcrypt、scrypt、Argon2。它们的核心不是更强的压缩函数,而是可调的工作因子,让单次校验故意消耗几十到几百毫秒的 CPU 甚至大量内存。对正常登录来说这点延迟无感,对批量爆破来说速度直接掉了几个数量级。scrypt 和 Argon2 还额外要求大量内存,专门用来削弱显卡与专用硬件的并行优势。
盐的角色也值得说清:它是公开的、每条记录一份的随机值,明文存在数据库里没有问题,泄露了也不算事故。它和 HMAC 的密钥完全不是一回事,后者必须保密,一旦泄露攻击者就能伪造任意签名。
需要一次看清同一段输入在多种算法下的摘要,或者对接开放平台接口时要调试 HMAC 签名,用哈希与HMAC计算最省事:九种算法同时输出,可切换十六进制大小写或 Base64,HMAC 的密钥还能按 UTF-8、Base64、Hex 三种方式解读,签名对不上时逐一切换比对通常几分钟就能定位。但要记住那个页面自己写着的边界:存储口令请用 bcrypt 或 Argon2,不要用它里面的任何算法。另外,服务端比对摘要与签名时应使用恒定时间比较函数,逐字节短路返回会通过响应耗时泄露信息。
常见问题
我的口令里有大小写、数字和符号,为什么还说不安全?
因为「包含四类字符」和「不可预测」是两件事。强度由不可预测的信息量决定,而不是由字符种类决定。P@ssw0rd! 四类齐全,但它是字典里的头部条目加两条常见替换规则,破解工具几秒钟就会试到。反过来,18 位纯小写字母的随机串没有任何符号,熵约 84 比特,比它安全无数倍。判断标准只有一条:这串字符是随机产生的,还是你想出来的。
熵要多少比特才算够? 按用途分档比较实际。普通网站账号 70 比特以上,对应四类字符全开约 11 位、纯字母数字约 12 位;重要账号、加密文件口令、服务器凭证建议 100 比特以上,对应四类字符 16 位、纯字母数字 17 位;密码管理器主密码建议 128 比特以上。注意这些数字只对随机生成的口令有意义。
UUID 会重复吗?可以直接当数据库主键吗?
标准 v4 的 122 个随机比特让碰撞概率低到工程上可以忽略,但前提是随机源合格;用 Math.random() 的实现在超大批量下的实际碰撞风险高于理论值,所以关键系统的主键请用后端库或数据库内置函数生成。作为主键还有性能问题要权衡:完全随机的 UUID 写入时会打散索引局部性,在 B+ 树上造成页分裂,大表插入性能会明显低于自增 ID。常见折中是用有序的 UUID 变体或雪花 ID,对外再暴露一个随机 ID。
已经明文存了口令,现在怎么补救?
不要试图把明文批量哈希后就宣布安全了,那些口令已经算泄露过一次。可行的迁移路径是:立刻停止写入明文,改为写入 bcrypt 或 Argon2;对现有记录,在用户下次成功登录时用他输入的明文重新生成慢哈希并覆盖旧字段;给所有未迁移的账号设置一个过渡期,期满仍未登录的强制走一次重置流程;同时清理日志、备份和监控里可能落下的明文口令。