大家好,我是二哥呀。
从星球嘉宾 Guide 哥那里看到一则炸裂的消息,说 Facebook 的母公司 Meta 由于使用明文存储密码,导致被爱尔兰罚款 7.11 亿人民币。
是不是非常离谱?在不少小伙伴的刻板印象里,外企大厂总是福利好、技术好的代名词,然而还是犯这种低级错误了。
对密码加密其实属于一个非常初级的开发常识了,但意外的是我也经常碰到一些球友找我要技术派的注册密码,说自己忘记了,但讲真我真不知道啊,因为技术派的密码都是哈希加密后存储的(🤣),作为站长的我也不知道啊。
给大家看一下我本地的用户密码吧,存到数据库里其实是这样的:
password 这一栏是不可逆的,因为经过了哈希后加密。注意,加密是可逆的,因为有加密就有解密;但哈希是一种单向函数,相当于它把输入的数据转换成了固定长度的“指纹”,整个过程是不可逆的。
有些初学的小伙伴会把这里的哈希函数和 HashMap 中的哈希函数关联起来,会觉得那么简单的哈希结果(int) 适合拿来处理密码吗?
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
我这里必须强调的一点是,HashMap 中的 hash 函数是为了快速查找,它追求的是把键值均匀的散列在数组中,而用于处理密码的哈希算法具备这样的特点:
如果输入发生了一点改变,由此产生的哈希值会完全不同。
只有加密哈希算法才可以用来进行密码哈希加密,像 SHA256、SHA512、RIPEMD 和 WHIRLPOOL 都是加密哈希函数。
技术派中的加密算法用的是 Spring 的 DigestUtils.md5DigestAsHex()
方法:
MD5 由密码学家 Ronald Linn Rivest 设计,于 1992 年公开,但现在被证实无法防止碰撞攻击,因此更推荐使用 BCrypt 算法。
BCrypt 属于慢哈希算法,可以用来抵抗暴力破解和彩虹表攻击,如果系统已经集成 Spring Security 的话,可以直接使用其提供的 BCryptPasswordEncoder 工具类来完成密码的哈希算法。
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
看过该类源码的小伙伴应该了解它内部其实使用了一种加盐技术:通过在密码中加入一段随机字符串再进行哈希加密,这个被加的字符串称之为盐值。
public String encode(CharSequence rawPassword) {
String salt = getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
技术派中的加密其实也用到了加盐技术。当然了,盐值最好每次都是随机的,这样就会让相同的密码每次加密都会生成完全不同的密文字符串。
因为攻击者无法事先知道盐值,所以也就没办法预先计算查询表和彩虹表,从而让反向查表法攻击无法奏效。
①、反向查表法攻击的原理是,攻击者预先计算出常用密码的哈希值,然后将这些哈希值与存储在数据库中的哈希密码进行比对,从而找出对应的原始密码。
②、彩虹表是一种特殊的查表法,不是直接存储所有可能的密码和其对应的哈希值,而是通过压缩存储链条(即不同的哈希值之间的映射关系),在有限的存储空间内保留大量的哈希和密码对。
那通过加盐,就能很巧妙的解决这两种攻击,因为哈希后的密文每次都不一样,破解起来非常消耗算力:
hash("hello") = 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
hash("hello" + "QxLUF1bgIAdeQX") = 9e209040c863f84a31e719795b2577523954739fe5ed3b58a75cff2127075ed1
hash("hello" + "bv5PehSMfV11Cd") = d1d3ec2e6f20fd420d50e2642992841d8338a314b8ea157c9e18477aaef226ab
hash("hello" + "YYLmfY6IehjZMQ") = a49670c3c18b9e079b9cfaf51634f563dc8ae3070db2c4a8544305df1b60f007
密码加密其实也是面试中经常被问到的题目,之前一位球友在面试拼多多服务端暑期实习的时候就被问到了。
我们来模拟一下。
面试官老王问:“用户名密码登录时是如何加密的,和验证的?”
你(小二):
在技术派中,我使用了 Spring Security 的 BCrypt 算法对密码进行了加盐哈希,盐值是随机的,因此即使两个用户使用了相同的密码,生成的哈希结果也是不同的。
然后将加密后的哈希值(包含盐值)存储到 MySQL 数据库中。
当登录的时候,直接调用 BCryptPasswordEncoder 的 matches 方法进行比对,该方法会自动从存储的加密密码中提取盐值,再对用户输入的密码进行相同的哈希计算。源码主要在 BCrypt 类中。
代码示例如下所示:
// 创建一个 BCryptPasswordEncoder 实例
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
// 注册时加密密码
String rawPassword = "沉默王二是条狗";
String encodedPassword = passwordEncoder.encode(rawPassword);
// 打印加密后的密码(每次加密结果都不同)
System.out.println("加密后的密码: " + encodedPassword);
// 登录时验证密码
boolean isPasswordMatch = passwordEncoder.matches(rawPassword, encodedPassword);
System.out.println("密码验证结果: " + isPasswordMatch); // 输出为 true
面试官老王问:“加盐有什么好处?哈希有什么好处?”
你(小二):如果密码未加盐,常用的密码(如 “123456”)的哈希值是已知的,攻击者可以直接匹配彩虹表中的哈希值来获取密码。而通过加盐,即使是相同的密码,生成的哈希值也不同,无法通过彩虹表轻松破解。
哈希函数是单向的,意味着从哈希值无法轻易推导出原始密码。密码哈希后,即使数据库泄露,攻击者也不能直接从哈希值中恢复原始密码。
好,今天是国庆节第一天,希望大家都能玩的愉快,不过应该还有一部分小伙伴在学习,加密哈希算法就给大家讲这么多,应付节后的面试是绰绰有余了(😄)。
ending
一个人可以走得很快,但一群人才能走得更远。二哥的编程星球已经有 6300 多名球友加入了,如果你也需要一个良好的学习环境,戳链接 🔗 加入我们吧。这是一个 编程学习指南 + Java 项目实战 + LeetCode 刷题 +【简历精修】的私密圈子,你可以阅读星球专栏、向二哥提问、帮你制定学习计划、和球友一起打卡成长。
欢迎点击左下角【阅读原文】了解二哥的编程星球,这可能是你学习求职路上最有含金量的一次点击。
最后,把二哥的座右铭送给大家:没有什么使我停留——除了目的,纵然岸旁有玫瑰、有绿荫、有宁静的港湾,我是不系之舟。共勉 💪。