# 密码存储

Spring Security 的`PasswordEncoder`接口用于执行密码的单向转换，以允许安全地存储密码。给定`PasswordEncoder`是单向转换，当密码转换需要双向（即存储用于对数据库进行身份验证的凭据）时，并不打算这样做。通常`PasswordEncoder`用于存储需要与用户在身份验证时提供的密码进行比较的密码。

## 密码存储历史

多年来，存储密码的标准机制一直在发展。在开始的时候，密码是用纯文本格式存储的。这些密码被认为是安全的，因为数据存储中的密码被保存在访问它所需的凭据中。然而，恶意用户能够通过 SQL 注入等攻击，找到获取大量用户名和密码的“数据转储”的方法。随着越来越多的用户证书成为公共安全专家意识到，我们需要做更多的工作来保护用户的密码。

然后鼓励开发人员在通过 SHA-256 等单向散列运行密码后存储密码。当用户试图进行身份验证时，会将散列密码与他们键入的密码的散列进行比较。这意味着系统只需要存储密码的单向散列。如果发生了漏洞，那么只会公开密码的单向散列。由于散列是一种方式，并且在计算上很难猜测给定散列的密码，因此不值得花费精力来计算系统中的每个密码。为了击败这个新系统，恶意用户决定创建名为[彩虹桌](https://en.wikipedia.org/wiki/Rainbow_table)的查找表。他们不是每次都猜测每个密码，而是计算一次密码并将其存储在查找表中。

为了降低 Rainbow 表的有效性，鼓励开发人员使用盐渍密码。不再只使用密码作为散列函数的输入，而是为每个用户的密码生成随机字节（称为 salt）。SALT 和用户的密码将通过散列函数运行，该函数将生成一个唯一的散列。这种盐将与用户的密码一起以明文形式存储。然后，当用户试图进行身份验证时，散列密码将与存储的盐的散列和他们输入的密码进行比较。独特的盐意味着彩虹表格不再有效，因为每种盐和密码组合的散列值都不同。

在现代，我们意识到加密散列（如 SHA-256）不再安全。原因在于，有了现代硬件，我们每秒钟可以进行数十亿次散列计算。这意味着我们可以轻松地单独破解每个密码。

现在鼓励开发人员利用自适应单向功能来存储密码。具有自适应单向功能的密码的验证是有意的资源密集型的（如 CPU、内存等）。一个自适应的单向功能允许配置一个“工作因素”，可以随着硬件变得更好而增长。建议将“工作因子”调整为在系统上验证密码所需的时间约为 1 秒。这样做的代价是让攻击者很难破解密码，但也不会因为代价太高而给自己的系统带来过大的负担。 Spring 安全性已经尝试为“工作因素”提供了一个很好的起点，但是鼓励用户为自己的系统定制“工作因素”，因为系统之间的性能会有很大的差异。应该使用的自适应单向函数的示例包括[bcrypt](#authentication-password-storage-bcrypt)、[PBKDF2](#authentication-password-storage-pbkdf2)、[scrypt](#authentication-password-storage-scrypt)和[argon2](#authentication-password-storage-argon2)。

由于自适应单向函数是故意的资源密集型的，因此为每个请求验证用户名和密码将大大降低应用程序的性能。 Spring 安全性（或任何其他库）不能做任何事情来加速密码的验证，因为安全性是通过使验证资源密集型来获得的。鼓励用户将长期凭据（即用户名和密码）交换为短期凭据（即会话、OAuth 令牌等）。短期凭据可以被快速验证，而不会损失任何安全性。

## PasswordEncoder

在 Spring Security5.0 之前，默认的`PasswordEncoder`是`NoOpPasswordEncoder`，它需要纯文本密码。基于[密码历史记录](#authentication-password-storage-history)部分，你可能认为默认的`PasswordEncoder`现在类似于`bcryptpasswordencoder`。然而，这忽略了三个现实世界中的问题：

* 有许多应用程序使用旧的密码编码，无法轻松地进行迁移。

* 密码存储的最佳实践将再次改变。

* 作为一种框架 Spring，安全不能频繁地进行破坏更改

Spring 安全性引入了`DelegatingPasswordEncoder`，它通过以下方式解决了所有问题：

* 确保使用当前的密码存储建议对密码进行编码

* 允许验证现代和遗留格式的密码。

* 允许在将来升级编码

你可以使用`PasswordEncoderFactories`轻松地构造`DelegatingPasswordEncoder`的实例。

例 1。创建默认的代理 PasswordEncoder

爪哇

```
PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();
```

Kotlin

```
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
```

或者，你可以创建自己的自定义实例。例如：

例 2。创建自定义代理 PasswordEncoder

爪哇

```
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
```

Kotlin

```
val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder()
encoders["scrypt"] = SCryptPasswordEncoder()
encoders["sha256"] = StandardPasswordEncoder()

val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)
```

### 密码存储格式

密码的一般格式是：

例 3。passwordencoder 存储格式

```
{id}encodedPassword
```

使得`id`是用于查找应该使用`PasswordEncoder`的标识符，而`encodedPassword`是所选`PasswordEncoder`的原始编码密码。`id`必须位于密码的开头，以`{`开头，以`}`结尾。如果不能找到`id`，则`id`将为空。例如，以下可能是使用不同的`id`编码的密码列表。所有的原始密码都是“密码”。

例 4。代理 PasswordEncoder 编码的密码示例

```
{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`，编码密码为`$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG`。<br/>匹配时，将委托给`BCryptPasswordEncoder`|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|第二个密码的`PasswordEncoder`ID 为`noop`，编码密码为`password`。|
|**3**|第三个密码的`PasswordEncoder`ID 为`pbkdf2`，编码密码为`5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc`。|
|**4**|第四个密码的`PasswordEncoder`ID 为`scrypt`，编码密码为`$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=`，当与之匹配时，它将委托给`ScryptPassWordEncoder`。|
|**5**|最终的密码将具有`PasswordEncoder`的 ID`sha256`和`97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0`的编码密码。<br/>匹配时，它将委托给`StandardPasswordEncoder`。|

|   |一些用户可能担心存储格式是为潜在的黑客提供的。<br/>这不是一个问题，因为密码的存储不依赖于算法是一个秘密。<br/>此外，大多数格式都很容易被攻击者在没有前缀的情况下发现。<br/>例如，bcrypt 密码通常以`$2a$`开头。|
|---|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 密码编码

传递到构造函数的`idForEncode`确定将使用哪个`PasswordEncoder`来编码密码。在我们上面构造的`DelegatingPasswordEncoder`中，这意味着编码的结果`password`将被委托给`BCryptPasswordEncoder`，并以`{bcrypt}`作为前缀。最终的结果会是：

例 5。delegatingPassWordEncoder 编码示例

```
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
```

### 密码匹配

匹配是基于`{id}`和`id`到构造函数中提供的`PasswordEncoder`的映射完成的。我们在[密码存储格式](#authentication-password-storage-dpe-format)中的示例提供了如何实现这一点的工作示例。默认情况下，使用密码调用`matches(CharSequence, String)`并调用未映射（包括空 ID）的`id`的结果将导致`IllegalArgumentException`。可以使用`DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)`定制此行为。

通过使用`id`，我们可以在任何密码编码上进行匹配，但是可以使用最现代的密码编码来编码密码。这一点很重要，因为与加密不同，密码散列的设计使得没有简单的方法来恢复明文。由于无法恢复明文，因此很难迁移密码。虽然迁移`NoOpPasswordEncoder`对用户来说很简单，但我们选择在默认情况下包含它，以使入门体验更简单。

### 入门经验

如果你正在组装一个演示或示例，那么花时间对用户的密码进行散列会有点麻烦。有方便的机制使这一点更容易，但这仍然不打算用于生产。

示例 6.与 DefaultPassWordEncoder 示例

爪哇

```
User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
```

Kotlin

```
val user = User.withDefaultPasswordEncoder()
    .username("user")
    .password("password")
    .roles("user")
    .build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
```

如果要创建多个用户，还可以重用构建器。

示例 7.WithDefaultPassWordEncoder 重用构建器

爪哇

```
UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
User admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();
```

Kotlin

```
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()
```

这会对存储的密码进行散列，但这些密码仍会在内存和编译后的源代码中公开。因此，对于生产环境来说，它仍然不被认为是安全的。对于生产，你应该[在外部散列密码](#authentication-password-storage-boot-cli)。

### 用 Spring boot cli 编码

正确编码密码的最简单方法是使用[Spring Boot CLI](https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-cli.html)。

例如，下面将对`password`的密码进行编码，以便与[PasswordEncoder](#authentication-password-storage-dpe)一起使用：

例 8。 Spring 启动 CLI EncodePassword 示例

```
spring encodepassword password
{bcrypt}$2a$10$X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6
```

### 故障排除

当存储的密码之一没有[密码存储格式](#authentication-password-storage-dpe-format)中所述的 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`。解决此问题的最简单方法是弄清楚当前如何存储密码，并显式地提供正确的`PasswordEncoder`。

如果你正在从 Spring Security4.2.x 迁移，则可以通过[公开`NoOpPasswordEncoder` Bean]（# 身份验证-密码-存储-配置）来恢复到以前的行为。

或者，你可以在所有密码前加上正确的 ID，然后继续使用`DelegatingPasswordEncoder`。例如，如果你正在使用 bcrypt，那么你将从以下内容迁移密码：

```
$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
```

to

```
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
```

对于映射的完整列表，请参考[PasswordEncoderFactories](https://docs.spring.io/spring-security/site/docs/5.0.x/api/org/springframework/security/crypto/factory/PasswordEncoderFactories.html)上的 爪哇doc。

## BCryptPasswordEncoder

`BCryptPasswordEncoder`实现使用广泛支持的[bcrypt](https://en.wikipedia.org/wiki/Bcrypt)算法来散列密码。为了使其更好地抵抗密码破解，bcrypt 故意放慢速度。像其他自适应单向功能一样，它应该调整为在系统上验证密码所需的时间约为 1 秒。`BCryptPasswordEncoder`的默认实现使用了在[bcryptpasswordencoder](https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.html)的 爪哇doc 中提到的强度 10。我们鼓励你在自己的系统上调整和测试强度参数，这样验证密码大约需要 1 秒钟。

例 9。bcryptpasswordencoder

爪哇

```
// Create an encoder with strength 16
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
```

Kotlin

```
// Create an encoder with strength 16
val encoder = BCryptPasswordEncoder(16)
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
```

## Argon2Passwordencoder

`Argon2PasswordEncoder`实现使用[Argon2](https://en.wikipedia.org/wiki/Argon2)算法来散列密码。Argon2 是[密码散列竞赛](https://en.wikipedia.org/wiki/Password_Hashing_Competition)的获胜者。为了击败定制硬件上的密码破解，Argon2 是一种故意缓慢的算法，需要大量内存。像其他自适应单向功能一样，它应该调整为在系统上验证密码所需的时间约为 1 秒。`Argon2PasswordEncoder`的当前实现需要 bouncycastle。

例 10。Argon2Passwordencoder

爪哇

```
// Create an encoder with all the defaults
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
```

Kotlin

```
// Create an encoder with all the defaults
val encoder = Argon2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
```

## PBKDF2PASSWORDENCODER

`Pbkdf2PasswordEncoder`实现使用[PBKDF2](https://en.wikipedia.org/wiki/PBKDF2)算法来散列密码。为了击败密码破解，PBKDF2 是一种故意缓慢的算法。像其他自适应单向功能一样，它应该调整为在系统上验证密码所需的时间约为 1 秒。当需要 FIPS 认证时，该算法是一个很好的选择。

例 11。PBKDF2PASSWORDENCODER

爪哇

```
// Create an encoder with all the defaults
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
```

Kotlin

```
// Create an encoder with all the defaults
val encoder = Pbkdf2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
```

## SCryptPasswordEncoder

`SCryptPasswordEncoder`实现使用[scrypt](https://en.wikipedia.org/wiki/Scrypt)算法来散列密码。为了击败定制硬件上的密码破解，Scrypt 是一种故意缓慢的算法，需要大量的内存。像其他自适应单向功能一样，它应该调整为在系统上验证密码所需的时间约为 1 秒。

例 12。ScryptPassWordEncoder

爪哇

```
// Create an encoder with all the defaults
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
```

Kotlin

```
// Create an encoder with all the defaults
val encoder = SCryptPasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))
```

## 其他 PasswordEncoders

还有相当数量的其他`PasswordEncoder`实现完全是为了向后兼容而存在的。它们都被弃用，以表明它们不再被认为是安全的。但是，由于很难迁移现有的遗留系统，因此没有删除它们的计划。

## 密码存储配置

Spring 安全性默认使用[PasswordEncoder](#authentication-password-storage-dpe)。然而，这可以通过将`PasswordEncoder`公开为 Spring  Bean 来定制。

如果从 Spring Security4.2.x 迁移，则可以通过公开`NoOpPasswordEncoder` Bean 来恢复到以前的行为。

|   |恢复到`NoOpPasswordEncoder`不被认为是安全的。<br/>你应该转而使用`DelegatingPasswordEncoder`来支持安全的密码编码。|
|---|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

例 13。NoopPassWordEncoder

爪哇

```
@Bean
public static PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
}
```

XML

```
<b:bean id="passwordEncoder"
        class="org.springframework.security.crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
```

Kotlin

```
@Bean
fun passwordEncoder(): PasswordEncoder {
    return NoOpPasswordEncoder.getInstance();
}
```

|   |XML 配置要求`NoOpPasswordEncoder` Bean 名称为`passwordEncoder`。|
|---|---------------------------------------------------------------------------------------|

## 更改密码配置

大多数允许用户指定密码的应用程序也需要更新该密码的功能。

[一个众所周知的更改密码的 URL](https://w3c.github.io/webappsec-change-password-url/)表示一种机制，通过这种机制，密码管理器可以发现给定应用程序的密码更新端点。

你可以配置 Spring 安全性以提供此发现端点。例如，如果应用程序中的更改密码端点是`/change-password`，那么你可以这样配置 Spring 安全性：

例 14。默认更改密码端点

Java

```
http
    .passwordManagement(Customizer.withDefaults())
```

XML

```
<sec:password-management/>
```

Kotlin

```
http {
    passwordManagement { }
}
```

然后，当密码管理器导航到`/.well-known/change-password`时， Spring 安全性将重定向你的端点，`/change-password`。

或者，如果你的端点不是`/change-password`，也可以这样指定：

例 15。更改密码端点

Java

```
http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    )
```

XML

```
<sec:password-management change-password-page="/update-password"/>
```

Kotlin

```
http {
    passwordManagement {
        changePasswordPage = "/update-password"
    }
}
```

通过上述配置，当密码管理器导航到`/.well-known/change-password`时， Spring Security 将重定向到`/update-password`。