密码生成器

生成安全的随机密码

为什么"密码复杂度规则"是错误的(NIST SP 800-63B)

几十年来,关于密码的标准建议一直是:至少一个大写字母、一个小写字母、一个数字、一个特殊字符;每90天强制更换;禁止重复使用最近5个密码。大多数企业IT部门至今仍在强制执行这一套。NIST——美国国家标准与技术研究院,负责制定联邦网络安全指南的机构——于2017年发布了Special Publication 800-63B,并在2024年的重大修订中明确指出:把这些做法全部停掉。 其依据来自对真实世界结果的测量:复杂度规则和强制轮换让密码变得更糟,而不是更好。当用户被迫满足"必须包含大写+数字+特殊字符"时,占主导地位的结果是一种可预测的变换——Password1!、Password2!、Password3!——基于人类习惯的猜测工具可以轻而易举地枚举出来。当被迫每90天轮换一次时,用户只会在原密码后面附加一个计数器。这种"复杂度"看似能通过正则校验,但相对于没有约束的同等密码,熵的增益基本为零,而用户的摩擦却急剧上升,密码最终被重复使用或写在便利贴上。 NIST SP 800-63B第5.1.1.2节(memorized secrets)现在写道:"验证方不应(SHOULD NOT)对记忆型秘密施加其他组成规则(例如要求混合不同类型的字符)。验证方不应(SHOULD NOT)要求任意地(例如定期地)更换记忆型秘密。"只有在出现泄露证据时才需要更换。最小长度为8(随机生成的为6),最大长度必须(SHALL)至少为64,并且必须(MUST)允许所有可打印的ASCII和Unicode字符。字符类别限制已被取消。 取代复杂度规则的是拒绝列表(denylist)。NIST强制要求将密码与已知被泄露值的列表进行比对:出现在泄露语料库中的密码、字典单词、重复序列(aaaaaaaa、qwerty),以及与上下文相关的字符串(用户名、站点名)。免费服务Have I Been Pwned提供了一个可以通过k-匿名性集成的API——发送SHA-1哈希的前5个字符,取回所有具有该前缀的哈希,再在本地比对。2026年,已有数百万服务在使用它。 由此得出的实用用户建议是:使用长随机密码(由生成器生成16个字符以上),每个站点一个,存放在密码管理器中。或者使用便于记忆的口令短语(从词表中随机选取4个以上英文单词,约50比特以上的熵)。停止强制轮换。停止屏蔽表情符号或Unicode。停止强制要求特殊字符。只拦截明显糟糕的密码,在此之外,长度和形式让用户自己选择。
// HIBP k-anonymity check (no full hash leaves your server)
async function checkPwned(password) {
  const hash = sha1Hex(password).toUpperCase();
  const prefix = hash.slice(0, 5);
  const suffix = hash.slice(5);
  const res = await fetch('https://api.pwnedpasswords.com/range/' + prefix);
  const text = await res.text();
  for (const line of text.split('\n')) {
    const [s, count] = line.trim().split(':');
    if (s === suffix) return parseInt(count, 10); // # of breaches
  }
  return 0;
}

// 2026 sign-up flow per NIST SP 800-63B
async function validateSignup(password) {
  if (password.length < 8) return 'too short';
  if (password.length > 64) return 'too long';
  if (/^(.)\1{4,}$/.test(password)) return 'too repetitive';
  const breached = await checkPwned(password);
  if (breached > 0) return `appeared in ${breached} breaches`;
  return null; // OK — no character-class checks
}

熵的数学——密码应该多长

