DEVTOOLBOX / GUIDES 本篇

生成器决策指南:UUID 版本怎么选、随机源够不够、口令与二维码纠错等级的代价

生成类工具把「产出」做得太便宜,于是判断被跳过了。这篇不介绍按钮,只回答几个必须自己拍板的问题:v4 与 v7 各自在哪里失败、Math.random 与 CSPRNG 的实际差距、哪些口令规则反而在削弱口令、二维码每升一级纠错要付多少容量、测试数据该可复现还是像真的,以及「唯一」这句话背后依赖的四个假设。

内容复核 10 分钟阅读

随机不是一回事:先看熵源,再看输出格式

「随机」在生成器里有两种含义,混用会把安全问题伪装成格式问题。第一种是不可预测:拿到已产出的若干个值,攻击者推不出下一个,也还原不了内部状态。第二种只是不重复:同一次运行、同一张表里不撞,但序列本身可以被复现。判断方法很直接:把这个值交给知道生成方式的对手,他能不能算出下一个?能,就只是第二种。Math.random 属于第二种:V8 是 128 位状态的 xorshift 系算法,Python 的 random 是 MT19937,624 个连续 32 位输出就能还原后续状态。

熵只由随机源与位数决定,输出格式一位也不会增加它。128 位随机按 RFC 4648 写成 Base16 是 32 个字符,写 Base32 是 26 个字符(128 除以 5 向上取整),写 Base64URL 是 22 个字符。三者信息量相同,区别只在传输安全:Base64 的 + 与 / 进 URL 会被转义,Base64URL 换成 - 与 _ 并省掉 =。反过来,任何截断都是不可逆的减法:32 位十六进制砍成 8 位,就只剩 32 位熵。

判据按用途分三档。第一档,值只在本地当键用(缓存、临时文件名、测试记录),PRNG 加时间戳或计数器就够,代价是序列可预测,别让它进对外接口。第二档,值会被对手看到并据此猜下一个(邀请码、订单号、会话标识),必须用 CSPRNG。第三档,值本身就是凭据(重置令牌、API key、一次性码),除 CSPRNG 外还要够长、能一次性作废、服务端只存哈希。最常见的误用是拿第三档当第一档处理:用时间戳拼随机数做重置链接,看着正常,实际可枚举、可预测,泄露后也收不回。

UUID 各版本:为什么 v4 与 v7 失败在不同地方

UUID 是 128 位,规范文本 36 个字符(32 个十六进制加 4 个连字符)。其中 6 位被格式占掉:第 7 字节的高 4 位写版本号,第 8 字节的高 2 位写变体,剩下 122 位才归实现支配。所以「128 位随机」从来不存在,v4 的上限就是 122 位,约 5.3 乘 10 的 36 次方。

v4 与 v7 失败的地方并不重合。v4 在碰撞上几乎不会输——达到 50% 碰撞概率要约 2.7 乘 10 的 18 次方个值——它输在写入模式:整段随机让 B 树索引的每次插入落在随机页,页分裂、缓存命中率下降、索引体积膨胀,表过百万级就能测出差别。v7 把 48 位毫秒时间放在最前面,插入近似追加,代价有三条:生成时刻被永久写进 ID,任何人拿到 ID 就知道记录何时创建、也能反推日增量;同一毫秒内默认无顺序,只有把 12 位 rand_a 当计数器用才有每毫秒 4096 个的单调性;系统时钟回拨会破坏单调,多副本生成还要额外协调。RFC 9562 于 2024 年 5 月发布并取代 RFC 4122,新增 v6、v7、v8 与全 f 的 Max UUID,但没有改变这个取舍。

其余版本各有唯一适用面。v1 用 60 位 100 纳秒时间戳(起点是 1582 年 10 月 15 日)加 14 位 clock_seq 和 48 位节点,节点默认取网卡 MAC,等于把主机标识写进每条记录;v6 只把时间字段重排成大端可排序,泄露照旧。v3 与 v5 是名字派生:命名空间 UUID 加名字做 MD5 或 SHA-1,同一输入永远同一输出,适合把自然键映射成稳定 ID,但它没有任何不可预测性,不能当令牌。不可逆点在时间字段:v4 丢弃的排序信息事后补不回来,v7 写入的生成时刻事后删不掉。

版本随机位排序性主要代价
v4122无序索引随机写
v774按毫秒泄露生成时刻
v10 或 62字段跨段泄露 MAC 地址
v60 或 62按时间仍泄露 MAC 地址
v3 / v50由名字决定完全可预测

五个版本对照:随机位与失败点各不相同

口令生成:长度、字符集,以及那些反而削弱口令的规则

口令强度只有一个可计算的口径:熵等于长度乘以字符集大小的以 2 为底的对数。62 个字符(大小写加数字)取 12 位是 71.5 位,取 16 位是 95.3 位;换成 94 个可打印 ASCII 字符取 12 位是 78.6 位——多出的 7.1 位抵不上把长度从 12 加到 14。结论很硬:长度是线性收益,字符集是对数收益,先加长再谈复杂度。

