“密码要包含大小写、数字和特殊符号”——这条建议人人皆知,但它催生了 P@ssw0rd1! 这类密码:人类觉得复杂,计算机觉得太弱。这篇文章讲清楚密码强度的本质,以及作为开发者该如何生成和存储密码。
密码强度的本质:熵
密码强度取决于熵(entropy)——攻击者需要猜多少次才能找到正确密码。熵用比特(bit)衡量,每增加 1 bit,搜索空间翻倍。
从大小为 N 的字符集中随机选 L 个字符:
熵 = L × log₂(N)
| 字符集 | 大小 | 12 位密码的熵 |
|---|---|---|
| 纯小写 | 26 | 56 bit |
| 大小写混合 | 52 | 68 bit |
| 大小写 + 数字 | 62 | 72 bit |
| 大小写 + 数字 + 32 符号 | 94 | 79 bit |
参考标准:
- < 50 bit:现代硬件可在秒到小时内破解
- 50–70 bit:对有频率限制的低风险账号勉强够用
- 70+ bit:大多数场景足够强
- 100+ bit:面向未来的安全边际
P@ssw0rd1! 看起来满足所有规则,但它遵循固定模式(词+数字替换+符号),字典攻击加规则变换可以快速穷举。真随机 > 感觉复杂。
在线生成强密码
使用浏览器原生 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 个随机词的密语 |
| 应用密钥/Token | secrets.token_hex(32) |
| 密码存储 | Argon2id(≥3次迭代,64MB内存) |
| 泄露检查 | Have I Been Pwned API |
| 日常管理 | Bitwarden 或 1Password |
强密码的本质是熵和随机性,不是记忆技巧或复杂度规则。