|
| 1 | +--- |
| 2 | +title: 为什么忘记密码时只能重置,不能告诉你原密码? |
| 3 | +description: 详细解答为什么忘记密码时网站只能让你重置密码,而不能告诉你原密码。核心原因是服务端使用哈希算法存储密码,哈希算法不可逆,无法从哈希值还原出原始密码。本文还介绍了密码存储安全、加盐机制、Bcrypt 加密、密码传输安全等知识。 |
| 4 | +category: |
| 5 | + - 系统设计 |
| 6 | +tag: |
| 7 | + - 数据安全 |
| 8 | + - 密码安全 |
| 9 | + - 哈希算法 |
| 10 | + - 面试题 |
| 11 | +head: |
| 12 | + - - meta |
| 13 | + - name: keywords |
| 14 | + content: 密码重置,密码找回,哈希算法,密码存储,Bcrypt,加盐,密码安全,面试题 |
| 15 | +--- |
| 16 | + |
| 17 | +这是一个挺有意思的问题,很多公司也在面试中问过。挺简单的,不知道大家平时在重置密码的时候有没有想过这个问题。 |
| 18 | + |
| 19 | + |
| 20 | + |
| 21 | +回答这个问题其实就一句话:**因为服务端也不知道你的原密码是什么**。存原密码的程序员已经被开了 🤣。 |
| 22 | + |
| 23 | +如果服务端知道你的原密码,那就是严重的安全风险问题了。 |
| 24 | + |
| 25 | +我们这里来简单分析一下。 |
| 26 | + |
| 27 | +这篇文章不会谈论太多加密算法相关的内容,感兴趣的朋友可以看这篇文章:[常见加密算法总结](https://javaguide.cn/system-design/security/encryption-algorithms.html)。 |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +## 为什么服务端不知道你的原密码? |
| 32 | + |
| 33 | +做过开发的应该都知道,服务端在保存密码到数据库的时候,**绝对不能直接明文存储**。 |
| 34 | + |
| 35 | +如果明文存储的话,风险太大: |
| 36 | + |
| 37 | +1. 数据库数据有被盗的风险 |
| 38 | +2. 有数据库权限的内部人员可能恶意利用 |
| 39 | +3. 黑客入侵后可以直接获取所有用户密码 |
| 40 | + |
| 41 | +因此,密码必须经过处理后才能存储。这个处理方式就是使用**哈希算法**。 |
| 42 | + |
| 43 | +## 哈希算法简介 |
| 44 | + |
| 45 | +哈希算法也叫散列函数或摘要算法,它的作用是对任意长度的数据生成一个固定长度的唯一标识,也叫哈希值、散列值或消息摘要(后文统称为哈希值)。 |
| 46 | + |
| 47 | + |
| 48 | + |
| 49 | +哈希算法有两个关键特点: |
| 50 | + |
| 51 | +1. **不可逆性**:你无法通过哈希之后的值再得到原值。这是核心! |
| 52 | +2. **确定性**:相同的输入永远产生相同的输出。 |
| 53 | + |
| 54 | +有个很形象的比喻:**你存的密码就像切过的土豆丝,不能被复原成土豆。但网站判断密码是否正确的方式,就是把你输入的新密码当成土豆再切一次,看看这两盘土豆丝是不是一样的。** |
| 55 | + |
| 56 | +这两个特点决定了哈希算法非常适合用于密码存储:服务端只存储密码的哈希值,验证时只需比较哈希值是否一致。 |
| 57 | + |
| 58 | +### 哈希算法的分类 |
| 59 | + |
| 60 | +哈希算法可以简单分为两类: |
| 61 | + |
| 62 | +1. **加密哈希算法**:安全性较高的哈希算法,它可以提供一定的数据完整性保护和数据防篡改能力,能够抵御一定的攻击手段,安全性相对较高,但性能较差,适用于对安全性要求较高的场景。例如 SHA2、SHA3、SM3、RIPEMD-160、BLAKE2等等。 |
| 63 | +2. **非加密哈希算法**:安全性相对较低的哈希算法,易受到暴力破解、冲突攻击等攻击手段的影响,但性能较高,适用于对安全性没有要求的业务场景。例如 CRC32、MurMurHash3等等。 |
| 64 | + |
| 65 | +除了这两种之外,还有一些特殊的哈希算法,例如安全性更高的**慢哈希算法**。 |
| 66 | + |
| 67 | +### 为什么不推荐 MD5? |
| 68 | + |
| 69 | +早期常用 MD5 来加密密码,但现在已经**不被推荐**,原因如下: |
| 70 | + |
| 71 | +1. **抗碰撞性差**:存在弱碰撞问题,即多个不同的输入可能产生相同的 MD5 值。 |
| 72 | +2. **哈希值较短**:128 位的哈希值容易被彩虹表攻击。 |
| 73 | +3. **计算速度太快**:反而容易被暴力破解。 |
| 74 | + |
| 75 | +详细介绍可以阅读这篇文章:[简历别再写 MD5 加密密码了!](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247542780&idx=1&sn=fb2fe3fb53fe596cc5b22e30766e0098&scene=21#wechat_redirect) |
| 76 | + |
| 77 | +### 为什么需要加盐? |
| 78 | + |
| 79 | +单纯使用哈希算法存储密码,仍然存在被**彩虹表攻击**的风险。彩虹表是一种预先计算好的哈希值对照表,攻击者可以通过查表的方式快速破解密码。 |
| 80 | + |
| 81 | +盐(Salt)在密码学中,是指通过在密码任意固定位置插入特定的字符串,让哈希后的结果和使用原始密码的哈希结果不相符,这种过程称之为"加盐"。 |
| 82 | + |
| 83 | +**加盐的作用**: |
| 84 | + |
| 85 | +1. 增加密码的复杂度和唯一性。 |
| 86 | +2. 使得彩虹表攻击失效(每个用户的盐都不同)。 |
| 87 | +3. 即使两个用户使用相同密码,哈希值也不同。 |
| 88 | + |
| 89 | +## 密码存储方案推荐 |
| 90 | + |
| 91 | +目前推荐的密码存储方案有两种: |
| 92 | + |
| 93 | +### 方案一:加密哈希算法 + Salt |
| 94 | + |
| 95 | +使用安全性较高的加密哈希算法(如 SHA-256、SHA-3)加上盐值。 |
| 96 | + |
| 97 | +SHA-256 + Salt 示例代码: |
| 98 | + |
| 99 | +```java |
| 100 | +String password = "123456"; |
| 101 | +String salt = "1abd1c"; |
| 102 | +// 创建SHA-256摘要对象 |
| 103 | +MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); |
| 104 | +messageDigest.update((password + salt).getBytes()); |
| 105 | +// 计算哈希值 |
| 106 | +byte[] result = messageDigest.digest(); |
| 107 | +// 将哈希值转换为十六进制字符串 |
| 108 | +String hexString = new HexBinaryAdapter().marshal(result); |
| 109 | +System.out.println("Original String: " + password); |
| 110 | +System.out.println("SHA-256 Hash: " + hexString.toLowerCase()); |
| 111 | +``` |
| 112 | + |
| 113 | +输出: |
| 114 | + |
| 115 | +```bash |
| 116 | +Original String: 123456 |
| 117 | +SHA-256 Hash: 424026bb6e21ba5cda976caed81d15a3be7b1b2accabb79878758289df98cbec |
| 118 | +``` |
| 119 | + |
| 120 | +### 方案二:慢哈希算法(更推荐) |
| 121 | + |
| 122 | +**Bcrypt** 是专门为密码加密而设计的哈希算法,属于慢哈希算法。它内置了 salt 机制和 cost(成本)参数: |
| 123 | + |
| 124 | +- **salt**:随机生成的字符串,用于和密码混合,增加密码的唯一性 |
| 125 | +- **cost**:控制迭代次数,增加计算时间和资源消耗 |
| 126 | + |
| 127 | +Bcrypt 可以有效防止彩虹表攻击和暴力破解攻击。 |
| 128 | + |
| 129 | +Java 应用程序的安全框架 Spring Security 官方推荐使用 `BCryptPasswordEncoder`: |
| 130 | + |
| 131 | +```java |
| 132 | +@Bean |
| 133 | +public PasswordEncoder passwordEncoder(){ |
| 134 | + return new BCryptPasswordEncoder(); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +## 登录验证流程 |
| 139 | + |
| 140 | +当你输入密码登录时,验证流程如下: |
| 141 | + |
| 142 | +1. 服务端根据用户名从数据库取出该用户的盐值和存储的哈希值。 |
| 143 | +2. 服务端将用户输入的密码与盐值拼接,计算哈希值。 |
| 144 | +3. 比较计算出的哈希值与数据库中存储的哈希值是否一致。 |
| 145 | +4. 如果一致,说明密码正确;否则密码错误。 |
| 146 | + |
| 147 | + |
| 148 | + |
| 149 | +## 重置密码时如何判断新密码与旧密码相同? |
| 150 | + |
| 151 | +细心的同学可能发现,有些网站在重置密码时会提示"新密码不可与旧密码相同"。那网站是怎么知道新密码和旧密码相同的呢? |
| 152 | + |
| 153 | +其实原理和验证密码正确性一样: |
| 154 | + |
| 155 | +1. 用户输入新密码。 |
| 156 | +2. 服务端用该用户的盐值,计算新密码的哈希值。 |
| 157 | +3. 将新密码的哈希值与数据库中存储的旧密码哈希值比较。 |
| 158 | +4. 如果相同,说明新密码和旧密码一样,拒绝修改。 |
| 159 | + |
| 160 | +所以网站并不知道你的旧密码是什么,只是比较了两盘"土豆丝"是否一样。 |
| 161 | + |
| 162 | +## 密码传输安全 |
| 163 | + |
| 164 | +前面讲的都是密码在服务端的存储安全,那密码在传输过程中安全吗? |
| 165 | + |
| 166 | +有个常见的面试问题:**如果某个员工知道加密方式,那岂不是他可以在私下或者离职后拦截包然后模拟加密从而获取密码?** |
| 167 | + |
| 168 | +答案是:**存储与传输本身就是分开处理的**。 |
| 169 | + |
| 170 | +完整的密码安全方案需要同时保障存储安全和传输安全。 |
| 171 | + |
| 172 | +### 使用 HTTPS |
| 173 | + |
| 174 | +HTTPS 协议是保障传输安全的基础。HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 则是运行在 SSL/TLS 之上的 HTTP 协议,所有传输的内容都经过加密。 |
| 175 | + |
| 176 | +关于 HTTP 和 HTTPS 的详细对比可以看这篇文章:[HTTP vs HTTPS(应用层)](https://javaguide.cn/cs-basics/network/http-vs-https.html)。 |
| 177 | + |
| 178 | +**但是,仅仅依赖 HTTPS 还不够安全**: |
| 179 | + |
| 180 | +1. HTTPS 存在降级攻击、中间人攻击等风险 |
| 181 | +2. HTTPS 只能保证传输过程中第三方抓包看到的是密文,无法防范客户端本身的恶意行为 |
| 182 | + |
| 183 | +因此,我们还需要对密码进行**加密后再传输**。 |
| 184 | + |
| 185 | +### 密码加密传输 |
| 186 | + |
| 187 | +加密算法分为**对称加密**和**非对称加密**两大类。 |
| 188 | + |
| 189 | +**对称加密**是指加密和解密使用同一个密钥的算法,也叫共享密钥加密算法。 |
| 190 | + |
| 191 | + |
| 192 | + |
| 193 | +**非对称加密**是指加密和解密使用不同密钥的算法,也叫公开密钥加密算法。这两个密钥一个称为公钥(可公开),另一个称为私钥(需保密)。用公钥加密的数据只能用对应的私钥解密,反之亦然。 |
| 194 | + |
| 195 | + |
| 196 | + |
| 197 | +常见的非对称加密算法有 RSA、DSA、ECC 等。 |
| 198 | + |
| 199 | +对于密码传输这一场景,**推荐使用非对称加密**。完整流程如下: |
| 200 | + |
| 201 | +1. 服务端生成公私钥对,私钥严格保密存储在服务端,公钥下发到客户端 |
| 202 | +2. 客户端传输密码前,使用公钥加密密码 |
| 203 | +3. 服务端收到加密数据后,用私钥解密获取原始密码 |
| 204 | +4. 服务端对原始密码进行哈希处理、加盐后存储 |
| 205 | + |
| 206 | +### 完整的安全方案 |
| 207 | + |
| 208 | +综合存储和传输,一个完整的密码安全方案包含三层: |
| 209 | + |
| 210 | +```javascript |
| 211 | +// 第一层:客户端加密(非对称加密传输) |
| 212 | +const encryptedPassword = rsaEncrypt(password, publicKey); |
| 213 | + |
| 214 | +// 第二层:HTTPS 安全传输 |
| 215 | +// 第三层:服务端存储(哈希 + 盐值) |
| 216 | +``` |
| 217 | + |
| 218 | +所以,即使内部员工知道加密算法,他也只能拿到: |
| 219 | + |
| 220 | +- 传输层:非对称加密后的密文(无私钥无法解密) |
| 221 | +- 存储层:哈希后的摘要(哈希不可逆,无法还原) |
| 222 | + |
| 223 | +这两层保护确保了密码在全链路的安全性。 |
| 224 | + |
| 225 | +## 总结 |
| 226 | + |
| 227 | +回到最初的问题:为什么忘记密码时只能重置,不能告诉你原密码? |
| 228 | + |
| 229 | +因为服务端存储的是密码经过哈希算法处理后的值,**哈希算法是不可逆的**,无法从哈希值还原出原始密码。这是密码安全的基本原则。 |
| 230 | + |
| 231 | +如果一个网站能够告诉你原密码,那说明它**明文存储了密码**,这是严重的安全隐患,建议立即修改密码并远离该网站。 |
| 232 | + |
| 233 | +**更重要的是**:如果你在所有网站都用了相同的密码,一个不靠谱的网站泄漏了你的密码,就相当于你所有的账户都面临风险。所以,**不要在所有网站使用相同密码**! |
0 commit comments