features-authentication-password-storage.md 20.0 KB
Newer Older
dallascao's avatar
dallascao 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
# 密码存储

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`