浏览器里经常有三类“看上去都像随机字符串”的需求:
- 给测试账号生成密码;
- 给一条记录分配唯一 ID;
- 给下载文件或文本生成校验值。
它们的目标不同,不能混着用。
密码需要不可预测;UUID 用于标识对象;SHA-256 用于判断字节内容是否一致。把 UUID 当 API 密钥、把 SHA-256 当密码存储方案,或者用 Math.random() 生成密码,都会留下不必要的风险。
| 目标 | 合适方案 | 不适合做什么 |
|---|---|---|
| 生成密码或令牌 | 密码学安全随机数 | 用作内容校验 |
| 标识数据库记录或请求 | UUID v4 / v7 | 充当登录凭据 |
| 校验文件或文本是否变化 | SHA-256 | 加密、签名、存储密码 |
一、生成密码:随机源比字符表更重要
下面这种写法很常见:
const chars = "*********************************************";
const value = chars[Math.floor(Math.random() * chars.length)];
它适合抽奖动画、界面效果和普通模拟数据,不适合密码。Math.random() 不是密码学安全随机源,攻击者在某些条件下可能推测后续输出。
浏览器需要使用 Web Crypto API:
const bytes = new Uint32Array(1);
crypto.getRandomValues(bytes);
console.log(bytes[0]);
crypto.getRandomValues() 提供适合密码学用途的随机值;Math.random() 则明确属于非密码学随机来源。MDN 的 getRandomValues 文档对此有清晰区分。
取字符时别忽略模偏差
假设字符表有 62 个字符,随机值范围是 0 到 2³² - 1。直接取模:
index = randomValue % 62;
并不能让每个字符完全等概率,因为 2³² 不能被 62 整除。前一部分字符会多一点机会被选中。
常见解决方式是拒绝采样:只接受落在某个可被字符表长度整除的随机范围内的值。
function secureIndex(maxExclusive) {
if (!Number.isInteger(maxExclusive) || maxExclusive < 1) {
throw new Error("字符表不能为空");
}
const limit = Math.floor(2 ** 32 / maxExclusive) * maxExclusive;
const values = new Uint32Array(1);
do {
crypto.getRandomValues(values);
} while (values[0] >= limit);
return values[0] % maxExclusive;
}
之后再根据索引取字符:
function choose(chars) {
return chars[secureIndex(chars.length)];
}
如果密码策略要求至少包含大写、小写、数字和符号,最容易写出的实现通常是“每类先放一个字符,再打乱”。这能满足规则,但不同密码组合出现的概率不一定一致。
更稳妥的做法是:从完整字符表中等概率生成整段密码,检查是否满足策略;不满足就重新生成。只要每次候选密码的生成分布一致,所有符合规则的结果也会保持等概率。
需要生成独立密码、调整字符集和长度,或检查密码策略是否满足站点要求时,可以使用 Password Generator。它在浏览器本地使用 Web Crypto,并用拒绝采样避免取模偏差。
密码生成后仍有几个现实问题:
- 每个服务应使用不同密码;
- 密码管理器比手动记忆更可靠;
- 剪贴板可能被其他软件读取,复制后应避免长时间停留;
- 密码强度无法防住钓鱼页面、终端恶意软件和重复使用。
二、UUID:标识符不是秘密
UUID 常用于数据库主键、日志关联 ID、任务 ID 和临时资源名。
浏览器可以直接生成 UUID v4:
const id = crypto.randomUUID();
console.log(id);
// 例如:36b8f84d-df4e-4d49-b662-bcde71a8764f
crypto.randomUUID() 在安全上下文中生成基于密码学安全随机数的 v4 UUID,结果是 36 个字符的标准文本形式。MDN 的 randomUUID 文档给出了接口和适用范围。
UUID v4 的大部分有效载荷来自随机数据,适合不依赖时间顺序的场景。
UUID v7 则在开头放入 Unix 毫秒时间戳,剩余非保留位使用随机数据。它在按创建时间排序的数据库索引中通常更友好:
UUID v4:随机数据为主
UUID v7:毫秒时间戳 + 随机数据
UUID v7 的时间特征也意味着它不是隐私字段。看到一个 v7 UUID,可以从中推断大致生成时间。同一毫秒内生成的多个 v7 UUID,尾部仍是随机的,因此不能把它当作严格的创建顺序。
UUID 的版本、变体和布局由 RFC 9562 定义。需要生成 v4、v7,或检查已有 UUID 的版本与文本格式时,可以使用 UUID Generator。
无论是 v4 还是 v7,都不应承担授权职责:
UUID ≠ 密码
UUID ≠ API Key
UUID ≠ Session Token
如果一个资源 URL 只靠 UUID 保护,例如 /download/550e8400-e29b-41d4-a716-446655440000,服务端仍应验证当前用户是否有权限访问资源。UUID 可以减少碰撞,不等于建立访问控制。
三、SHA-256:比较的是字节,不是“文件名”
SHA-256 接收任意长度的字节,输出固定 256 位摘要。输入只要有一点变化,摘要通常就会完全不同。
浏览器中可以这样计算 UTF-8 文本的 SHA-256:
async function sha256Hex(text) {
const bytes = new TextEncoder().encode(text);
const buffer = await crypto.subtle.digest("SHA-256", bytes);
return Array.from(new Uint8Array(buffer))
.map(byte => byte.toString(16).padStart(2, "0"))
.join("");
}
console.log(await sha256Hex("hello"));
SubtleCrypto.digest() 接收完整字节数据并返回摘要。它支持 SHA-256、SHA-384 和 SHA-512;SHA-1 不应再用于需要密码学安全性的场景。MDN 的 digest 文档还指出,该接口不支持流式输入,文件需要完整读入内存后才能计算。
同一段“看起来一样”的文本,字节不一定相同:
hello
hello\n
hello\r\n
它们会得到不同摘要。文件校验也一样,比较的是文件内容,不是文件名、大小或修改时间。
常见场景是下载校验:
本地文件 → SHA-256 → 与发布方给出的 SHA-256 对比
摘要一致只能证明两份字节内容一致。它不能证明文件安全,也不能证明网页上的预期摘要来自真正的发布者。预期值本身应从可信的发布渠道获取;需要确认发布者身份时,应使用数字签名或其他认证机制。
需要计算文本或本地文件的 SHA-256、SHA-384、SHA-512,并和预期摘要进行核对时,可以使用 Hash Generator。它支持十六进制和 Base64 两种摘要表示,输入在浏览器中处理。
四、哈希不是加密,也不能直接保存密码
哈希、加密和编码常被写在同一段需求里,但职责完全不同:
| 技术 | 是否可还原 | 常见用途 |
|---|---|---|
| SHA-256 | 不可逆 | 文件完整性、内容指纹 |
| AES 等加密 | 持有密钥可还原 | 保存需要重新读取的敏感数据 |
| Base64 | 可逆 | 文本化表示字节 |
| Argon2id、bcrypt、scrypt | 不可逆且有意放慢 | 密码存储 |
不能这样存密码:
const stored = await sha256Hex(password);
SHA-256 是快速通用摘要。攻击者拿到数据库后,可以高速尝试大量候选密码并对比摘要。密码存储应使用专门的慢哈希算法,并为每个密码使用独立盐值。
OWASP 当前建议优先使用 Argon2id;在不具备条件时可选择 scrypt、bcrypt 或 PBKDF2 等方案。其 密码存储指南明确指出,SHA-256 等快速摘要算法不适合存储密码。
五、三个工具如何放在一次开发流程里
假设要给一个内部文件处理服务补齐基础能力,可以按下面的边界拆分:
创建测试账户
→ 生成独立随机密码
创建上传任务
→ 生成 UUID v4 或 UUID v7
用户上传文件
→ 计算 SHA-256
下载或转存文件
→ 再次计算 SHA-256 并核对
其中:
- 随机密码保护账户;
- UUID 标记任务和文件记录;
- SHA-256 检查内容是否发生变化。
三者都可能出现在同一个系统里,却没有一个能替代另外两个。把用途分清,代码会简单很多,安全边界也更容易审查。