如何在数据库中安全地存储用户密码
如何在数据库中安全地存储用户密码
账号密码登录是最常见的身份认证方式之一,但“能登录”并不代表“存得安全”。很多系统在功能上完成了登录,实际上却把用户密码暴露在了极高风险之下。
这篇文章想回答一个基础但非常重要的问题:如果要在数据库里存储用户密码,什么做法才算相对安全?
最糟糕的做法:直接存明文密码
最早也最危险的实现方式,是直接把用户名和明文密码存进数据库。
登录时,后端执行类似这样的查询:
1 | select count(*) |
如果能查到记录,就认为密码正确。
这种做法的问题非常明显:
- 数据库一旦泄露,所有用户的密码都会被直接看到;
- 数据库管理员、运维人员或有读取权限的人也能轻易查看密码;
- 很多用户会在多个网站复用密码,一次泄露可能波及多个平台;
- 即使没有人恶意利用,要求用户全量修改密码本身也是巨大的成本。
所以,密码绝不能以明文形式存储。 这是最基本的安全常识。
第二阶段:只存密码的 MD5 值
很多系统会进一步改进,改成在数据库里存储密码的 MD5 值,而不是明文。
登录时,先对用户输入的密码做 MD5,再和数据库中的 MD5 值比对:
1 | select count(*) |
相比明文存储,这确实前进了一步,因为数据库里不再直接出现用户原始密码。但从今天的安全标准来看,这仍然是不安全的。
为什么单独使用 MD5 不安全?
因为像 MD5 这样的快速哈希算法,存在两个很大的问题:
- 同样的输入总会得到同样的输出;
- 计算速度太快,攻击者可以大规模批量尝试。
这意味着攻击者完全可以预先建立大量“常见密码 → MD5 值”的映射表,也就是常说的彩虹表。数据库一旦泄露,很多弱密码几乎可以被秒级还原。
例如,像 123456、admin123、password 这种常见密码,其哈希值通常早就被收录在公开数据库里了。攻击者拿到哈希之后,不需要“解密”,只需要查表或暴力尝试,就有很高概率还原出原密码。
这里还要特别澄清一个常见误区:
MD5 不是“加密”,而是哈希。
加密算法设计出来是为了“可逆”,也就是有机会解密回来;而哈希本身是不可逆设计。但“不可逆”不等于“安全”。如果哈希算法太快、输入空间又不够大,攻击者照样可以通过枚举和碰撞手段把大量密码猜出来。
第三阶段:哈希 + 随机盐值
再进一步,很多系统会采用“密码哈希 + 随机盐值(salt)”的方式。
它的大致流程是这样的。
注册时
- 用户提交明文密码;
- 系统为该用户生成一段随机盐值;
- 将“密码 + 盐值”组合后做哈希;
- 把哈希结果和盐值一起存入数据库;
- 不保存明文密码。
登录时
- 根据用户名查出数据库中的盐值和密码哈希;
- 用用户刚输入的密码和同一份盐值重新计算哈希;
- 将新计算结果与数据库中的哈希值比较;
- 一致则登录成功,否则失败。
这种方案比“纯 MD5”安全得多,因为它至少解决了一个核心问题:不同用户即使使用了相同密码,也不会得到相同的存储结果。
这样一来,彩虹表的价值就被大幅削弱了。攻击者即使拿到了数据库,也不能再直接用一套公共映射表批量还原所有密码,而必须针对每个用户逐个尝试。
但这还不够:为什么今天更推荐 bcrypt / scrypt / Argon2?
如果文章只停在“MD5 + 随机盐值”,放在很多年前还算及格,但按今天的标准已经不够好了。
原因很简单:现代密码存储不只是要防查表,还要故意让破解过程变慢。
MD5、SHA1、SHA256 这些通用哈希算法设计目标之一就是“快”。这对文件校验、摘要计算是好事,但对密码存储反而是坏事。
因为攻击者最喜欢的就是“快”。
如果一次猜测只需要极短时间,那么 GPU、ASIC 或大规模并行计算就能在有限时间里尝试海量密码组合。即使你给 MD5 或 SHA256 加了盐,也只是让攻击不能直接查公共彩虹表,但并没有从根本上提高“每次试错”的成本。
这就是为什么现在更推荐专门用于密码存储的算法:
- bcrypt
- scrypt
- Argon2
它们和 MD5 / SHA256 最大的区别在于:它们是故意设计得比较慢的。
更进一步,像 scrypt 和 Argon2 还会提高内存消耗,让大规模并行破解变得更贵。
一个更符合现代实践的结论
如果今天要做一个新系统,我的建议非常直接:
- 不要明文存密码;
- 不要只做一次 MD5 或 SHA256;
- 优先使用成熟库提供的 bcrypt、scrypt 或 Argon2;
- 不要自己发明密码存储算法。
这点很重要。很多人会觉得:
- 我把密码先 MD5 一次;
- 再把盐值也哈希一次;
- 然后把两个结果拼接;
- 最后再来一次 SHA256;
这样是不是就比 bcrypt 更安全?
通常不是。
自己设计的组合算法往往只有两种结果:
- 增加实现复杂度和维护成本;
- 产生一些自己意识不到的安全问题。
而成熟的密码哈希算法和现成实现,已经经过了长期实践和安全研究验证。对于大多数业务系统来说,正确使用成熟方案,远比“自己设计一套看起来很复杂的方案”更可靠。
一个简化的正确思路
如果用伪代码表达,一个现代系统的密码存储逻辑更像这样:
注册
1 | hash = password_hash(userPassword) |
登录
1 | hash = load_hash_by_username(username) |
注意这里往往不需要你自己手动管理 salt,因为像 bcrypt / Argon2 这类方案通常会把盐值和算法参数一起编码进最终结果里,由框架或库帮你处理。
也就是说,很多时候你真正该做的是:选对算法,调用成熟库,别自己乱造轮子。
还需要注意哪些问题?
密码安全不只是“怎么存”,还包括一整套配套策略。
1. 不要把密码写进日志
有些系统在调试登录失败时,会把请求参数完整打印到日志里。如果里面包含明文密码,那你虽然没把密码存进数据库,却把它存进了日志系统。
这同样危险。
2. 不要把前端哈希当成后端安全
有些人会说:前端先把密码 MD5 一下,再传给后端,不就安全吗?
这并不能替代后端安全存储。
因为前端传过来的“哈希后密码”,在很多场景里已经等价于新的明文密码了。攻击者如果截获这个值,依然可能拿它直接登录。
3. 要有密码强度策略
即使你用了再好的存储算法,用户如果设置的是 123456、111111 这种弱密码,系统整体安全性仍然很差。
所以还需要配合:
- 最低密码长度要求;
- 黑名单弱密码检测;
- 必要时启用二次验证。
4. 登录接口要限制暴力尝试
密码即使安全存储了,登录接口如果允许无限次尝试,也一样会被撞库或暴力破解。
常见的补充措施包括:
- 限制单位时间内的失败次数;
- 对异常 IP 或账号增加风控;
- 增加验证码或二次验证;
- 监控异常登录行为。
最后给一个实用 checklist
如果你正在设计一个用户系统,可以快速自查:
- 数据库里绝不保存明文密码
- 不使用纯 MD5 / SHA1 / SHA256 直接存密码
- 使用 bcrypt / scrypt / Argon2 等专用密码哈希算法
- 使用成熟框架或安全库,不手写密码存储逻辑
- 不在日志中记录用户密码
- 设置基础密码强度规则
- 对登录失败和异常尝试做限流或风控
总结
密码存储这件事,看起来只是数据库里的一个字段设计,实际上影响的是整个账号体系最基础的安全底线。
从明文,到纯 MD5,再到“加盐哈希”,这是一个很自然的认知演进过程;但如果按今天的实践标准来看,更合理的选择已经很明确了:
使用专门的密码哈希算法,把破解成本尽可能抬高,并且尽量依赖成熟方案而不是自己发明。
只要记住一句话就够了:
存密码的目标,不是“看起来不像明文”,而是“即使数据库泄露,也尽量让攻击者难以利用”。





