密码认证:静态密码的存储与传输安全、加盐哈希、密码策略与强度评估

密码认证,说白了就是最古老也最常用的身份验证方式。你输入一串字符,系统验证它是不是对的。听起来简单,但这里面的坑,我踩过不少。

我记得刚入行那会儿,有个项目直接把用户密码明文存数据库里。当时我觉得没什么问题,直到被安全审计骂了个狗血淋头。嗯,从那以后,我对密码的存储和传输就格外上心。

静态密码的存储安全

密码存哪儿?怎么存?这是第一个要解决的问题。

千万别存明文。这应该是常识了,但我见过太多初创公司把用户密码直接扔进数据库。一旦数据泄露,用户的账号就全完了。更可怕的是,很多人所有平台用同一个密码,你这边泄露了,人家银行账号也跟着遭殃。

正确的做法是:只存密码的哈希值。哈希是个单向函数,你从哈希值推不出原始密码。但光哈希还不够,你得加盐。

核心原则:永远不要存储用户密码的明文或可逆加密形式。只存储加盐后的哈希值。

为什么需要加盐?

不加盐的哈希有个大问题:彩虹表攻击。攻击者提前算好常见密码的哈希值,一比对就知道你密码是什么。加盐就是在每个密码后面拼一段随机字符串,再算哈希。这样就算两个用户密码一样,哈希值也不同。

我个人习惯用 16 字节以上的随机盐,每个用户单独生成。盐不需要保密,跟哈希值一起存就行。

bcrypt 与 argon2

现在主流推荐两个算法:bcrypt 和 argon2。我重点说说它们。

特性 bcrypt argon2
设计时间 1999 年 2015 年(PHC 优胜者)
抗 GPU 攻击 较好(内存占用固定) 优秀(可调内存/并行度)
抗 ASIC 攻击 一般 优秀(内存硬)
配置参数 cost factor(工作因子) 时间成本、内存成本、并行度
推荐场景 通用场景,兼容性好 新系统,对安全性要求极高

bcrypt 我用了很多年,稳定可靠。它的 cost factor 我一般设到 10-12,服务器响应时间还能接受。argon2 是新一代算法,拿过密码哈希竞赛冠军。我在一个新项目里试过,确实更安全,但配置参数多了点,需要调优。

我的建议:如果项目刚起步,用 bcrypt 就够了。如果对安全性有极致要求,或者用户量不大,上 argon2。别自己实现哈希算法,用现成的库。

代码示例(Node.js 用 bcrypt):

const bcrypt = require('bcrypt');
const saltRounds = 12;

// 注册时:哈希密码
const hash = await bcrypt.hash(password, saltRounds);
// 存 hash 到数据库

// 登录时:比对密码
const match = await bcrypt.compare(password, storedHash);
if (match) {
    // 密码正确
} else {
    // 密码错误
}

静态密码的传输安全

存好了,怎么传过来?

你想想看,用户在浏览器输入密码,然后通过网络发到服务器。如果中间被人截了,那存得再安全也没用。

第一道防线:HTTPS。这不用我说了吧?所有密码传输必须走 TLS 加密。我在项目中遇到过有人为了省事,在内部网络用 HTTP,结果被同事抓包抓到了测试账号的密码。尴尬不?

第二道防线:前端哈希?这个问题有争议。有些人说前端哈希可以防止明文密码泄露,但我个人不建议。为什么?因为前端哈希后,哈希值就成了新的“密码”,攻击者拿到哈希值照样能登录。而且前端哈希容易受 JavaScript 篡改影响。

注意:前端哈希不能替代 HTTPS。它最多算个辅助手段。真正的安全靠的是传输层加密和服务端加盐哈希。

我现在的做法是:前端不做任何密码变换,直接通过 HTTPS 发送明文密码到服务端。服务端收到后立即用 bcrypt/argon2 处理。这样最干净,也最可控。

密码策略与强度评估

用户设密码,你不能不管。太简单的密码,比如 "123456"、"password",跟没设一样。但管得太严,用户记不住,最后贴便签在显示器上,更不安全。

这里有个平衡点。我一般建议的策略:

  • 最小长度:8 位以上。12 位更安全。
  • 字符类型:至少包含大写、小写、数字、特殊字符中的三种。
  • 禁止常见密码:维护一个黑名单,包含 "password"、"123456"、"qwerty" 等。
  • 禁止与用户名相同:避免用户用邮箱前缀当密码。
  • 密码过期:这个有争议。我个人觉得 90 天强制改密码反而让用户设更弱的密码。不如只在检测到泄露时要求改。

强度评估怎么做?我习惯用 zxcvbn 这个库。它比传统的长度+字符类型判断准得多。传统方法会说 "P@ssw0rd" 很强,但 zxcvbn 知道它是个常见模式,评分很低。

避坑指南:我曾经在项目里只用了长度和字符类型判断,结果用户设了 "Password1!",系统说强,其实弱爆了。后来换成 zxcvbn,效果好很多。

密码强度评分示例(zxcvbn):

const zxcvbn = require('zxcvbn');
const result = zxcvbn('P@ssw0rd');
console.log(result.score); // 0-4,4 为最强
// 结果:1(很弱)
// 因为 "P@ssw0rd" 是常见模式

下面这张图,是我总结的密码认证核心流程。你看一眼,基本就全明白了。

密码认证核心流程 用户输入密码 HTTPS 传输 服务端接收密码 加盐哈希(bcrypt/argon2) 与数据库哈希比对 数据库存储 盐 + 哈希值 (不存明文) 返回认证结果

最后说一句,密码认证虽然基础,但做好不容易。从存储到传输,从算法选择到策略制定,每个环节都可能出问题。我见过太多因为密码存储不当导致的数据泄露事件,代价惨重。

嗯,这一章就到这儿。记住:加盐哈希是底线,HTTPS 是标配,密码策略要平衡安全与用户体验。做到这三点,你的密码认证基本就稳了。