“密码要包含大小写、数字和特殊符号”——这条建议人人皆知,但它催生了 P@ssw0rd1! 这类密码:人类觉得复杂,计算机觉得太弱。这篇文章讲清楚密码强度的本质,以及作为开发者该如何生成和存储密码。

密码强度的本质:熵

密码强度取决于熵(entropy)——攻击者需要猜多少次才能找到正确密码。熵用比特(bit)衡量,每增加 1 bit,搜索空间翻倍。

从大小为 N 的字符集中随机选 L 个字符:

熵 = L × log₂(N)
字符集大小12 位密码的熵
纯小写2656 bit
大小写混合5268 bit
大小写 + 数字6272 bit
大小写 + 数字 + 32 符号9479 bit

参考标准:

  • < 50 bit:现代硬件可在秒到小时内破解
  • 50–70 bit:对有频率限制的低风险账号勉强够用
  • 70+ bit:大多数场景足够强
  • 100+ bit:面向未来的安全边际

P@ssw0rd1! 看起来满足所有规则,但它遵循固定模式(词+数字替换+符号),字典攻击加规则变换可以快速穷举。真随机 > 感觉复杂。

在线生成强密码

使用 ZeroTool 密码生成器 →

使用浏览器原生 crypto.getRandomValues() API 生成密码,支持自定义长度、字符集和生成数量。所有运算在本地完成,密码不经过服务器。

安全随机的关键:CSPRNG

密码生成器必须使用密码学安全的随机数生成器(CSPRNG),不能用普通的伪随机数。

// 错误:Math.random() 不是密码学安全的
// 攻击者知道种子就能预测所有输出
const chars = 'ABCDEFabcdef0123456789';
let password = '';
for (let i = 0; i < 16; i++) {
    password += chars[Math.floor(Math.random() * chars.length)];
}

// 正确:crypto.getRandomValues() 是密码学安全的
function generatePassword(length = 16) {
    const charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
    const array = new Uint32Array(length);
    crypto.getRandomValues(array);
    return Array.from(array, n => charset[n % charset.length]).join('');
}

Python 中同样有这个区别:

import secrets
import string

def generate_password(length=16, use_symbols=True):
    alphabet = string.ascii_letters + string.digits
    if use_symbols:
        alphabet += string.punctuation
    # secrets.choice 底层调用 os.urandom(),密码学安全
    return ''.join(secrets.choice(alphabet) for _ in range(length))

password = generate_password(20)

random.choice() 是伪随机,不要用于安全相关的随机数生成。始终使用 secrets 模块。

密语(Passphrase):另一种强密码方案

密语是由随机单词组成的密码短语:

correct-horse-battery-staple
紫色-山脉-云朵-七月-河流

从 7776 词的 Diceware 词表中随机选词,每个词贡献约 12.9 bit 熵:

  • 4 词:~52 bit(最低可接受)
  • 5 词:~64 bit(推荐)
  • 6 词:~77 bit(高安全需求)

优点:

  • 更容易在手机上输入
  • 偶尔需要记忆时更友好
  • 视觉上易于区分,不容易看错

适合作为密码管理器的主密码。

长度 vs 复杂度

如果必须二选一,选更长。每增加一个字符,搜索空间增大 26–94 倍,远超从字符集上做文章。

16 位已混合字符类的随机密码,比 12 位的”更复杂”密码强得多。

实际建议:至少 16 位,使用大小写+数字+符号的混合字符集。

每个账号用不同密码——用密码管理器

如果每个账号都用随机强密码(应该这样),人脑是记不住的。这正是密码管理器的意义:

  • 用一个强主密码(建议用密语)解锁管理器
  • 管理器为每个账号存储独立的随机密码
  • 浏览器插件自动填充

推荐选择:

  • Bitwarden:开源,免费版功能完整,支持自托管
  • 1Password:产品体验好,适合团队
  • KeePassXC:本地存储,无云端依赖,适合离线使用

永远不要复用密码。 一次数据泄露会暴露所有账号。

开发者专区:应用密钥生成

生成 API Key、JWT 密钥、Session Key 等应用密钥:

# Shell — 32 字节十六进制(64 字符,适合大多数密钥)
openssl rand -hex 32

# Shell — Base64 编码
openssl rand -base64 32
import secrets

# API Key(十六进制)
api_key = secrets.token_hex(32)

# Session Secret(URL 安全 Base64)
session_secret = secrets.token_urlsafe(32)
// Node.js
import { randomBytes } from 'crypto';
const apiKey = randomBytes(32).toString('hex');

规则:

  • JWT 密钥至少 256 bit(32 字节)
  • AES-256 需要恰好 32 字节
  • 永远不要用人类可记忆的字符串作为加密密钥

HaveIBeenPwned:检查密码是否已泄露

HaveIBeenPwned API 可以检查密码是否出现在已知的数据泄露中,且不暴露真实密码(使用 k-anonymity 模型):

import hashlib
import requests

def is_pwned(password: str) -> bool:
    sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix, suffix = sha1[:5], sha1[5:]

    resp = requests.get(f'https://api.pwnedpasswords.com/range/{prefix}')
    return any(line.split(':')[0] == suffix for line in resp.text.splitlines())

if is_pwned("P@ssw0rd1"):
    print("该密码已出现在数据泄露记录中,请更换!")

只发送哈希前 5 位,API 返回所有同前缀的哈希,本地比对——密码本身不离开你的机器。

注册表单的密码校验:该做和不该做

该做:

  • 设置最小长度(至少 12 位,推荐 16 位)
  • 允许所有可打印字符,包括空格
  • 对照 HIBP 数据库拒绝已泄露密码
  • 显示密码强度指示器

不该做:

  • 强制要求特定字符类型组合(会导致用户使用固定模式)
  • 设置最大长度(在服务端用哈希存储时毫无意义)
  • 定期强制更换密码(会导致用户做递增变化:Pass1!Pass2!
  • 允许密码提示(泄露密码信息)

密码存储:开发者必须知道

用户密码绝对不能明文存储,也不能用 SHA-256 直接哈希(太快,可暴力破解)。必须使用专为密码设计的慢哈希算法

算法推荐度说明
Argon2id首选密码哈希竞赛冠军,内存困难
bcrypt广泛支持兼容性最好
scrypt次选内存困难,是 Argon2 的好替代
# Python — argon2-cffi
from argon2 import PasswordHasher

ph = PasswordHasher(time_cost=3, memory_cost=65536, parallelism=1)
hashed = ph.hash("用户输入的密码")
ph.verify(hashed, "用户输入的密码")  # True

Django 默认使用 PBKDF2+SHA256,可以切换为 Argon2;Spring Security 默认 bcrypt。

速查表

场景推荐方案
日常账号密码16+ 位随机混合字符
需要记忆的密码6 个随机词的密语
应用密钥/Tokensecrets.token_hex(32)
密码存储Argon2id(≥3次迭代,64MB内存)
泄露检查Have I Been Pwned API
日常管理Bitwarden 或 1Password

强密码的本质是熵和随机性,不是记忆技巧或复杂度规则。

在线生成密码学安全的强密码 →