密码强度用Shannon熵来量化:H = log2(N^L),其中N是字母表大小,L是长度。单位是比特,每增加一比特,攻击者所需的平均猜测次数就翻一倍。从26个字母的字母表中均匀随机抽取的长度为10的密码有26^10 ≈ 1.4 × 10^14种可能取值,即log2(1.4 × 10^14) ≈ 47比特的熵。同样长度、基于95个可打印ASCII字符的字母表则是95^10 ≈ 6 × 10^19,约66比特。 关键词是"均匀随机"。人类选择的密码的熵远低于字母表所暗示的水平,因为人类只从整个空间中极小的一部分里挑选。对泄露密码语料库(RockYou、LinkedIn、Adobe)的研究一致估计,人类选择的8字符密码的有效熵为18~22比特——以50%的概率破解只需约400万到800万次猜测,单块GPU几秒钟就完全够用。同样的字符数由生成器生成则是50比特以上,约10^15次猜测——在专用硬件上要以年为单位计算。 作为对现代攻击者能力的一个校准:2026年,单块Nvidia RTX 4090每天可计算约2000亿次bcrypt cost-5哈希。由100块GPU组成的小型集群可达20万亿次。租用云端hashcat集群几个小时,就能达到高出若干数量级的规模。密码哈希算法的防护性work factor影响极其巨大——在bcrypt cost 12(当前OWASP推荐)下,同样的硬件每天只能计算约8万次哈希,因此进行暴力破解的攻击者所需时间比cost 5长约2.5亿倍。这就是为什么密码哈希的选择至少与密码长度同等重要。 2026年的运营指南(假设采用典型的Argon2id work factor):从95个符号的字母表中随机生成12个字符,约有79比特的熵,在未来十年内实际上无法破解。16个字符约为105比特,即使面对国家级行为者的预算,在可预见的未来也无法破解。对于口令短语,从7,776个单词的列表(EFF wordlist)中随机选取4个单词是log2(7776^4) ≈ 51比特——与12字符密码相当,但好记得多。5个单词是65比特,是人类可记忆用途的实用甜点区。 一个微妙的陷阱:熵假设的是均匀随机选择。如果你的生成器使用Math.random(),那么关于熵的说法就是谎言——V8中的Math.random()是xorshift128+,攻击者只要捕获几个输出就能预测后续输出。任何密码级别的输出都要始终使用crypto.getRandomValues(new Uint32Array(...))(浏览器)或crypto.randomBytes(...)(Node)。这两者的差别,就是79比特熵与0之间的差别。
// Entropy table (assumes uniform random)
//
//  alphabet  | length | bits  | crack time at 1e9 guesses/sec
//  --------- | ------ | ----- | -----------------------------
//  26        |   8    |  37.6 |  2 minutes
//  62        |   8    |  47.6 |  1 day
//  95        |   8    |  52.5 |  ~50 days
//  95        |  12    |  78.7 |  ~10 million years
//  95        |  16    | 105.0 |  ~10^15 years (heat death of sun)
//
// — and remember: real cracking goes through Argon2id which is
//   ~10^6 to ~10^9 times slower than raw hashing.

// CORRECT: cryptographic random
function generate(length, alphabet) {
  const out = new Array(length);
  const r = new Uint32Array(length);
  crypto.getRandomValues(r);
  for (let i = 0; i < length; i++) out[i] = alphabet[r[i] % alphabet.length];
  return out.join('');
}

// WRONG: Math.random — predictable, NOT for passwords
function bad(length) {
  let s = '';
  for (let i = 0; i < length; i++) s += String.fromCharCode(97 + Math.floor(Math.random() * 26));
  return s;
}

// EFF passphrase
import wordlist from './eff_large_wordlist.json'; // 7776 words
function passphrase(n = 5) {
  const r = new Uint32Array(n);
  crypto.getRandomValues(r);
  return [...r].map(x => wordlist[x % 7776]).join('-');
}

Passkey、FIDO2、WebAuthn——后密码时代的世界