那些「必须包含」的规则通常在帮倒忙。NIST SP 800-63B 要求验证方不得施加组合规则、不得强制定期更换,因为实测中它们把用户推向 Password1! 这类固定形状:首字母大写、尾部补数字和符号。攻击字典与规则集本就覆盖大小写变换、a 换 @、o 换 0,替换技巧的边际收益接近零。同一份规范还要求允许至少 64 个字符、允许空格与 Unicode,并必须比对已泄露口令清单做拦截——这比任何复杂度规则都有效。

生成之后仍有几处会丢信息。bcrypt 只处理前 72 个字节,超出部分静默丢弃:UTF-8 下一个汉字 3 字节,也就是第 24 个汉字之后的内容不再参与校验;部分实现遇到 NUL 字节还会提前截断。首尾空格是合法字符,却常被表单 trim、被导入流程吃掉,同样不可逆。要人记住就换词表法:EFF 长词表 7776 个词,每词 12.9 位,6 词约 77.5 位,强于 12 位随机串且能背。

判据按使用者分:机器用的密钥走 32 位以上随机串;人记的主口令走 5 到 6 个词;一次性码可以短,但必须绑定失败上限与过期时间,否则 6 位数字只有 10 的 6 次方种组合,无速率限制时几小时就穷举完。

方案熵代价
12 位,62 字符集71.5 位手机键盘难输入
16 位,62 字符集95.3 位基本需密码管理器
8 位,强制四类字符约 52 位规则逼出固定模式
6 词,7776 词词表77.5 位依赖词表单独保密

同一个「看起来很强」的印象,背后差着 40 位以上

二维码:纠错等级与每一级的代价

二维码的容量与抗损是一对反向指标。ISO/IEC 18004 定义了 40 个版本,版本 1 是 21 乘 21 个模块,每加一版边长加 4,版本 40 是 177 乘 177。纠错分 L、M、Q、H 四级,分别能恢复约 7%、15%、25%、30% 的码字。这四级不是叠加在容量之上的附赠品,而是从同一批码字里划走一部分去做冗余。

代价可以精确算。版本 40 的字节模式下,L 级能放 2953 字节,M 级 2331,Q 级 1663,H 级 1273——从 L 升到 H,可用容量只剩 43%(1273 除以 2953)。同样的内容升一级往往要跳版本;版本越高模块越多,固定印刷尺寸下单个模块变小,解码信噪比下降。按面积折算,容量掉到 43% 相当于模块边长缩到约 0.66 倍(0.43 的平方根),每模块像素少了三分之一。所以「调到最高纠错更保险」是错的:它可能把一个 3 厘米能扫的码,变成扫不出的码。

选级判据按损耗场景分:屏幕显示、近距离、纯电子渠道用 M;印刷品、易磨损标签、曲面、户外、需要隔塑料膜扫,用 Q;要在码面中间叠 logo,必须 H,且遮挡面积控制在两成上下,别压到三个角上的定位图案。内容接近容量上限时,正确动作是缩短内容(换短链、砍掉跟踪参数),而不是把等级降下来——降一级省出的容量通常不如删掉一串 UTM 参数。

其他默认值也会害人,导出后最好逐条验过。不可逆点很硬:内容一旦编码进图像就固定了,改一个字都得重新生成、重新印刷;也别把会过期的短链印在纸质物料上,那张纸的有效期于是由短链服务决定。

等级可恢复码字版本 40 字节容量适用场景
L约 7%2953屏幕、近距离
M约 15%2331常规印刷品
Q约 25%1663易磨损、曲面
H约 30%1273叠 logo、强光户外

纠错等级的收益与容量代价,都是可查的具体数值

Lorem 与测试数据:可复现还是像真的

lorem ipsum 是从西塞罗《论至善与至恶》(公元前 45 年)截出的一段改写过的拉丁文,本身没有语义。它存在的理由只有一个:让人看版式而不是读内容。这个理由在排版场景成立,一旦要验证「文本会不会撑破布局」,它就会系统性隐瞒问题。

lorem 的统计特征太整齐:词全取自几十个拉丁词,平均 5 到 7 个字符,既没有双宽汉字,也没有代理对。于是三类 bug 必然漏掉:截断切在字素簇中间,CJK 与拉丁混排时行高不对,超长无空格串(URL、哈希)横向溢出。要覆盖这些,得换成像真但假的数据,并显式准备空值与超长记录。

可复现性是另一半判断。断言型测试必须可复现:种子固定,或提交 JSON fixture;随机数据会让快照每次都不同,团队最后只能把断言删掉。演示型数据反而要像真的:有分布、有长尾、有边界,否则性能测试低估 IO,界面评审以为名字都只有八个字母。判断顺序是先问这份数据会不会进断言,再问它给谁看——两个答案经常指向相反的做法。

最危险的一步是拿生产数据脱敏。替换姓名却留着手机号、邮编这类准标识符,组合起来仍可重新识别到个人;而数据一旦复制进测试库,备份、日志、CI 缓存都留下副本,撤回不可能。生成器能做的正确事情是从零合成,不是从真数据里擦。随机数据进了版本库还有另一重代价:种子一改,整个 fixture 全量 diff,评审失去意义。

