|
此版本仍在开发中,尚未被视为稳定版本。如需最新稳定版本,请使用 Spring Security 7.0.4! |
密码存储
Spring Security 的 PasswordEncoder 接口用于对密码执行单向转换,以便安全地存储密码。
由于 PasswordEncoder 是单向转换,因此在需要双向转换密码的场景下(例如存储用于向数据库进行身份验证的凭据)并不适用。
通常,PasswordEncoder 用于存储密码,该密码在身份验证时需与用户提供的密码进行比对。
密码存储历史
多年来,存储密码的标准机制不断演进。 最初,密码以明文形式存储。 当时人们认为密码是安全的,因为存储密码的数据存储库需要凭据才能访问。 然而,恶意用户通过 SQL 注入等攻击手段,找到了获取大量用户名和密码“数据转储”的方法。 随着越来越多的用户凭证被公开,安全专家意识到我们需要采取更多措施来保护用户的密码。
随后,开发人员被鼓励在对密码进行单向哈希(例如 SHA-256)处理后再存储。 当用户尝试进行身份验证时,系统会将存储的哈希密码与用户输入密码的哈希值进行比对。 这意味着系统只需存储密码的单向哈希值。 如果发生数据泄露,暴露的也只是密码的单向哈希值。 由于这些哈希是单向的,并且根据哈希值推测出原始密码在计算上非常困难,因此攻击者通常认为逐个破解系统中的每个密码并不值得投入精力。 为了攻破这种新机制,恶意用户创建了被称为彩虹表(Rainbow Tables)的查找表。 他们不再每次重复猜测每个密码,而是预先计算一次密码哈希值并将其存储在查找表中。
为了降低彩虹表(Rainbow Tables)的有效性,开发人员被鼓励使用加盐密码(salted passwords)。 具体做法是:不再仅将密码作为哈希函数的输入,而是为每个用户的密码生成随机字节(称为“盐值”或 salt)。 将盐值与用户密码一起输入哈希函数,生成唯一的哈希值。 该盐值会以明文形式与用户的密码哈希值一同存储。 当用户尝试进行身份验证时,系统会将用户输入的密码与存储的盐值组合后进行哈希,并将结果与存储的哈希值进行比对。 由于每个盐值都是唯一的,因此对于不同的盐值和密码组合,所产生的哈希值也各不相同,从而使彩虹表攻击不再有效。
在现代,我们意识到加密哈希算法(如 SHA-256)已不再安全。 原因是借助现代硬件,我们每秒可以执行数十亿次哈希计算。 这意味着我们可以轻松地逐个破解每个密码。
现在建议开发人员使用自适应单向函数来存储密码。使用自适应单向函数对密码进行验证是有意设计为资源密集型的(它们有意大量占用 CPU、内存或其他资源)。一种自适应的单向函数允许配置一个“工作因子”,该因子可随着硬件性能的提升而增大。我们建议将“工作因子”调整为在您的系统上验证一个密码大约需要一秒钟。这种权衡是为了增加攻击者破解密码的难度,但又不至于对您自己的系统造成过重负担或引起用户不满。Spring Security 已尝试为“工作因子”提供一个良好的起点,但我们鼓励用户根据自己的系统自定义“工作因子”,因为不同系统的性能差异极大。应使用的自适应单向函数示例包括bcrypt、PBKDF2、scrypt和argon2。
由于自适应单向函数有意设计为资源密集型,因此在每个请求中都验证用户名和密码会显著降低应用程序的性能。 Spring Security(或其他任何库)都无法加快密码验证的速度,因为正是通过使验证过程消耗大量资源才获得了安全性。 建议用户将长期凭证(即用户名和密码)交换为短期凭证(例如会话、OAuth Tokens等)。 短期凭证可以快速验证,且不会造成任何安全性的损失。
委托密码编码器
在 Spring Security 5.0 之前,默认的 PasswordEncoder 是 NoOpPasswordEncoder,它要求使用明文密码。
根据密码历史一节的内容,你可能会认为现在的默认 PasswordEncoder 应该类似于 BCryptPasswordEncoder。
然而,这种想法忽略了三个现实世界中的问题:
-
许多应用程序使用无法轻易迁移的旧密码编码。
-
密码存储的最佳实践将再次发生变化。
-
作为一个框架,Spring Security 无法频繁地进行破坏性变更。
相反,Spring Security 引入了 DelegatingPasswordEncoder,它通过以下方式解决了所有这些问题:
-
确保使用当前推荐的密码存储方式对密码进行编码
-
支持以现代和传统格式验证密码
-
允许在未来升级编码
你可以通过使用 DelegatingPasswordEncoder 轻松构造一个 PasswordEncoderFactories 的实例:
-
Java
-
Kotlin
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
或者,您可以创建自己的自定义实例:
-
Java
-
Kotlin
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()
encoders["pbkdf2@SpringSecurity_v5_8"] = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["scrypt"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()
encoders["scrypt@SpringSecurity_v5_8"] = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["argon2"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()
encoders["argon2@SpringSecurity_v5_8"] = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
encoders["sha256"] = StandardPasswordEncoder()
val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
密码存储格式
密码的一般格式为:
{id}encodedPassword
id 是一个标识符,用于查找应使用哪个 PasswordEncoder,而 encodedPassword 是为所选 PasswordEncoder 编码的原始密码。
id 必须位于密码开头,以 { 开始,并以 } 结束。
如果找不到 id,则将 id 设置为 null。
例如,以下可能是使用不同 id 值编码的密码列表。
所有原始密码均为 password。
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG (1)
{noop}password (2)
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc (3)
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= (4)
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0 (5)
| 1 | 第一个密码的 PasswordEncoder ID 为 bcrypt,其 encodedPassword 值为 $2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG。
在进行匹配时,它将委托给 BCryptPasswordEncoder。 |
| 2 | 第二个密码的 PasswordEncoder ID 为 noop,encodedPassword 值为 password。
在进行匹配时,它将委托给 NoOpPasswordEncoder。 |
| 3 | 第三个密码的 PasswordEncoder ID 为 pbkdf2,其 encodedPassword 值为 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc。
在进行匹配时,它将委托给 Pbkdf2PasswordEncoder。 |
| 4 | 第四个密码的 PasswordEncoder ID 为 scrypt,其 encodedPassword 值为 $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=。
在进行匹配时,它将委托给 SCryptPasswordEncoder。 |
| 5 | 最终密码的 PasswordEncoder ID 为 sha256,encodedPassword 值为 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0。
在进行匹配时,它将委托给 StandardPasswordEncoder。 |
|
一些用户可能会担心,提供存储格式会为潜在的黑客带来便利。
这其实无需担忧,因为密码的存储并不依赖于算法本身保密。
此外,即使没有前缀,攻击者通常也很容易推断出大多数格式。
例如,BCrypt 密码通常以 |
密码加密
传入构造函数的 idForEncode 决定了使用哪个 PasswordEncoder 来对密码进行编码。
在我们之前构造的 DelegatingPasswordEncoder 中,这意味着对 password 进行编码的结果将委托给 BCryptPasswordEncoder,并在前面加上前缀 {bcrypt}。
最终结果如下例所示:
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
密码匹配
匹配基于构造函数中提供的 {id} 以及该 id 到 PasswordEncoder 的映射。
密码存储格式 中的示例展示了如何实现这一机制。
默认情况下,当调用 matches(CharSequence, String) 方法传入一个未映射的 id(包括 null 的 id)和密码时,会抛出 IllegalArgumentException。
通过使用 DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder) 可以自定义此行为。
通过使用 id,我们可以匹配任意密码编码方式,但会使用最新的密码编码方式对密码进行编码。
这一点非常重要,因为与加密不同,密码哈希的设计初衷就是无法简单地还原出明文。
由于无法还原明文,因此迁移密码就变得十分困难。
虽然用户可以轻松地迁移到 NoOpPasswordEncoder,但我们默认将其包含在内,以简化入门体验。
入门体验
如果你正在搭建一个演示或示例项目,花时间对用户密码进行哈希处理会有些麻烦。 虽然有一些便捷机制可以让这个过程更简单,但这些方法仍然不适用于生产环境。
-
Java
-
Kotlin
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
如果你要创建多个用户,也可以重用该构建器:
-
Java
-
Kotlin
UserBuilder users = User.withDefaultPasswordEncoder();
UserDetails user = users
.username("user")
.password("password")
.roles("USER")
.build();
UserDetails admin = users
.username("admin")
.password("password")
.roles("USER","ADMIN")
.build();
val users = User.withDefaultPasswordEncoder()
val user = users
.username("user")
.password("password")
.roles("USER")
.build()
val admin = users
.username("admin")
.password("password")
.roles("USER", "ADMIN")
.build()
这确实会对存储的密码进行哈希处理,但密码仍然会以明文形式暴露在内存和编译后的源代码中。 因此,在生产环境中仍被认为不够安全。 对于生产环境,您应在外部对密码进行哈希处理。
使用 Spring Boot CLI 进行编码
正确编码密码最简单的方法是使用Spring Boot CLI。
例如,以下示例对密码 password 进行编码,以便与 DelegatingPasswordEncoder 一起使用:
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
故障排除
当存储的密码之一没有 id 时,就会发生以下错误,如密码存储格式中所述。
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233) at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)
最简单的解决方法是弄清楚当前密码是如何存储的,并显式提供正确的PasswordEncoder。
如果您正在从 Spring Security 4.2.x 迁移,可以通过暴露一个 NoOpPasswordEncoder bean来恢复到之前的行为。
或者,你可以为所有密码加上正确的 id 前缀,并继续使用 DelegatingPasswordEncoder。
例如,如果你正在使用 BCrypt,则可以将你的密码从类似以下内容进行迁移:
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
to
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
有关映射的完整列表,请参阅 PasswordEncoderFactories 的 Javadoc。
BCryptPasswordEncoder
BCryptPasswordEncoder 的实现使用广泛支持的 bcrypt 算法对密码进行哈希。
为了提高抗密码破解能力,bcrypt 被设计得较慢。
与其他自适应单向函数一样,应将其调整至在您的系统上验证一个密码大约需要 1 秒。
BCryptPasswordEncoder 的默认实现使用了强度 10,如 BCryptPasswordEncoder 的 Javadoc 中所述。建议您
在自己的系统上调整和测试强度参数,以确保验证一个密码大约需要 1 秒。
-
Java
-
Kotlin
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with strength 16
val encoder = BCryptPasswordEncoder(16)
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Argon2PasswordEncoder
Argon2PasswordEncoder 实现使用 Argon2 算法对密码进行哈希。
Argon2 是 密码哈希竞赛 的获胜者。
为了抵御在定制硬件上进行的密码破解,Argon2 是一种刻意设计得较慢且需要大量内存的算法。
与其他自适应单向函数一样,应对其进行调优,使其在您的系统上验证密码大约耗时 1 秒。
当前 Argon2PasswordEncoder 的实现需要 BouncyCastle。
-
Java
-
Kotlin
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
Pbkdf2PasswordEncoder
Pbkdf2PasswordEncoder 实现使用 PBKDF2 算法对密码进行哈希。
为了抵御密码破解,PBKDF2 是一种刻意设计得较慢的算法。
与其他自适应单向函数一样,应对其进行调整,使其在您的系统上验证一个密码大约需要 1 秒钟。
当需要 FIPS 认证时,该算法是一个很好的选择。
-
Java
-
Kotlin
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
SCryptPasswordEncoder
SCryptPasswordEncoder 实现使用 scrypt 算法对密码进行哈希。
为了抵御在定制硬件上进行的密码破解,scrypt 是一种刻意设计得较慢的算法,并且需要大量内存。
与其他自适应单向函数一样,应对其进行调优,使其在您的系统上验证一个密码大约耗时 1 秒。
-
Java
-
Kotlin
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()
val result: String? = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
其他PasswordEncoders
还有大量其他的 PasswordEncoder 实现,它们的存在完全是为了向后兼容。
这些实现都已被弃用,以表明它们不再被认为是安全的。
然而,目前没有计划将它们移除,因为迁移现有的遗留系统十分困难。
基于 Password4j 的密码编码器
Spring Security 7.0 引入了基于 Password4j 库的替代密码编码器实现。 这些编码器为流行的哈希算法提供了额外的选项,可作为现有 Spring Security 实现的替代方案。
Password4j 库是一个 Java 加密库,专注于密码哈希,并支持多种算法。 当你需要特定的算法配置或希望利用 Password4j 的优化功能时,这些编码器尤其有用。
所有基于 Password4j 的编码器都是线程安全的,可以在多个线程之间共享。
Argon2Password4jPasswordEncoder
Argon2Password4jPasswordEncoder 实现通过 Password4j 库使用 Argon2 算法对密码进行哈希。
这为 Spring Security 内置的 Argon2PasswordEncoder 提供了一种替代方案,具有不同的配置选项和潜在的性能特性。
Argon2 是密码哈希竞赛的获胜者,推荐用于新应用程序。 此实现利用了 Password4j 的 Argon2 支持,该支持会将盐值正确地包含在输出的哈希值中。
使用默认设置创建一个编码器:
-
Java
-
Kotlin
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定义的 Argon2 参数创建一个编码器:
-
Java
-
Kotlin
Argon2Function argon2Fn = Argon2Function.getInstance(65536, 3, 4, 32,
Argon2.ID);
PasswordEncoder encoder = new Argon2Password4jPasswordEncoder(argon2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val argon2Fn = Argon2Function.getInstance(
65536, 3, 4, 32,
Argon2.ID
)
val encoder: PasswordEncoder = Argon2Password4jPasswordEncoder(argon2Fn)
val result = encoder.encode("myPassword")
assertThat(encoder.matches("myPassword", result)).isTrue()
BcryptPassword4jPasswordEncoder
BcryptPassword4jPasswordEncoder 实现通过 Password4j 库使用 BCrypt 算法对密码进行哈希。
这为 Spring Security 内置的 BCryptPasswordEncoder 提供了一种替代方案,并具有 Password4j 实现的特性。
BCrypt 是一种成熟可靠的密码哈希算法,内置盐值生成功能,并能有效抵御彩虹表攻击。 此实现利用了 Password4j 提供的 BCrypt 支持,其生成的哈希输出中已正确包含了盐值。
使用默认设置创建一个编码器:
-
Java
-
Kotlin
PasswordEncoder encoder = new BCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = BCryptPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定义的 bcrypt 参数创建一个编码器:
-
Java
-
Kotlin
BcryptFunction bcryptFn = BcryptFunction.getInstance(12);
PasswordEncoder encoder = new BcryptPassword4jPasswordEncoder(bcryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val bcryptFunction = BcryptFunction.getInstance(12)
val encoder: PasswordEncoder = BcryptPassword4jPasswordEncoder(bcryptFunction)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
ScryptPassword4jPasswordEncoder
ScryptPassword4jPasswordEncoder 实现通过 Password4j 库使用 SCrypt 算法对密码进行哈希。
这为 Spring Security 内置的 SCryptPasswordEncoder 提供了一种替代方案,并具有 Password4j 实现的特性。
SCrypt 是一种内存密集型密码哈希算法,旨在抵御硬件暴力破解攻击。 此实现利用了 Password4j 的 SCrypt 支持,该支持会将盐值(salt)正确地包含在输出的哈希值中。
使用默认设置创建一个编码器:
-
Java
-
Kotlin
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定义的 scrypt 参数创建一个编码器:
-
Java
-
Kotlin
ScryptFunction scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32);
PasswordEncoder encoder = new ScryptPassword4jPasswordEncoder(scryptFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val scryptFn = ScryptFunction.getInstance(32768, 8, 1, 32)
val encoder: PasswordEncoder = ScryptPassword4jPasswordEncoder(scryptFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
Pbkdf2Password4jPasswordEncoder
Pbkdf2Password4jPasswordEncoder 实现通过 Password4j 库使用 PBKDF2 算法对密码进行哈希。
这为 Spring Security 内置的 Pbkdf2PasswordEncoder 提供了一种替代方案,并支持显式的盐值管理。
PBKDF2 是一种密钥派生函数,其设计在计算上代价高昂,以抵御字典攻击和暴力破解攻击。
此实现显式处理盐值(salt)管理,因为 Password4j 的 PBKDF2 实现不会将盐值包含在输出的哈希值中。
编码后的密码格式为:{salt}:{hash},其中 salt 和 hash 均采用 Base64 编码。
使用默认设置创建一个编码器:
-
Java
-
Kotlin
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定义 PBKDF2 参数创建一个编码器:
-
Java
-
Kotlin
PBKDF2Function pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256);
PasswordEncoder encoder = new Pbkdf2Password4jPasswordEncoder(pbkdf2Fn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val pbkdf2Fn = PBKDF2Function.getInstance(Hmac.SHA256, 100000, 256)
val encoder: PasswordEncoder = Pbkdf2Password4jPasswordEncoder(pbkdf2Fn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
BalloonHashingPassword4jPasswordEncoder
BalloonHashingPassword4jPasswordEncoder 实现通过 Password4j 库使用 Balloon 哈希算法对密码进行哈希。
Balloon 哈希是一种内存密集型密码哈希算法,旨在抵御时间和内存权衡攻击(time-memory trade-off attacks)以及旁路攻击(side-channel attacks)。
此实现显式处理盐值(salt)管理,因为 Password4j 的 Balloon 哈希实现不会将盐值包含在输出的哈希值中。
编码后的密码格式为:{salt}:{hash},其中盐值和哈希值均采用 Base64 编码。
使用默认设置创建一个编码器:
-
Java
-
Kotlin
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder();
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder()
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
使用自定义参数创建一个编码器:
-
Java
-
Kotlin
BalloonHashingFunction ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3);
PasswordEncoder encoder = new BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn);
String result = encoder.encode("myPassword");
assertThat(encoder.matches("myPassword", result)).isTrue();
val ballooningHashingFn =
BalloonHashingFunction.getInstance("SHA-256", 1024, 3, 4, 3)
val encoder: PasswordEncoder = BalloonHashingPassword4jPasswordEncoder(ballooningHashingFn)
val result = encoder.encode("myPassword")
Assertions.assertThat(encoder.matches("myPassword", result)).isTrue()
密码存储配置
Spring Security 默认使用 DelegatingPasswordEncoder。
不过,你可以通过将 PasswordEncoder 暴露为 Spring Bean 来自定义此行为。
如果您正在从 Spring Security 4.2.x 迁移,可以通过暴露一个 NoOpPasswordEncoder Bean 来恢复之前的行为。
|
回退到 |
-
Java
-
XML
-
Kotlin
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
<b:bean id="passwordEncoder"
class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
@Bean
fun passwordEncoder(): PasswordEncoder {
return NoOpPasswordEncoder.getInstance();
}
|
XML 配置要求 |
更改密码配置
大多数允许用户指定密码的应用程序还需要提供更新该密码的功能。
用于更改密码的已知 URL 指明了一种机制,通过该机制,密码管理器可以发现给定应用程序的密码更新端点。
你可以配置 Spring Security 来提供此发现端点。
例如,如果你应用程序中的更改密码端点是 /change-password,那么你可以按如下方式配置 Spring Security:
-
Java
-
XML
-
Kotlin
http
.passwordManagement(Customizer.withDefaults())
<sec:password-management/>
http {
passwordManagement { }
}
然后,当密码管理器导航到 /.well-known/change-password 时,Spring Security 会将请求重定向到您的端点 /change-password。
或者,如果你的端点不是 /change-password,你也可以像这样指定它:
-
Java
-
XML
-
Kotlin
http
.passwordManagement((management) -> management
.changePasswordPage("/update-password")
)
<sec:password-management change-password-page="/update-password"/>
http {
passwordManagement {
changePasswordPage = "/update-password"
}
}
通过上述配置,当密码管理器导航到 /.well-known/change-password 时,Spring Security 将重定向到 /update-password。
检查已泄露密码
在某些场景下,你需要检查密码是否已被泄露。例如,如果你正在开发一个处理敏感数据的应用程序,通常需要对用户的密码进行某种检查,以确保其可靠性。 其中一种检查方式就是确认该密码是否已被泄露,通常是因为它出现在数据泄露事件中。
为此,Spring Security 通过 Have I Been Pwned API 的 HaveIBeenPwnedRestApiPasswordChecker 实现与 CompromisedPasswordChecker 接口进行了集成。
您可以自行使用 CompromisedPasswordChecker API,或者如果您通过 Spring Security 认证机制 使用 DaoAuthenticationProvider,则可以提供一个 CompromisedPasswordChecker Bean,它将被 Spring Security 配置自动识别。
通过这种方式,当你尝试使用弱密码(例如 123456)通过表单登录进行身份验证时,你会收到一个 401 状态码,或者被重定向到 /login?error 页面(具体取决于你的用户代理)。
然而,在这种情况下,仅返回 401 或执行重定向并不太有用,因为这会造成一些困惑:用户输入了正确的密码,却仍然无法登录。
在这种情况下,你可以通过 CompromisedPasswordException 来处理 AuthenticationFailureHandler 异常,以执行你期望的逻辑,例如将用户代理重定向到 /reset-password 页面:
-
Java
-
Kotlin
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.anyRequest().authenticated()
)
.formLogin((login) -> login
.failureHandler(new CompromisedPasswordAuthenticationFailureHandler())
);
return http.build();
}
@Bean
public CompromisedPasswordChecker compromisedPasswordChecker() {
return new HaveIBeenPwnedRestApiPasswordChecker();
}
static class CompromisedPasswordAuthenticationFailureHandler implements AuthenticationFailureHandler {
private final SimpleUrlAuthenticationFailureHandler defaultFailureHandler = new SimpleUrlAuthenticationFailureHandler(
"/login?error");
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
if (exception instanceof CompromisedPasswordException) {
this.redirectStrategy.sendRedirect(request, response, "/reset-password");
return;
}
this.defaultFailureHandler.onAuthenticationFailure(request, response, exception);
}
}
@Bean
open fun filterChain(http: HttpSecurity): SecurityFilterChain {
http {
authorizeHttpRequests {
authorize(anyRequest, authenticated)
}
formLogin {
authenticationFailureHandler = CompromisedPasswordAuthenticationFailureHandler()
}
}
return http.build()
}
@Bean
open fun compromisedPasswordChecker(): CompromisedPasswordChecker {
return HaveIBeenPwnedRestApiPasswordChecker()
}
class CompromisedPasswordAuthenticationFailureHandler : AuthenticationFailureHandler {
private val defaultFailureHandler = SimpleUrlAuthenticationFailureHandler("/login?error")
private val redirectStrategy = DefaultRedirectStrategy()
override fun onAuthenticationFailure(
request: HttpServletRequest,
response: HttpServletResponse,
exception: AuthenticationException
) {
if (exception is CompromisedPasswordException) {
redirectStrategy.sendRedirect(request, response, "/reset-password")
return
}
defaultFailureHandler.onAuthenticationFailure(request, response, exception)
}
}