All Tools / Blog / 如何用 JavaScript 和 Python 生成安全随机密码

如何用 JavaScript 和 Python 生成安全随机密码

3 min read

大多数人用 Math.random() 编写的"随机"密码生成器实际上并不安全。要生成能抵御暴力破解的密码,需要使用密码学安全的随机数生成器。本文介绍正确的强密码生成方法。

密码安全的决定因素

密码强度由两个因素决定:长度和字符集大小。熵(以比特为单位)同时体现两者:

熵 = log2(字符集大小 ^ 长度)
   = 长度 × log2(字符集大小)

实际目标:

  • 128 比特 — 适用于大多数场景(相当于随机 128 位密钥)
  • 80 比特 — 重要事务的最低要求
  • < 40 比特 — 可被普通消费级硬件破解
密码类型 每字符比特数 达到 128 比特熵所需长度
仅小写(26 个字符) 4.7 28 个字符
混合大小写+数字(62 个字符) 5.95 22 个字符
全部可打印 ASCII(94 个字符) 6.55 20 个字符
Diceware 词组(7,776 个词) 12.9 10 个词

使用大写、小写、数字和符号的 20 位密码具有约 128 比特的熵,这是推荐目标。

JavaScript — 浏览器

使用 crypto.getRandomValues()——这是唯一正确的 API。永远不要使用 Math.random()

function generatePassword(length = 20, options = {}) {
    const {
        uppercase = true,
        lowercase = true,
        digits = true,
        symbols = true,
    } = options;

    let chars = '';
    if (uppercase) chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    if (lowercase) chars += 'abcdefghijklmnopqrstuvwxyz';
    if (digits)    chars += '0123456789';
    if (symbols)   chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';

    if (!chars) throw new Error('至少需要启用一种字符集');

    const array = new Uint32Array(length);
    crypto.getRandomValues(array);

    return Array.from(array, x => chars[x % chars.length]).join('');
}

console.log(generatePassword(20));
// "kT9!mP2#xQ7@rL5$nW8^"

关于取模偏差: Uint32Array 提供 0–4,294,967,295 的值。如果字符集有 94 个字符,取模并不完全均匀,但偏差极小(不足 0.001%),对密码生成而言可以忽略不计。

如需完全均匀分布(例如用于密码学密钥材料),可使用拒绝采样:

function secureSample(chars, length) {
    const result = [];
    while (result.length < length) {
        const bytes = new Uint8Array(length * 2);
        crypto.getRandomValues(bytes);
        for (const b of bytes) {
            if (result.length >= length) break;
            if (b < Math.floor(256 / chars.length) * chars.length) {
                result.push(chars[b % chars.length]);
            }
        }
    }
    return result.join('');
}

JavaScript — Node.js

const crypto = require('crypto');

function generatePassword(length = 20) {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()';
    const bytes = crypto.randomBytes(length);
    return Array.from(bytes, b => chars[b % chars.length]).join('');
}

console.log(generatePassword(24));
// "Xm9kQ!rL2nP@jT5vW8sZ#hK"

Python

Python 的 secrets 模块(Python 3.6+)是正确的工具——它使用操作系统的密码学安全随机数生成器。

import secrets
import string

def generate_password(length: int = 20) -> str:
    alphabet = string.ascii_letters + string.digits + string.punctuation
    return ''.join(secrets.choice(alphabet) for _ in range(length))

print(generate_password(20))
# "k!T9mP2#xQ7@rL5$nW8^"

secrets.choice() 使用操作系统的 CSPRNG 从序列中选取字符。这是正确的函数。不要使用 random.choice()——random 模块不是密码学安全的。

自定义字符集:

import secrets
import string

def generate_password(
    length: int = 20,
    uppercase: bool = True,
    lowercase: bool = True,
    digits: bool = True,
    symbols: bool = True,
) -> str:
    chars = ''
    required = []

    if uppercase:
        chars += string.ascii_uppercase
        required.append(secrets.choice(string.ascii_uppercase))
    if lowercase:
        chars += string.ascii_lowercase
        required.append(secrets.choice(string.ascii_lowercase))
    if digits:
        chars += string.digits
        required.append(secrets.choice(string.digits))
    if symbols:
        sym = '!@#$%^&*()-_=+[]{}|;:,.<>?'
        chars += sym
        required.append(secrets.choice(sym))

    if not chars:
        raise ValueError('至少需要启用一种字符集')

    # 用完整字符集填充剩余长度
    remaining = length - len(required)
    pool = required + [secrets.choice(chars) for _ in range(remaining)]

    # 打乱顺序,避免位置可预测(如第一个字符总是大写)
    secrets.SystemRandom().shuffle(pool)
    return ''.join(pool)

print(generate_password(24))

命令行

macOS/Linux — openssl:

# 20 位 base64 密码
openssl rand -base64 20

# 20 位十六进制密码
openssl rand -hex 20

# 仅字母数字(去除特殊字符)
openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 20

Linux — /dev/urandom:

# 可打印 ASCII 字符
cat /dev/urandom | tr -dc 'a-zA-Z0-9!@#$%^&*' | head -c 20; echo

Python 一行命令:

python3 -c "import secrets, string; print(''.join(secrets.choice(string.ascii_letters + string.digits + '!@#\$%^&*') for _ in range(20)))"

密码短语(Diceware)

由 4–6 个从大词表中随机选取的词组成的密码短语,安全性可能高于较短的随机密码——且更易记忆。

import secrets

# EFF 大词表包含 7776 个词(掷 6 次骰子)
# 下载:https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt

def generate_passphrase(word_count: int = 5, wordlist_path: str = 'eff_large_wordlist.txt') -> str:
    with open(wordlist_path) as f:
        words = [line.split('\t')[1].strip() for line in f if '\t' in line]
    return '-'.join(secrets.choice(words) for _ in range(word_count))

print(generate_passphrase(5))
# "crumpet-waffle-harbor-slime-donkey"

从 EFF 词表中选 5 个词可获得约 64.6 比特的熵,6 个词约 77.5 比特——相当于使用完整 ASCII 字符集的 12 位随机密码。

不应使用的方案

  • Math.random()(JavaScript)——使用伪随机数生成器,不是密码学安全的,且可被预测。
  • random.random() / random.choice()(Python)——同样的问题。
  • 任何对时间戳或用户名进行哈希的系统——不是真正的随机。
  • 少于 12 位的密码——对于任何敏感用途都太短。

要点总结

  • 浏览器 JavaScript 使用 crypto.getRandomValues(),Node.js 使用 crypto.randomBytes()
  • Python 使用 secrets.choice(),不要用 random.choice()
  • 命令行使用 openssl rand
  • 目标是 20 位以上、混合字符集,以获得约 128 比特的熵。
  • Diceware 密码短语是绝佳的替代方案——更长且更易记忆。