场景该用什么会漏掉什么
版式评审lorem长度分布、字符宽度
断言与快照固定 fixture边界需手工维护
性能与容量合成但贴近真实分布合成分布本身要验证
国际化布局多语种真实字符集字重与体积差异
演示与截图像真的假数据容易被当成已上线

先问这份数据会不会进断言,再决定用哪一种

唯一性声明:碰撞概率背后假设了什么

「碰撞概率可以忽略」永远附带条件。基础是生日界:在 N 个可能值里随机取 n 个,至少一对相同的概率约为 n 的平方除以 2N,50% 的临界点约在 n 等于 1.177 倍根号 N。这个式子只依赖一个前提——取值均匀且互相独立,唯一性承诺的成败都落在它上面,而不是位数看起来有多长。

代入数字才有意义。32 位空间(8 个十六进制字符)在约 7.7 万条时就有一半概率撞上;64 位要约 51 亿条;122 位的 v4 要到约 2.7 乘 10 的 18 次方条才到 50%,按每秒 100 万个算要 8 万多年,所以 v4 的问题从来不是碰撞。v7 的 74 位随机部分看似小得多,但比较范围是「同一毫秒」,对应约 1.6 乘 10 的 11 次方条,没有单机能在一毫秒内产出这个量级。真正危险的是截断与拼接:把 UUID 砍到前 8 位当短码,空间立刻掉到 32 位。

前提被破坏的方式都不改变位数,只让「均匀且独立」不再成立。存储层还会制造等价类:不区分大小写的排序规则会把只差大小写的两个 ID 视作同一个,定长列会截断,URL 编码往返可能把 + 变成空格。

还有一类「看起来正常」的错误:用 v3 或 v5 从邮箱派生用户 ID,换环境时复用同一个命名空间 UUID,测试环境的张三和生产环境的张三就成了同一个 ID;或者把生成值当业务主键又在展示层截断成 8 位,两个用户在某些页面显示成同一个号码。判据很简单:唯一性声明必须连带写出取值范围、有效位数与存储是否无损。

形态空间50% 碰撞条数
8 位十六进制2^32,约 43 亿约 7.7 万
64 位2^64,约 1.8 乘 10^19约 51 亿
74 位(v7 毫秒内)2^74,约 1.9 乘 10^22约 1.6 乘 10^11
122 位(v4)2^122,约 5.3 乘 10^36约 2.7 乘 10^18

同一个「不会撞」的结论,取值范围相差 90 位

信任一个生成值之前的检查清单

把生成值交出去之前,按顺序问七个问题,任何一个答不上来,就说明这一步还没做完。顺序是从「已经泄露」往回推的:泄露不可撤回,位数却可以重新生成。

清单里最容易被跳过的是第五条,因为它在开发环境永远不出错。VARCHAR(36) 存 UUID 没问题,但 CHAR(36) 在比较时可能忽略尾部空格;不区分大小写的排序规则让只差大小写的两个值相等;应用层为显示方便截成 8 位,又把这个截断值写回库当外键。三处改动单独看都合理,合起来就是一次无声的数据合并。

最后一条判据是关于「什么时候不该生成」。能用标准的地方不要自创:会话标识用现成库的 CSPRNG,别拿时间戳拼随机数;需要防篡改的令牌用带签名的结构,别指望够长的随机串;需要同一实体永远同一个 ID 时用 v5 加固定命名空间,别用随机值加查重表。自创方案真正的成本不在写出来的那二十行,而在没人替你验证过它在时钟回拨、并发、多副本下的行为。

常见问题

UUIDv7 能完全替代 UUIDv4 吗?
不能。v7 把 48 位毫秒时间写在前段,等于公开创建时刻与写入速率;对不可枚举的凭据类标识仍要用 v4 或额外随机后缀。v7 的收益在于数据库插入的局部性。
用 Math.random 生成 UUIDv4 到底差在哪?
位数看着是 122 位,实际受 128 位 PRNG 状态约束,观察到少量连续输出就能反推后续序列,跨进程还可能撞上同一种子。凡对外可见、可被猜的值都必须走 CSPRNG。
16 位随机串和「四类字符齐全的 8 位」哪个强?
16 位 62 字符集约 95 位熵,8 位 94 字符集约 52 位,相差 40 位以上。组合规则还会把用户推向固定形状,实际收益远小于单纯加长。
二维码用 H 级纠错是不是总更保险?
不是。H 只能恢复约 30% 的码字,代价是版本 40 的字节容量从 2953 降到 1273。同样内容要更高版本、更多模块,固定印刷尺寸下模块更小,反而更难扫。
lorem ipsum 在什么情况下不能用?
凡是测试长度、换行与字符宽度的场景都不能用。lorem 全是短英文词,测不出 CJK 双宽、emoji 代理对、超长字段与空值造成的布局问题。

动手试试