用户密码应该如何安全传输与存储

一、引言:密码是互联网世界的最后防线

在整个互联网安全体系中,密码是最常见但也最脆弱的那一环。 我们每天使用的几乎所有服务——邮箱、网银、社交平台、游戏、论坛——都依赖密码来识别用户身份。

密码本质上是一种“弱凭证”:

  • 它不能证明用户是谁,只能证明“知道某个秘密”
  • 一旦这个秘密泄露,攻击者就能完全冒充你

现代互联网攻击频繁且多样化,一次普通的密码泄露事件,往往会导致用户多个平台的账号一起“沦陷”。 要想守住这道防线,必须从传输阶段存储阶段两方面同时发力。

本文将从基础概念出发,逐层深入,到最后构建出现代网站应采用的完整密码安全体系。

二、密码为什么如此危险?

初看起来,密码就是一些字符组成的字符串。 但密码泄露的危害远比想象严重,主要体现在两个方面。

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 可以构成双重防线:

  1. TLS 层:传输安全
  2. hash 层:降低密码复用的连带风险

对于安全要求较高的系统(银行、企业级服务、开发者平台),常会采用这种方式。

五、服务器端应该如何存储密码?

接下来进入密码安全的核心: 密码永不以明文形式出现,即便在服务器内部。

行业标准包含三个关键词:

  1. hash
  2. salt
  3. 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)

双层含义:

  1. 即使 MITM 拿到 H1,也无法登录其他网站
  2. 即使数据库泄露拿到 H2,也很难反推 H1,更推不回 P
  3. 服务器永不接触明文密码,减少数据泄露面
  4. 日志、监控、报错信息都不会泄露 P

这种“前端 hash + 后端慢哈希”的双层方案,在一些高安全要求的系统中被采用,用来降低密码泄露带来的连带风险。

七、前端 hash 面临的“重放攻击”问题与解决方案

如果前端 hash 写成:

H1 = hash(P)

攻击者截获 H1 → 可反复使用 H1 伪装用户登录。 这与“截获明文密码”几乎一样危险。

解决方案是加入 随机 challenge(挑战值)

7.1 完整流程

  1. 用户访问登录页 服务器生成 challenge

    challenge = 随机字符串
    
  2. 浏览器收到 challenge 并计算

    H1 = hash(P + challenge)
    
  3. 服务器验证 服务器用相同的规则验证:

    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 不要允许弱密码

弱密码的破解速度是指数级快的:

  • 123456
  • password
  • qwerty
  • 用户手机号
  • 用户生日

应采用:

  • 黑名单词表(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
  • 登录异常检测
  • 密码泄露比对
  • 日志不记录敏感信息

最终实现的目标只有一个:

即便整个服务器被攻陷,攻击者仍然无法从系统中拿到任何用户的真实密码。