一、引言:密码是互联网世界的最后防线
在整个互联网安全体系中,密码是最常见但也最脆弱的那一环。 我们每天使用的几乎所有服务——邮箱、网银、社交平台、游戏、论坛——都依赖密码来识别用户身份。
密码本质上是一种“弱凭证”:
- 它不能证明用户是谁,只能证明“知道某个秘密”
- 一旦这个秘密泄露,攻击者就能完全冒充你
现代互联网攻击频繁且多样化,一次普通的密码泄露事件,往往会导致用户多个平台的账号一起“沦陷”。 要想守住这道防线,必须从传输阶段和存储阶段两方面同时发力。
本文将从基础概念出发,逐层深入,到最后构建出现代网站应采用的完整密码安全体系。
二、密码为什么如此危险?
初看起来,密码就是一些字符组成的字符串。 但密码泄露的危害远比想象严重,主要体现在两个方面。
2.1 风险一:传输途中泄露
从用户输入密码,到密码抵达服务器,中间会经过:
- 浏览器
- 本地域名解析
- 操作系统网络栈
- 路由器
- 运营商网络
- 服务器负载均衡
只要链路中任何一个环节被攻击,密码就可能被拦截。
理论上,HTTPS 已经解决了“被监听问题”。 但实际情况比理论复杂得多,常见风险包括:
- 用户电脑被恶意软件植入假根证书(典型的中间人攻击手段)
- 公司/学校内网采用流量审计,自动解密 TLS
- 公共 Wi-Fi 通过 ARP 欺骗截获 HTTPS 并强制重新签发证书
- 浏览器插件注入恶意脚本读取用户输入
- 某些软件声称“优化网速”,实际插入监听模块
- 某些企业网关进行 SSL Inspection(合法但不安全)
换句话说:
你无法假设用户设备、网络环境永远是安全的。
因此,传输阶段仍有提升空间。
2.2 风险二:服务器端泄露
更多泄露事件来自服务器端,常见场景包括:
- 数据库被拖库(最常见)
- 日志中意外打印了用户密码
- 开发者临时调试时把密码输出到控制台
- 备份文件未加密,被误传到公共服务器
- 第三方监控系统抓取了敏感字段
- 代码漏洞(SQL 注入、RCE)导致数据库暴露
- 运维人员内部滥用权限
如果服务器保存的是 明文密码,后果极其严重:
- 所有用户密码全部泄露
- 用户在其他网站的账号也会被攻击(密码复用)
- 一旦攻击者能登录用户邮箱,后果会更加连锁扩大
因此,服务器端必须确保“即使泄露,攻击者也不能从数据库中拿到真正密码”。
三、密码应该如何在网络中安全传输?
3.1 最朴素的方案:HTTPS 直接发送密码
这是目前几乎所有网站的默认做法:
浏览器 ——HTTPS——> 服务器
优点:
- 简单
- 成熟
- 性能高
- 兼容性好
缺点是: 它把所有风险完全寄托在 TLS 的可靠性上。
如果 TLS 被中间人攻击破坏,密码也会被明文劫持。
于是有人提出:是否应该 在浏览器端先对密码做一次 hash?
四、前端 hash:可降低的风险与无法降低的风险
前端 hash 的核心思想是:
即便传输被监听,攻击者拿到的也不是明文密码。
假设:
P = 用户密码
H1 = hash(P)
浏览器发送 H1,而非 P。
4.1 前端 hash 能防护什么?
- 避免 MITM 直接获得用户真实密码
- 服务器日志、监控系统误输出时不会泄露明文密码
- 攻击者得到 H1 也无法用它登录用户在其他网站的账号
- 即使用户复用密码,也不会因为你的网站泄露而波及其他网站(很重要)
- 服务器可以完全不接触明文密码(零知识认证的一种初级形态)
前端 hash 的意义在于:
“就算你的网站被攻破,也不能泄露用户在其他网站的密码。”
这是传统方案做不到的。
4.2 前端 hash 不能防护什么?
- 攻击者可以直接使用 H1 登录你的网站(因为你把它当成凭证)
- 用户电脑中毒、键盘监听仍能拿到 P
- 不带 challenge 的 hash 会被重放攻击利用
- 不能替代 HTTPS(hash 后的结果也需要加密)
换句话说:
前端 hash 能减少损失,但不能消灭风险。
4.3 前端 hash 是否必要?
实践中,它属于 可选但增益明确 的安全措施。
如果实现得当(通过 challenge 防重放),前端 hash 可以构成双重防线:
- TLS 层:传输安全
- hash 层:降低密码复用的连带风险
对于安全要求较高的系统(银行、企业级服务、开发者平台),常会采用这种方式。
五、服务器端应该如何存储密码?
接下来进入密码安全的核心: 密码永不以明文形式出现,即便在服务器内部。
行业标准包含三个关键词:
- hash
- salt
- slow hash(慢哈希)
5.1 哈希算法:从可逆变成不可逆
一个好的哈希函数应具备:
- 碰撞概率极低
- 输入变化一位,输出完全不同(雪崩效应)
- 无法从输出反推输入
服务器保存的不是密码,而是:
H = hash(P)
如果数据库泄露,攻击者无法直接得到密码。
5.2 盐(Salt):阻止彩虹表和批量破解
没有盐,所有用户的弱密码会产生相同的哈希。攻击者可以:
- 用现成彩虹表匹配
- 批量推测密码(只需对每种常见密码算一次 hash)
加入随机盐:
H = hash(P + salt)
每个用户的 salt 都不同,这意味着:
- 两个“123456”的用户哈希也完全不同
- 彩虹表无效
- 攻击者必须针对每个用户分别尝试(成本剧增)
salt 必须:
- 足够长(16 字节以上)
- 随机(CSPRNG)
- 每个用户独立生成
- 不加密,明文存储在数据库中
5.3 慢哈希(Slow Hash):真正提升攻击成本
SHA256 这类传统算法太快了。 现代 GPU 每秒可计算数十亿次 SHA256。
这意味着:
如果使用 SHA256 直接存密码,即便加盐,攻击者仍然可以在短时间内撞库。
因此必须使用专门为密码存储设计的慢哈希算法,例如:
| 算法 | 特点 |
|---|---|
| bcrypt | 经典方案,可靠成熟 |
| scrypt | 抗 GPU,内存占用高 |
| Argon2(推荐) | 密码学大赛冠军,可调节 CPU、内存、并行度 |
慢哈希的目标并不是“更安全的哈希”,而是:
让每次计算都变得昂贵,使攻击者无法进行大规模暴力破解。
工程实践中,登录速度影响不大,因为:
- 用户登录次数相对少
- 每次 slow-hash 只需几十毫秒
- 攻击者无法用同样的速度尝试数十亿次密码
六、双层保护:前端 hash + 后端 slow-hash
综合前端与后端:
P → H1 = hash1(P) → (传输) → H2 = bcrypt(H1 + salt)
双层含义:
- 即使 MITM 拿到 H1,也无法登录其他网站
- 即使数据库泄露拿到 H2,也很难反推 H1,更推不回 P
- 服务器永不接触明文密码,减少数据泄露面
- 日志、监控、报错信息都不会泄露 P
这种“前端 hash + 后端慢哈希”的双层方案,在一些高安全要求的系统中被采用,用来降低密码泄露带来的连带风险。
七、前端 hash 面临的“重放攻击”问题与解决方案
如果前端 hash 写成:
H1 = hash(P)
攻击者截获 H1 → 可反复使用 H1 伪装用户登录。 这与“截获明文密码”几乎一样危险。
解决方案是加入 随机 challenge(挑战值)。
7.1 完整流程
-
用户访问登录页 服务器生成 challenge
challenge = 随机字符串 -
浏览器收到 challenge 并计算
H1 = hash(P + challenge) -
服务器验证 服务器用相同的规则验证:
bcrypt(H1 + salt) == stored_hash
7.2 这样做的好处
- H1 每次不同(因为 challenge 不同)
- 攻击者截获 H1 不能重放
- MITM 攻击的价值下降
- 不需要在后端存储 challenge,只需验证一次后丢弃即可
7.3 常见工程问题
- challenge 需要避免被缓存(设置 no-cache 响应头)
- challenge 不能太短(至少 16 字节以上)
- 前端必须使用安全的哈希函数(SHA256 或以上)
- 必须配合 HTTPS,否则 challenge 也会被劫持
八、密码安全的若干补充实践(常被忽略)
以下是许多工程团队常忽略但非常重要的细节。
8.1 永远不要通过邮件发送用户密码
“重置密码并邮件发送新密码”这种做法极其危险。 正确方式是:
- 邮件发送重置链接
- 链接带一次性 token
- 用户必须重新设置密码
8.2 不要允许弱密码
弱密码的破解速度是指数级快的:
123456passwordqwerty- 用户手机号
- 用户生日
应采用:
- 黑名单词表(top 10k 最常见密码)
- 长度策略(≥12 字符)
- 强制使用密码管理器提示
8.3 多因素身份验证(MFA / 2FA)
密码只是第一层。 加入第二层身份验证可以极大降低攻击成功率:
- TOTP(Google Authenticator)
- 短信验证码(弱,但仍有意义)
- 硬件密钥(FIDO2 / U2F)
8.4 账号保护机制
- 登录失败次数过多 → 暂时锁定
- 在可疑设备登录 → 发送提醒邮件
- 登录 IP/UA 异常 → 要求二次验证
- 支持用户查看“最近登录设备记录”
8.5 密码泄露检测(HIBP API)
大型网站会在用户设置新密码时检查:
- 该密码是否出现在大型泄露数据库中(如 HaveIBeenPwned)
这能显著降低弱密码带来的风险。
九、现代密码体系的目标
通过本文的 step-by-step 介绍,我们可以建立一套现代密码工程的完整体系:
9.1 传输安全
- 使用 HTTPS
- 必要时使用前端 hash 防止密码复用带来的连锁风险
- 使用 challenge 防止重放
9.2 存储安全
- 永不存储明文密码
- 每个用户使用独立 salt
- 使用 slow-hash
9.3 账号安全机制
- 强密码策略
- MFA
- 登录异常检测
- 密码泄露比对
- 日志不记录敏感信息
最终实现的目标只有一个:
即便整个服务器被攻陷,攻击者仍然无法从系统中拿到任何用户的真实密码。