1. 密码加密方式

1.1 明文密码

最初,密码以明文形式存储在数据库中。但是恶意用户可能会通过SQL注入等手段获取明文密码,同时程序员将数据库数据泄露的情况也可能发生。明文存储密码是很危险的事情。

1.2 Hash算法

Spring Security的 PasswordEncoder 接口对于密码进行 单向转换 ,从而将密码安全的存储,对密码单向转换需要用到 哈希算法 ,例如MD5、SHA-256、SHA-512等,哈希算法是单向的,只能加密,不能解密。 因此,数据库中存储的是单向转换后的密码 ,Spring Security在进行用户身份验证时需要将用户输入的密码进行单向转换,然后与数据库的密码进行比较。如果发生数据泄露,只有密码的单项哈希会被暴露,由于哈希是单向的,并且在给定哈希的情况下只能通过 暴力破解的方式猜测密码

1.3 彩虹表

恶意用户创建称为 彩虹表 的查找表。一个庞大的、针对各种可能的字母组合预先生成的哈希值集合,有了它就可以快速破解各类密码,越是复杂的密码,需要的彩虹表就越庞大。主流的彩虹表都是100G以上,目前主要的算法有LM,NTLM,MD5,SHA1,MYSQLSHA1,HALFLMCHALL,NTLMCHALL,ORACLE-SYSTEM,MD5-HALF

1.4加盐密码

为了防治彩虹表,开发人员开始使用加盐密码。不再只是用密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(盐)。 盐和用户的密码将一起经过哈希函数运算,生成一个唯一的哈希值。盐将以明文形式与用户密码一起储存。然后当用户尝试进行身份验证时,盐和用户输入的密码一起经过哈希函数运输,再与存储的密码进行比较。唯一的盐意味着彩虹表不再有效,因为对于每个盐和密码的组合,哈希值都是不同的。

1.5 自适应单向函数:

随着硬件的不断发展,加盐哈希也不再安全,计算机可以每秒执行数十亿次哈希计算。这意味着可以轻松的使用暴力破解的方式拿到任何密码。
现在开发人员开始使用自适应单向函数来存储密码。使用自适应单向函数验证密码时,故意占用资源 。自适应单向函数允许配置一个 ”工作因子“ ,随着硬件的改进而增加。我们将“工作因子”调整到系统中验证密码需要约一秒钟的时间。这种权衡是为了 使攻击者难以暴力破解密码 。自适应单向函数包括 bcrypt PBKDF2 argon2

2. PasswordEncoder 实现详解

在Spring Security中,PasswordEncoder接口的主要作用是对密码进行编码(即哈希处理)和验证。选择合适的编码器不仅能增强系统的安全性,还能有效抵御密码破解攻击。下面详细介绍几种常用的PasswordEncoder实现。

2.1 BCryptPasswordEncoder

BCryptPasswordEncoder使用广泛支持的bcrypt算法对密码进行哈希处理。bcrypt算法设计为一种自适应单向函数,意味着它能够随着计算能力的提升,通过调整工作因子(cost factor)来增加破解难度。默认情况下,BCryptPasswordEncoder的工作因子为10,可以在大多数系统上提供约1秒的验证时间。这种故意设计得较慢的算法,有效地抵御了暴力破解和彩虹表攻击。
优点:

  • 支持跨平台,广泛兼容。
  • 默认情况下提供良好的安全性。
  • 工作因子可调,灵活性高。

示例代码:

1
2
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10); 
String encodedPassword = passwordEncoder.encode("myPassword");

2.2 Argon2PasswordEncoder

Argon2PasswordEncoder使用的是Argon2算法,这是一种更为现代化的密码哈希算法,并且是2015年密码哈希竞赛的获胜者。Argon2的设计目标是最大限度地抵抗基于自定义硬件(如GPU和ASIC)的攻击,因此它不仅依赖于计算能力,还要求大量内存。为了使用Argon2PasswordEncoder,需要在项目中引入BouncyCastle库。

优点:

  • 算法现代,具备较高的安全性。
  • 设计上能够抵抗基于硬件的破解攻击。

注意事项:

  • 需要额外引入BouncyCastle库。
  • 相较于bcrypt,配置和使用稍微复杂。

示例代码:

1
2
PasswordEncoder passwordEncoder = new Argon2PasswordEncoder(); 
String encodedPassword = passwordEncoder.encode("myPassword");

2.3 Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder使用的是PBKDF2(Password-Based Key Derivation Function 2)算法,它同样是一种故意设计得较慢的哈希算法,旨在抵御基于硬件的破解攻击。PBKDF2通过反复多次哈希处理,提高了密码的破解难度。它在许多安全标准中被推荐使用,如NIST SP 800-132。

优点:

  • 广泛使用,支持度高。
  • 可调整迭代次数以增加破解难度。

示例代码:

1
2
PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder(); 
String encodedPassword = passwordEncoder.encode("myPassword");

3. 密码加密测试

在test中添加testPassword方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@SpringBootTest  
class SecurityDemoApplicationTests {

@Test
void contextLoads() {
}

@Test
void testPassword(){
// 参数为工作因子,默认10,最小4,最大31,越大运算时间越长
PasswordEncoder encoder = new BCryptPasswordEncoder(4);
String result = encoder.encode("123456");
System.out.println(result);

Assert.isTrue(encoder.matches("123456", result), "密码校验失败");
}

}

4. DelegatingPasswordEncoder

DelegatingPasswordEncoder 的设计旨在处理多个密码编码方案并支持向后兼容。它允许你在应用中使用不同的编码器,并能够识别并正确处理存储在数据库中的不同编码格式的密码。

工作原理:

  • 委托机制: DelegatingPasswordEncoder 会根据密码的前缀标识符,将验证和编码操作委托给特定的编码器。例如,一个以{bcrypt}为前缀的密码将使用 BCryptPasswordEncoder 进行处理。
  • 向后兼容: 通过使用 DelegatingPasswordEncoder,可以在更新密码哈希策略时继续支持旧的密码编码格式,而无需强制用户立即更新他们的密码。
  • 默认编码器: DelegatingPasswordEncoder 允许设置一个默认编码器用于新密码的编码。当没有指定前缀时,默认编码器会被使用。

典型用法:

1
2
3
4
5
6
7
8
9
10
11
// 创建编码器的映射关系 
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("argon2", new Argon2PasswordEncoder());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
// 创建 DelegatingPasswordEncoder,并设置默认编码器为 bcrypt
PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);
// 对新密码进行编码
String encodedPassword = passwordEncoder.encode("mySecurePassword");
// 验证密码时,DelegatingPasswordEncoder 会根据前缀选择相应的编码器
boolean matches = passwordEncoder.matches("mySecurePassword", encodedPassword);

优势:

  • 灵活性: 允许在不同的密码编码器之间灵活切换。
  • 向后兼容性: 在不强制要求用户更改密码的情况下,可以逐步迁移到更强的密码编码方案。
  • 扩展性: 可以轻松添加自定义编码器以满足特殊需求。
    DelegatingPasswordEncoder 是大型应用程序中常用的工具,特别是在需要支持多种密码编码方案或逐步过渡到新的哈希算法时。它确保了密码的安全性,同时提供了良好的兼容性和灵活性。

在文章的结尾推荐一些密码安全领域的经典参考资料