这是一个挺有意思的问题,很多技术面试中也会涉及。看似简单,但背后涉及到的密码学原理和安全实践却值得每一位开发者深入了解。

回答这个问题最核心的一句话是:因为服务端也不知道你的原密码是什么。如果服务端能告诉你原密码,那只能说明它采用了明文存储,这是严重的安全漏洞。
因此,一个健壮的系统在设计用户密码模块时,必须遵循“服务端不知晓用户明文密码”这一基本原则。这也引出了我们今天要讨论的重点:密码是如何被安全地存储和验证的。
注意:即便使用AI辅助开发,也需对生成的密码处理代码保持警惕,务必确认其采用了安全的哈希方案,而非危险的明文存储。
为什么服务端不知道你的原密码?
任何有经验的开发者都知道,密码绝对不能以明文形式直接存入数据库。明文存储的风险极高:
- 数据库一旦被盗,所有用户密码将直接暴露。
- 拥有数据库权限的内部人员可能恶意利用这些信息。
- 黑客入侵后可以轻而易举地获取全部用户凭证。
因此,密码在存储前必须经过不可逆的转换处理,这个处理的核心就是哈希算法。
哈希算法简介
哈希算法,也叫散列函数或摘要算法,它能将任意长度的输入数据映射为一个固定长度的、唯一的字符串(称为哈希值、散列值或消息摘要)。

哈希算法有两个关键特性,使其成为密码存储的理想选择:
- 不可逆性:无法从哈希值反推出原始输入。这是保障密码安全的核心。
- 确定性:相同的输入一定会产生相同的输出。
我们可以打一个形象的比喻:服务器存储的密码就像被切碎的土豆丝,无法复原成原来的土豆。而验证密码时,服务器只是把你新输入的“土豆”也切一遍,然后对比这两盘“土豆丝”是否一样。
哈希算法的分类
大致可以分为两类:
- 加密哈希算法:安全性高,能提供数据完整性保护和防篡改能力,但性能相对较差。例如 SHA-2、SHA-3、SM3 等,常用于安全要求高的场景。
- 非加密哈希算法:性能高,但安全性低,易受攻击。例如 CRC32、MurMurHash3 等,适用于对安全性无要求的场景。
此外,还有专门为密码设计的慢哈希算法,如 Bcrypt、Argon2 等,通过故意增加计算成本来对抗暴力破解。
为什么不推荐 MD5?
MD5 曾广泛用于密码加密,但现在已被完全弃用,主要原因如下:
- 抗碰撞性差:已被证明存在碰撞漏洞,不同输入可能产生相同输出。
- 哈希值过短:128位的哈希值容易被彩虹表攻击破解。
- 计算过快:现代硬件能快速进行大批量MD5计算,方便了暴力破解。
为什么需要加盐?
即使使用安全的哈希算法,直接存储密码的哈希值仍面临彩虹表攻击的风险。攻击者可以预先计算海量常用密码的哈希值形成查询表,从而快速“撞库”。
“加盐”(Salt)就是在密码的特定位置插入一个随机字符串,然后再进行哈希计算。这个盐值对每个用户都是独一无二的。
加盐的作用:
- 极大增加密码的复杂度,使彩虹表攻击失效(因为攻击者无法为每个盐值预计算庞大的彩虹表)。
- 即使两个用户使用了相同的密码,由于盐值不同,最终的哈希值也完全不同。

密码存储方案推荐
目前主流的密码存储方案有两种:
方案一:加密哈希算法 + Salt
例如使用 SHA-256 配合随机盐值。以下是 Java 示例代码:
String password = "123456";
String salt = "1abd1c";
// 创建SHA-256摘要对象
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update((password + salt).getBytes());
// 计算哈希值
byte[] result = messageDigest.digest();
// 将哈希值转换为十六进制字符串
String hexString = new HexBinaryAdapter().marshal(result);
System.out.println("Original String: " + password);
System.out.println("SHA-256 Hash: " + hexString.toLowerCase());
输出:
Original String: 123456
SHA-256 Hash: 424026bb6e21ba5cda976caed81d15a3be7b1b2accabb79878758289df98cbec
方案二:慢哈希算法(更推荐)
Bcrypt 是专为密码加密设计的慢哈希算法,它自动处理加盐,并包含一个可配置的 cost(成本)因子来控制计算强度,从而有效抵御暴力破解。
在 Spring Security 框架中,官方推荐使用 BCryptPasswordEncoder:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
登录验证流程
理解了存储,验证流程就非常清晰了:
- 用户登录时输入用户名和密码。
- 服务端根据用户名从数据库取出该用户对应的盐值和已存储的哈希值。
- 服务端将用户输入的密码与查出的盐值拼接,使用相同的哈希算法进行计算。
- 将计算出的新哈希值与数据库中存储的旧哈希值进行比对。
- 一致则登录成功,否则失败。
重置密码时如何判断新密码与旧密码相同?
有些系统在重置密码时会提示“新密码不能与旧密码相同”。这并非因为系统知道你的旧密码,其判断原理与登录验证完全相同:
- 用户提交新密码。
- 服务端使用该用户的盐值,计算新密码的哈希值。
- 将这个新哈希值与数据库中存储的旧密码哈希值进行比对。
- 如果两者相同,则说明新旧密码一致,拒绝修改。
所以,系统始终只是在比较两盘“土豆丝”是否一样,而从未见过“土豆”本身。
密码传输安全
存储安全解决了,那密码在从客户端到服务端的传输过程中是否安全呢?一个常见的面试问题是:如果内部人员知道加密方式,是否可能拦截传输数据并解密?
答案是,一个完整的方案会将存储安全与传输安全分开处理。
使用 HTTPS
HTTPS 是传输安全的基础,它通过 SSL/TLS 协议对传输通道进行加密。但仅依赖 HTTPS 仍不够:
- 存在降级攻击、中间人攻击等风险。
- 它无法防止客户端本身被恶意软件窃取输入。
密码加密传输
通常建议在客户端对密码进行非对称加密后再通过 HTTPS 传输。
- 对称加密:加解密使用同一密钥。

- 非对称加密:使用公钥加密,私钥解密。

对于密码传输,推荐流程如下:
- 服务端生成 RSA 密钥对,私钥保密存储,公钥下发给客户端。
- 客户端在提交前,先用公钥加密密码。
- 服务端收到密文后,用私钥解密获得明文密码,再进行后续的哈希加盐存储。
这样,即使数据在网络传输中被拦截,攻击者没有私钥也无法解密。而内部人员即便知道存储算法,拿到的也只是不可逆的哈希值。
完整的安全方案
综合来看,一个健壮的密码安全体系包含三层防护:
// 第一层:客户端非对称加密(保障传输内容安全)
const encryptedPassword = rsaEncrypt(password, publicKey);
// 第二层:HTTPS 安全通道(保障传输过程安全)
// 第三层:服务端哈希加盐存储(保障存储安全)
总结
回到最初的问题:为什么忘记密码时只能重置,不能告诉你原密码?
因为从安全设计上,服务端存储的只是密码的哈希值,而哈希算法是不可逆的。 这是现代密码学保障用户安全的基础设计。
如果一个网站能直接告诉你原密码,那是一个危险的红旗信号,表明它很可能在明文存储密码。请立即修改该网站的密码,并切勿在所有网站使用同一套密码,以免一个站点被攻破,全线账户沦陷。
对密码安全、哈希算法及更多后端开发实践感兴趣,欢迎在云栈社区与更多开发者交流探讨。