密码安全
前言
密码是信息系统中最基础也是最重要的身份验证手段。尽管无密码认证技术正在兴起,密码在可预见的未来仍将是认证体系的核心组成部分。本文从密码哈希、加盐策略、登录保护、密码策略、凭证填充防御、密码重置安全、密码强度评估、无密码认证趋势、密码管理器使用以及各语言最佳实践等多个维度,系统性地阐述密码安全的设计与实现。
一、密码哈希算法选型
密码哈希与普通哈希(如 MD5、SHA-256)有本质区别。密码哈希需要具备计算可控慢(抵御暴力破解)、抗 GPU/ASIC 并行加速(内存密集或并行度受限)、内置加盐等特性。
1.1 主流算法对比
| 算法 | 设计时间 | 内存密集 | 可调参数 | 抗 GPU/ASIC | 推荐程度 |
|---|---|---|---|---|---|
| bcrypt | 1999 | 否 | 轮数 (cost) | 中等 | 推荐 |
| PBKDF2 | 2000 | 否 | 迭代次数 | 弱 | 不推荐(新系统) |
| scrypt | 2009 | 是(CPU+内存) | N/r/p 参数 | 强 | 推荐 |
| Argon2id | 2015 | 是(CPU+内存+并行) | m/t/p 参数 | 很强 | 强烈推荐 |
1.2 算法详解与选型建议
bcrypt:基于 Blowfish 密码算法,内置加盐(128-bit),通过 cost 因子(2 的幂次)控制计算时间。推荐 cost 值 ≥ 12(2026 年建议 ≥ 14)。bcrypt 的密码长度限制为 72 字节,超出部分会被截断,这是一个需要注意的陷阱。广泛支持,几乎所有语言都有高质量实现。
bcrypt 的哈希输出格式为:$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
其中 $2b$ 为版本标识,10 为 cost 因子,后续为 22 字符的盐和 31 字符的哈希值。
PBKDF2:基于 HMAC 的密钥派生函数,虽然被 NIST 推荐,但其抗 GPU 攻击能力较弱——GPU 可以高效并行计算 HMAC-SHA256。仅在没有更好选择(如 FIPS 合规场景)时使用,且迭代次数应 ≥ 600,000(2026 年标准)。
scrypt:在 PBKDF2 基础上增加了内存密集特性,通过大内存访问模式显著提高 GPU/ASIC 攻击成本。参数推荐:N=2^16 (65536), r=8, p=1。
Argon2id:2015 年 Password Hashing Competition 的优胜算法,是目前公认的最佳密码哈希算法。Argon2id 混合模式同时抵御 GPU 攻击(内存密集)和侧信道攻击(数据依赖访问)。OWASP 推荐新系统优先选用 Argon2id。参数推荐(2026 年):m=64MB, t=3, p=4。
1.3 选型决策
| 场景 | 推荐算法 |
|---|---|
| 新系统开发 | Argon2id |
| 兼容老旧环境 | bcrypt (cost ≥ 14) |
| FIPS 合规要求 | PBKDF2(迭代次数 ≥ 600,000) |
| 遗留系统升级 | 逐步迁移至 Argon2id(rehash on login) |
二、加盐策略
2.1 为什么要加盐
不加盐的密码哈希存在以下风险:
- 彩虹表攻击:预计算哈希链可快速反查明文密码
- 批量破解:相同密码产生相同哈希值,一次破解命中多个账户
- 查表攻击:对常见密码预计算哈希后直接匹配
2.2 Salt 生成与存储
生成标准:
| 属性 | 推荐值 |
|---|---|
| 长度 | ≥ 16 字节(128 bit),推荐 32 字节 |
| 随机源 | 密码学安全伪随机数生成器(CSPRNG) |
| 唯一性 | 每个用户、每次密码变更都生成新的盐 |
错误做法:使用用户名作为盐、使用固定全局盐、使用短盐(< 8 字节)、使用非加密随机数(如 Math.random() / rand())。
存储方式:盐不保密,直接存储在用户表的密码哈希值旁边。常见存储格式为哈希值自包含盐(如 bcrypt/Argon2id 的输出),或使用单独的数据库字段。
2.3 代码示例
TIP
以下代码展示了如何使用 CSPRNG 生成盐,然后进行密码哈希。
import os
import hashlib
# 生成 32 字节(256 位)密码学安全随机盐
salt = os.urandom(32)import "crypto/rand"
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
panic(err)
}import java.security.SecureRandom;
byte[] salt = new byte[32];
SecureRandom.getInstanceStrong().nextBytes(salt);三、登录限速与账户锁定策略
3.1 为什么需要登录限速
密码哈希虽然增加了单次验证的耗时,但对单个 IP 或单个账户发起大规模暴力破解仍然可行。必须在应用层、网络层同时实施防御。
3.2 分层防御策略
| 层级 | 措施 | 阈值参考 |
|---|---|---|
| 网络层 | WAF / IP 限速 | 单个 IP 每分钟 ≤ 30 次登录请求 |
| 应用层 | 渐进式延迟 | 失败次数越多,等待时间指数增长 |
| 账户层 | 临时/永久锁定 | 连续 5-10 次失败后临时锁定 15-30 分钟 |
| 全局层 | 分布式限速(Redis 计数器) | 全局失败率监控告警 |
3.3 渐进式延迟算法示例
import time
from functools import lru_cache
def get_login_delay(attempt_count: int) -> float:
"""
使用指数退避算法计算延迟时间
attempt_count: 连续失败次数
"""
if attempt_count <= 3:
return 0.0
# 延迟时间:min(2^count, 3600) 秒
delay = min(2 ** (attempt_count - 3), 3600)
return delay3.4 账户锁定注意事项
- 防止账户枚举:模糊提示"用户名或密码错误",不区分哪种情况
- 锁定通知:通过备用邮箱/手机通知用户账户被锁定
- 解锁机制:通过邮箱验证链接或人工客服解锁,避免自动解锁被持续攻击
- 分布式环境:使用 Redis 或数据库记录失败次数,注意原子性操作
- 不要永久锁定:通常临时锁定 15-30 分钟即可,永久锁定会导致攻击者恶意锁定用户
3.5 CAPTCHA 集成
当检测到异常登录行为时,引入 CAPTCHA 验证是一种有效的降噪手段。建议:
- 正常登录不需要 CAPTCHA
- 单 IP 登录失败 ≥ 3 次后触发 CAPTCHA
- 不推荐 Google reCAPTCHA v2(有被绕过风险),推荐 reCAPTCHA v3 或 hCaptcha
四、密码策略设计
4.1 现代密码策略理念
传统密码策略强调复杂度(大小写 + 数字 + 特殊字符),导致用户倾向于使用 Password1! 这样的可预测模式。NIST SP 800-63B 和 OWASP 推荐以长度为重心的策略。
4.2 NIST SP 800-63B vs 传统策略
| 维度 | 传统策略(不推荐) | NIST 推荐策略 |
|---|---|---|
| 最小长度 | 8 字符 | 12-16 字符 |
| 复杂度 | 必须包含大小写/数字/特殊字符 | 不强制要求(鼓励密码短语) |
| 密码提示 | 允许"安全问题" | 禁止 |
| 定期更换 | 强制 30-90 天更换 | 不强制(除非有泄露证据) |
| 历史检查 | 禁止最近 3-5 个密码 | 检查是否出现在已知泄露数据库中 |
4.3 最佳实践建议
密码策略设计原则:
├── 长度优先:最小 12 字符,推荐 16+ 字符
├── 检查泄露:对接 Have I Been Pwned API
├── 密码短语:鼓励使用 "correct-horse-battery-staple" 模式
├── 限制组合规则:不强制特定字符类型(避免用户可预测行为)
├── 无安全问题:忘记密码使用邮箱/短信验证重置
└── 无定期更换:除非密码泄露,否则不强制更换4.4 密码黑名单
维护常见密码黑名单(top 10,000 常见密码),在新注册和修改密码时进行校验。
COMMON_PASSWORDS = {
"123456", "password", "12345678", "qwerty123",
"admin123", "letmein", "welcome1", "monkey123",
# ... 可从 SecLists 项目获取完整列表
}
def is_password_blacklisted(password: str) -> bool:
return password.lower() in COMMON_PASSWORDS五、凭证填充(Credential Stuffing)攻击与防御
5.1 攻击原理
凭证填充攻击利用用户在多个网站复用同一密码的行为,通过已泄露的账号密码库(数据泄露获得),自动化尝试登录其他网站。攻击流程:
- 攻击者从数据泄露中获取大量(邮箱 + 密码)对
- 使用自动化工具(如 OpenBullet、Sentinel)批量尝试登录目标网站
- 由于密码复用,成功率通常在 0.1% - 2% 之间
- 成功登录的账户可用于数据窃取、欺诈交易或进一步攻击
5.2 攻击特征
| 特征 | 描述 |
|---|---|
| 来源 IP | 分散大量(代理/VPN/僵尸网络),难以通过单 IP 封禁 |
| 请求频率 | 低于传统暴力破解以规避限速 |
| 用户代理 | 模拟真实浏览器指纹 |
| 时间分布 | 分散在数小时甚至数天内 |
5.3 防御措施
核心防御:
- 多因素认证(MFA):凭证填充攻击的核心防御手段。即使密码泄露,没有第二因素也无法登录
- CAPTCHA 智能触发:对可疑登录行为触发 CAPTCHA 验证
- 设备指纹:分析 TLS 指纹(JA3)、浏览器指纹、屏幕分辨率、时区等
- IP 信誉评分:对已知代理/VPN IP 提高验证要求
辅助防御:
- Have I Been Pwned 数据集对接:注册时检查密码是否已泄露
- 同 IP 多账户检测:单个 IP 短时间内登录多个不同账户,判定为自动化攻击
- Cookie/Token 绑定:将登录 Token 绑定到设备指纹
- 异常登录检测:检测与历史行为不一致的登录(异地、异设备、异时段)
六、密码重置功能安全
密码重置功能是攻击者最常利用的攻击面之一。设计不当的密码重置流程可能导致账户被完全接管。
6.1 Token 安全
| 属性 | 安全要求 |
|---|---|
| 长度 | ≥ 128 bit(推荐 256 bit / 32 字节) |
| 随机性 | CSPRNG 生成 |
| 存储 | 仅存储 Token 的 SHA-256 哈希值,不存明文 |
| 有效期 | 通常 15-60 分钟,根据安全等级调整 |
| 使用次数 | 一次有效,使用后立即失效 |
| 传输 | 仅通过 HTTPS 传输,需在 URL 中不引入 Referer 泄露 |
6.2 Token 生成示例
import secrets
import hashlib
def generate_reset_token() -> tuple[str, str]:
"""
返回 (raw_token, token_hash) 元组
仅将 token_hash 存入数据库
"""
raw_token = secrets.token_urlsafe(32) # 256 bit
token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
return raw_token, token_hash6.3 邮箱验证
- 速率限制:每个账户每小时最多请求 1 次密码重置邮件
- 枚举保护:无论邮箱是否存在,都返回"如果邮箱存在,重置链接已发送"
- 来自验证:重置页面显示部分脱敏邮箱(如
u***@example.com) - 过期处理:Token 过期后清除,用户需重新申请
6.4 时间窗口与状态管理
密码重置流程:
1. 用户提交邮箱/用户名
2. 系统生成重置 Token(有效期 30 分钟),存入数据库
3. 发送包含 Token 的邮件
4. 用户点击链接,系统验证 Token:
- Token 是否存在且未过期
- Token 是否已被使用
- 是否超过最大尝试次数
5. 验证通过后允许设置新密码
6. 令牌标记为已使用
7. 通知用户密码已更改(发邮件提醒)6.5 防范账户接管
- 新密码策略:新密码不能与旧密码相同
- 会话失效:密码重置后使所有活跃会话失效
- 通知机制:密码变更后立即向用户的备用联系渠道发送通知
- 审计日志:记录所有密码重置请求的来源 IP、时间、结果
七、密码强度评估与 zxcvbn 库
7.1 传统评估方法的缺陷
传统的密码强度检测(如正则匹配大小写/数字/特殊字符)存在以下问题:
- 将
Password1!判定为"强"——实际是常见模式 - 将
Tr0ub4dor&3判定为"强"——但用户难以记忆 - 无法识别键盘序列(
qwerty123)、字典单词变形等模式
7.2 zxcvbn 简介
zxcvbn 是 Dropbox 开源的密码强度评估库,采用模式匹配和概率估计的方法,能够准确评估密码的实际强度。其核心优势:
- 识别常见密码、字典单词、键盘序列、日期、重复模式、L33t 替换
- 提供时间估计:离线快速破解、离线慢速破解、在线限速破解所需时间
- 提供具体改进建议
- 轻量级,支持 JavaScript / Python / Go / Java 等多语言
7.3 使用示例
// JavaScript (前端使用)
import zxcvbn from '@zxcvbn-ts/core'
import '@zxcvbn-ts/language-zh' // 中文语言包
const result = zxcvbn('correct-horse-battery-staple')
console.log(result.score) // 0-4,4 为最强
console.log(result.crack_times_display)
// {
// offline_fast_hashing_1e10_per_second: "centuries",
// offline_slow_hashing_1e4_per_second: "centuries",
// online_no_throttling_10_per_second: "centuries",
// online_throttling_100_per_hour: "centuries"
// }
console.log(result.feedback.warning) // 警告信息
console.log(result.feedback.suggestions) // 改进建议# Python 后端使用
from zxcvbn import zxcvbn
def check_password_strength(password: str) -> dict:
result = zxcvbn(password)
score = result['score'] # 0-4
if score < 3:
suggestions = result['feedback']['suggestions']
warning = result['feedback']['warning']
return {
'valid': False,
'score': score,
'suggestions': suggestions,
'warning': warning
}
return {'valid': True, 'score': score}7.4 集成建议
- 前端注册表单实时显示密码强度条,使用 zxcvbn 的前端版本
- 后端在密码变更 API 中再次校验强度
- 阈值:要求 score ≥ 3(推荐 4)才允许提交
- 展示破解时间:用户看到具体的时间估计比抽象星级更有说服力
八、无密码认证趋势
8.1 从密码到无密码
无密码认证并非单一技术,而是一组旨在消除传统密码依赖的认证方式集合。
| 方式 | 原理 | 安全级别 | 用户体验 |
|---|---|---|---|
| 魔法链接(Magic Link) | 通过邮箱一次性链接登录 | 中 | 好 |
| 一次性验证码(OTP) | 邮箱/短信发送一次性验证码 | 中低(短信) | 好 |
| Passkeys / WebAuthn | 公钥密码学,设备绑定 | 高 | 极好 |
| 生物识别 | 指纹/面部/虹膜 | 中(取决于设备) | 极好 |
| 第三方 OAuth | Google/Apple/GitHub 登录 | 高(取决于 IdP) | 好 |
8.2 WebAuthn 与 Passkeys
WebAuthn(Web Authentication)是 W3C 和 FIDO 联盟制定的无密码认证标准。其核心机制:
- 服务端存储公钥,客户端(通过设备 TPM/安全芯片)存储私钥
- 登录时,服务端发送 challenge,客户端用私钥签名后发回
- 服务端验证签名——私钥永不离开用户设备
Passkeys 是 Apple/Google/Microsoft 在 WebAuthn 基础上推广的终端用户体验方案:
- 支持跨设备同步(通过 iCloud Keychain/Google 密码管理器)
- 支持同一生态系统内的跨设备认证(如用 iPhone 扫码登录 Mac)
- 公钥存储在服务端,私钥通过端到端加密同步到用户的其他设备
8.3 混合认证策略
在实际系统中,建议采用渐进迁移策略:
阶段一:密码 + MFA(当前大多数系统的状态)
↓
阶段二:密码 + Passkey 可选(用户逐步注册 Passkey)
↓
阶段三:Passkey 为主要方式,密码为降级选项
↓
阶段四:纯 Passkey(高风险场景可完全弃用密码)8.4 实现注意事项
- 后备方案:始终保留至少一种降级认证方式(如邮箱验证码)
- 注册引导:在用户成功登录后提示注册 Passkey
- 跨设备:确保用户在不同设备上都有可用的认证方式
- 隐私:生物识别数据不离开用户设备(只返回验证结果)
九、密码管理器的安全使用
9.1 为什么需要密码管理器
现代用户需要管理数十甚至数百个账户。记忆所有强密码不现实,复用密码又极为危险。密码管理器是解决这一矛盾的核心工具。
9.2 主流密码管理器对比
| 产品 | 开源 | 本地存储 | 云同步 | 安全架构 |
|---|---|---|---|---|
| Bitwarden | 是 | 是 | 是 | 端到端加密,可选自托管 |
| 1Password | 否 | 否 | 是 | Secret Key + 主密码,SRP 协议 |
| KeePassXC | 是 | 是 | 否 | 本地数据库,支持多种加密后端 |
| Apple iCloud 钥匙串 | 否 | 是 | 是 | 设备端加密,端到端同步 |
| Chrome 内置管理器 | 否 | 是 | 是 | 使用 Google 账户加密 |
9.3 密码管理器的安全原理
主密码 → [PBKDF2/Argon2id] → 加密密钥 → [AES-256-GCM] → 加密密码库
↑
存储密文- 零知识架构:服务端无法获知用户的主密码和存储的密码
- 端到端加密:数据在客户端加密后再同步到云端
- 主密码强度:主密码应足够强(推荐 Diceware 方式生成的 6+ 个单词)
9.4 自托管密码管理器
对于企业或安全意识高的个人用户,可以通过自托管密码管理器实现完全控制:
- Vaultwarden(Bitwarden 的 Rust 实现):极低资源消耗,支持 Docker 部署
- 需要配置 HTTPS、数据库备份、定期安全更新
9.5 密码管理器的使用规范
| 规范 | 说明 |
|---|---|
| 主密码 | ≥ 6 个 Diceware 单词 / ≥ 20 字符随机密码 |
| 双因素 | 开启密码管理器的 2FA(TOTP 或硬件密钥) |
| 自动填充 | 启用后注意防范钓鱼攻击(查看实际域名) |
| 定期审计 | 检查弱密码、复用密码、已泄露密码 |
| 备份 | 定期导出加密备份,存储在安全位置 |
| 更新 | 保持密码管理器软件为最新版本 |
十、各语言密码哈希最佳实践实现示例
10.1 Python:使用 Argon2
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher(
time_cost=3, # t 参数
memory_cost=65536, # m 参数(64 MB)
parallelism=4, # p 参数
hash_len=32, # 输出哈希长度
salt_len=16, # 盐长度
)
def hash_password(password: str) -> str:
"""返回 Argon2id 哈希字符串"""
return ph.hash(password)
def verify_password(password: str, hashed: str) -> bool:
"""验证密码,自动处理 rehash 需求"""
try:
return ph.verify(hashed, password)
except VerifyMismatchError:
return False
def needs_rehash(hashed: str) -> bool:
"""检查是否需要使用更新的参数重新哈希"""
return ph.check_needs_rehash(hashed)10.2 Node.js / TypeScript:使用 bcrypt
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 14; // 2026 年推荐 ≥ 14
export async function hashPassword(password: string): Promise<string> {
const salt = await bcrypt.genSalt(SALT_ROUNDS);
return bcrypt.hash(password, salt);
}
export async function verifyPassword(
password: string,
hash: string
): Promise<boolean> {
return bcrypt.compare(password, hash);
}10.3 Go:使用 Argon2id
package auth
import (
"crypto/rand"
"crypto/subtle"
"encoding/base64"
"fmt"
"golang.org/x/crypto/argon2"
)
type Argon2Params struct {
Memory uint32 // 64 * 1024 = 64MB
Iterations uint32 // 3
Parallelism uint8 // 4
SaltLength uint32 // 16
KeyLength uint32 // 32
}
var DefaultParams = &Argon2Params{
Memory: 64 * 1024,
Iterations: 3,
Parallelism: 4,
SaltLength: 16,
KeyLength: 32,
}
func GenerateFromPassword(password string, params *Argon2Params) (string, error) {
salt := make([]byte, params.SaltLength)
if _, err := rand.Read(salt); err != nil {
return "", err
}
hash := argon2.IDKey(
[]byte(password), salt,
params.Iterations, params.Memory,
params.Parallelism, params.KeyLength,
)
// 编码格式:$argon2id$v=19$m=65536,t=3,p=4$<base64-salt>$<base64-hash>
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
encodedHash := fmt.Sprintf(
"$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
params.Memory, params.Iterations, params.Parallelism,
b64Salt, b64Hash,
)
return encodedHash, nil
}
func ComparePasswordAndHash(password, encodedHash string) (bool, error) {
// 解析和验证的完整实现略
// 应使用 subtle.ConstantTimeCompare 进行常数时间比较
return subtle.ConstantTimeCompare([]byte(password), []byte(encodedHash)) == 1, nil
}10.4 Java:使用 Spring Security 的 DelegatingPasswordEncoder
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import java.util.HashMap;
import java.util.Map;
public class PasswordSecurity {
// 推荐使用 DelegatingPasswordEncoder 以实现平滑升级
public static PasswordEncoder createPasswordEncoder() {
String idForEncode = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder(14));
// 后续可以升级到 argon2,保留旧编码的解码能力
return new DelegatingPasswordEncoder(idForEncode, encoders);
}
public static void main(String[] args) {
PasswordEncoder encoder = createPasswordEncoder();
String rawPassword = "correct-horse-battery-staple";
String encoded = encoder.encode(rawPassword);
System.out.println(encoded); // {bcrypt}$2b$14$...
boolean matches = encoder.matches(rawPassword, encoded);
System.out.println(matches); // true
}
}10.5 Rust:使用 argon2 crate
use argon2::{
password_hash::{
rand_core::OsRng,
PasswordHash, PasswordHasher, PasswordVerifier, SaltString
},
Argon2
};
fn hash_password(password: &str) -> Result<String, argon2::password_hash::Error> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let hash = argon2.hash_password(password.as_bytes(), &salt)?;
Ok(hash.to_string())
}
fn verify_password(password: &str, hash: &str) -> Result<bool, argon2::password_hash::Error> {
let parsed_hash = PasswordHash::new(hash)?;
Ok(Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}10.6 Ruby on Rails:使用 bcrypt (has_secure_password)
# Gemfile
# gem 'bcrypt', '~> 3.1'
class User < ApplicationRecord
has_secure_password
validates :password, length: { minimum: 12 }
validates :password, not_pwned: true # 使用 pwned gem 检查泄露
end
# has_secure_password 自动提供:
# - password= 方法:自动加盐哈希(bcrypt cost = 12+)
# - authenticate 方法:常数时间验证
# - 不存储明文密码10.7 PHP:使用 password_hash (内置函数)
<?php
// PHP 5.5+ 内置 password_hash 默认使用 bcrypt
$options = [
'cost' => 14, // bcrypt cost 因子
];
// 哈希
$hash = password_hash('user_password', PASSWORD_BCRYPT, $options);
// 验证
if (password_verify('user_password', $hash)) {
echo "密码正确";
}
// 检查是否需要重新哈希
if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 14])) {
// 使用更安全的参数重新哈希
$newHash = password_hash('user_password', PASSWORD_BCRYPT, ['cost' => 14]);
}总结
密码安全是一个多层次的系统工程,涉及算法选择、架构设计、用户体验和运维策略。本文覆盖了从密码哈希到无密码认证的全链条安全考量。总结关键原则如下:
| 原则 | 核心要求 |
|---|---|
| 算法优先 | 新系统使用 Argon2id,对旧系统使用 bcrypt (cost ≥ 14) |
| 盐不重复 | 每个用户、每次密码变更使用 CSPRNG 生成新盐(≥ 16 字节) |
| 登录保护 | 渐进式延迟 + 临时锁定 + CAPTCHA + MFA |
| 策略宽松 | 长度优先于复杂度,强制检查泄露密码黑名单 |
| 凭证填充 | MFA 是最有效防御,辅以设备指纹和 IP 信誉评分 |
| 重置安全 | 随机 Token、短有效期、单次使用、通知机制 |
| 渐进步伐 | 预留 Passkey/WebAuthn 支持路径,逐步减少对密码的依赖 |
| 用户教育 | 推荐密码管理器,避免密码复用,引导使用密码短语 |
密码安全的最终目标不是"让密码不可破解"(这在理论上是不可行的),而是在攻击成本和系统资源的博弈中,将攻击者的成功率降低到可接受的水平。结合多因素认证和持续的安全监控,才能构建真正健壮的认证安全体系。