密码有一个无论多长、无论怎样哈希都无法修复的结构性问题:它们是持有者秘密(bearer secret)。用户知道它,服务器存储它的哈希,网络传输它。这个秘密存在的每一个地方,都可能被钓鱼、被泄露、被截获或被重放。Passkey(通行密钥)是业界协调一致给出的答案。 Passkey是一对公私钥,由用户的设备生成,并存储在设备的安全隔区中(Apple Secure Enclave、Android StrongBox、Windows Hello TPM,以及像YubiKey这样的硬件FIDO2密钥)。注册时,设备生成密钥对,只把公钥发送给服务器。登录时,服务器发送一个随机challenge,设备用私钥对challenge签名,服务器再用公钥验证。私钥永远不会离开设备。challenge每次登录都会更换。不存在可被钓走的共享秘密。 相关标准包括:FIDO2(FIDO Alliance的总体框架)、WebAuthn(W3C浏览器API,自2019年起为Recommendation状态)以及CTAP2(浏览器与认证器硬件之间的协议)。Apple、Google、Microsoft和Mozilla在2022~2023年同步推出了passkey支持,Apple iCloud Keychain和Google Password Manager使用端到端加密在用户的各个设备之间复制passkey。截至2026年,所有主要站点(Google、Apple、Microsoft、GitHub、PayPal、Amazon、Shopify等)都支持仅用passkey登录。 抗钓鱼能力是结构性的。WebAuthn签名包含relying party identifier(域名),浏览器会拒绝在错误的origin上使用passkey。被诱骗在google.com.attacker.example输入Google凭据的用户,无法在那里使用他们的Google passkey——浏览器会检查origin,设备会拒绝签名。这就是为什么FIDO是面向消费者推出的、单项效果最强的反钓鱼技术。 实现模式(服务器端):注册时,接收WebAuthn registration response,验证attestation,为该用户存储公钥 + credential ID + counter。登录时,生成随机challenge发送给客户端,接收签名后的assertion,用存储的公钥验证签名,检查counter是否单调递增(抵御被克隆的认证器),并检查origin与你的域名一致。可用库:@simplewebauthn/server(Node)、webauthn-rs(Rust)、java-webauthn-server(Java)。 2026年大多数应用遵循的过渡模式是:为向后兼容保留密码登录,但在首次登录时提示注册passkey,之后的登录优先使用passkey。GitHub正是这样做的。领先站点报告称,在提供passkey后的12~18个月内,60~80%的活跃用户至少注册了一个passkey,密码的使用也相应下降。最终状态是"默认passkey,密码仅作为迁移场景的后备",并最终对新账户实现无密码认证。
// Browser: register a passkey
const opts = await fetch('/passkey/register/options').then(r => r.json());
const cred = await navigator.credentials.create({
  publicKey: {
    challenge: base64ToArrayBuffer(opts.challenge),
    rp: { name: 'Acme', id: 'acme.example' },
    user: { id: opts.userId, name: 'alice', displayName: 'Alice' },
    pubKeyCredParams: [{ type: 'public-key', alg: -7 /* ES256 */ }],
    authenticatorSelection: { userVerification: 'preferred' },
  }
});
await fetch('/passkey/register', { method: 'POST', body: JSON.stringify(cred) });

// Browser: sign in with passkey
const opts2 = await fetch('/passkey/login/options').then(r => r.json());
const assertion = await navigator.credentials.get({
  publicKey: {
    challenge: base64ToArrayBuffer(opts2.challenge),
    rpId: 'acme.example',
  }
});
await fetch('/passkey/login', { method: 'POST', body: JSON.stringify(assertion) });

// Server: verify with @simplewebauthn/server (Node)
import { verifyAuthenticationResponse } from '@simplewebauthn/server';
const verify = await verifyAuthenticationResponse({
  response: req.body,
  expectedChallenge: storedChallenge,
  expectedOrigin: 'https://acme.example',
  expectedRPID: 'acme.example',
  authenticator: storedCred,
});
if (verify.verified) await db.users.updateCounter(userId, verify.authenticationInfo.newCounter);
最后更新:

关于此工具

密码生成器按你选择的长度与字符集(大小写、数字、符号)生成密码学安全的随机字符串。随机密码比人想出的密码难猜得多,与密码管理器配合使用是现代最佳实践。本工具基于 Web Crypto API,完全在浏览器内运行。

使用方法

  1. 设置所需长度——建议16个字符或以上。
  2. 切换要包含的字符类别。
  3. 如需人工抄写,可选择排除易混淆字符(l/1/I、O/0)。
  4. 点击 Generate 生成新密码。
  5. 复制结果并存入密码管理器——切勿写在纸上或粘贴到聊天中。

常见用例

  • 为新的网站或服务创建唯一密码。
  • 在事故或人员变动后轮换服务凭据。
  • 生成存放在保险库中的API令牌或webhook密钥。
  • 为共享设备或一次性账户生成临时密码。
  • 为将进行静态加密的CI/CD流水线密钥提供初始值。
  • 签发临时的访客Wi-Fi密码。

常见问题

Q. 密码应该多长?

A. 对大多数账户来说,16个以上的随机字符已经绰绰有余。如今更大的威胁是密码重用和钓鱼,而不是长度本身。

Q. 这些密码在密码学上安全吗?

A. 安全。生成器使用的是Web Crypto API的crypto.getRandomValues,它专为密码学强度而设计,而不是Math.random。

Q. 我需要背下我的密码吗?

A. 只需记住密码管理器的一个强主密码。其余的都交给管理器来生成和记忆。

Q. 为什么要排除相似字符?

A. 这是可选项,但当需要人工从屏幕上抄写密码时很有用——l、1、I、0、O 很容易混淆。只存放在密码管理器中的密码可以不启用此选项。