04.Spring Security in Action 第四章 SpringSecurity处理密码的相关讨论
04.Spring Security in Action 第四章 SpringSecurity处理密码的相关讨论
文章目录
本章包括
- 实现并使用PasswordEncoder
- 使用Spring Security Crypto模块提供的工具
在第三章中,我们讨论了在用Spring Security实现的应用程序中管理用户。但是密码呢?它们当然是认证流程中的一个重要部分。在这一章中,你将学习如何在一个用Spring Security实现的应用程序中管理密码和秘密。我们将讨论PasswordEncoder合约以及Spring Security Crypto模块(SSCM)提供的用于管理密码的工具。
4.1 了解PasswordEncoder合约
从第三章开始,你现在应该对UserDetails的界面有一个清晰的印象,以及使用其实现的多种方式。但正如你在第2章中所学到的,在认证和授权过程中,不同的角色会管理用户代表。你还了解到,其中一些有默认值,比如UserDetailsService和PasswordEncoder。你现在知道了,你可以覆盖
从第三章开始,你现在应该对UserDetails的界面有一个清晰的印象,以及使用其实现的多种方式。但正如你在第2章中所学到的,在认证和授权过程中,不同的角色会管理用户代表。你还了解到,其中一些有默认值,比如UserDetailsService和PasswordEncoder。现在你知道你可以覆盖这些默认值了。我们继续深入了解这些Bean和实现它们的方法,所以在这一节,我们分析PasswordEncoder。图4.1提醒了你PasswordEncoder在认证过程中的位置。
图4.1 Spring Security的认证过程。AuthenticationProvider在认证过程中使用PasswordEncoder来验证用户的密码。
因为一般情况下,系统不会管理纯文本的密码,这些密码通常会经过某种转化,使读取和窃取密码的难度增加。对于这个责任,Spring Security定义了一个单独的契约。为了在本节中轻松解释,我提供了大量与密码编码器实现相关的代码示例。我们将从理解契约开始,然后在一个项目中写出我们的实现。然后在4.1.3节中,我将为你提供一个由Spring Security提供的最著名和最广泛使用的密码编码器的实现列表。
4.1.1 密码编码器合同的定义
在本节中,我们将讨论PasswordEncoder合约的定义。你实现了这个契约,告诉Spring Security如何验证用户的密码。在验证过程中,PasswordEncoder决定一个密码是否有效。每个系统都会以某种方式存储密码编码。你最好是将它们散列存储,这样就不会有机会被人读取密码。密码编码器也可以对密码进行编码。合同中声明的方法encode()和matches(),实际上是对其责任的定义。这两个都是同一个契约的一部分,因为这些都是强关联的,一个接一个。应用程序对密码进行编码的方式与密码的验证方式有关。让我们首先回顾一下PasswordEncoder接口的内容。
该接口定义了两个抽象的方法和一个默认的实现。抽象的encode()和matches()方法也是你在处理PasswordEncoder的实现时最常听到的。
encode(CharSequence rawPassword)方法的目的是返回一个提供的字符串的转换。就Spring Security的功能而言,它被用来为一个给定的密码提供加密或哈希值。之后你可以使用matches(CharSequence rawPassword, String encodedPassword)方法来检查编码后的字符串是否与原始密码匹配。你在认证过程中使用matches()方法来测试所提供的密码与一组已知的凭证。第三个方法叫做upgradeEncoding(CharSequence encodedPassword),在合同中默认为false。如果你覆盖它并返回true,那么编码后的密码将被重新编码以获得更好的安全性。
在某些情况下,对编码后的密码进行编码,可以使从结果中获得明文密码的难度增加。一般来说,这是某种隐蔽性,我个人不喜欢。但是,如果你认为它适用于你的情况,该框架为你提供了这种可能性。
4.1.2 实现PasswordEncoder合约
正如你所观察到的,matches()和encode()这两个方法有很大的关系。如果你覆盖它们,它们在功能上应该总是对应的:由encode()方法返回的字符串应该总是可以用同一个PasswordEncoder的matches()方法来验证。在这一节中,你将实现PasswordEncoder契约,并定义接口所声明的两个抽象方法。知道了如何实现PasswordEncoder,你就可以选择应用程序在认证过程中管理密码的方式了。最直接的实现是一个认为密码是明文的密码编码器:也就是说,它不对密码做任何编码。
管理明文密码正是NoOpPasswordEncoder的实例所做的。我们在第二章的第一个例子中使用了这个类。如果你要写你自己的例子,它看起来会像下面的列表。
清单4.1 密码编码器的最简单实现
编码的结果总是与密码相同。所以要检查这些是否匹配,你只需要用equals()来比较这些字符串。一个使用散列算法SHA-512的PasswordEncoder的简单实现,看起来像下一个列表。
清单4.2 实现一个使用SHA-512的密码编码器
在清单4.2中,我们使用了一个方法,用SHA512对提供的字符串值进行散列。我在清单4.2中省略了这个方法的实现,但你可以在清单4.3中找到它。我们从encode()方法中调用这个方法,现在该方法返回其输入的哈希值。为了对输入的哈希值进行验证,matches()方法对其输入的原始密码进行哈希处理,并将其与进行验证的哈希值进行比较。
清单4.3 用SHA-512对输入进行哈希的方法的实现
你将在下一节中学习到更好的选择,所以现在不要太在意这个代码。
4.1.3 从所提供的密码编码器的实现中进行选择
虽然知道如何实现你的PasswordEncoder是很强大的,但你也要知道Spring Security已经为你提供了一些有利的实现方式。如果其中一个符合你的应用,你就不需要重写它。在本节中,我们将讨论Spring Security提供的PasswordEncoder实现选项。这些选项是:
- NoOpPasswordEncoder-不对密码进行编码,而是将其保持为明文。我们只在例子中使用这个实现。因为它没有对密码进行哈希处理,所以你不应该在真实世界的场景中使用它。
- StandardPasswordEncoder-使用SHA-256对密码进行哈希处理。这个实现现在已经被废弃了,你不应该在你的新实现中使用它。它被弃用的原因是它使用的散列算法我们认为已经不够强大了,但你可能仍然会发现在现有的应用程序中使用这种实现。
- Pbkdf2PasswordEncoder-使用基于密码的密钥衍生函数2(PBKDF2)。
- BCryptPasswordEncoder-使用bcrypt强散列函数对密码进行编码。
- SCryptPasswordEncoder-使用scrypt散列函数对密码进行编码。
关于散列和这些算法的更多信息,你可以在David Wong的《真实世界密码学》(Manning, 2020)第二章中找到很好的讨论。这里有链接。
https://livebook.manning.com/book/real-world-cryptography/chapter-2/
让我们来看看如何创建这些类型的PasswordEncoder实现实例的一些例子。NoOpPasswordEncoder不对密码进行编码。它的实现类似于列表4.1中我们的例子中的PlainTextPasswordEncoder。由于这个原因,我们只在理论上的例子中使用这个密码编码器。另外,NoOpPasswordEncoder类被设计成一个singleton。你不能从类外直接调用它的构造函数,但你可以使用NoOpPasswordEncoder.getInstance()方法来获得该类的实例,像这样。
PasswordEncoder p = NoOpPasswordEncoder.getInstance();
Spring Security提供的StandardPasswordEncoder实现使用SHA-256来哈希密码。对于StandardPasswordEncoder,你可以提供一个用于散列过程的密码。你通过构造函数的参数来设置这个密码的值。如果你选择调用无参数构造器,实现会使用空字符串作为密钥的值。然而,StandardPasswordEncoder现在已经废弃了,我不建议你在新的实现中使用它。你可能会发现旧的应用程序或遗留代码仍在使用它,所以这就是为什么你应该注意到它。下面的代码片段向你展示了如何创建这个密码编码器的实例。
PasswordEncoder p = new StandardPasswordEncoder();
PasswordEncoder p = new StandardPasswordEncoder("secret");
Spring Security提供的另一个选择是Pbkdf2PasswordEncoder实现,它使用PBKDF2进行密码编码。要创建Pbkdf2PasswordEncoder的实例,你有以下选择。
PasswordEncoder p = new Pbkdf2PasswordEncoder();
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret");
PasswordEncoder p = new Pbkdf2PasswordEncoder("secret", 185000, 256);
PBKDF2是一个相当简单、缓慢的哈希函数,它按照迭代参数指定的次数执行HMAC。最后一次调用收到的三个参数分别是用于编码过程的密钥值,用于编码密码的迭代次数,以及哈希值的大小。第二个和第三个参数可以影响结果的强度。你可以选择更多或更少的迭代次数,以及结果的长度。哈希值越长,密码的强度就越大。然而,要注意性能会受到这些值的影响:迭代次数越多,你的应用程序消耗的资源就越多。你应该在生成哈希值所消耗的资源和所需的编码强度之间做出明智的妥协。
注意 在本书中,我提到了几个你可能想了解的密码学概念。关于HMAC和其他密码学细节的相关信息,我推荐David Wong的《真实世界密码学》(Manning, 2020)。该书的第3章提供了关于HMAC的详细信息。你可以在https://livebook.manning.com/ book/real-world-cryptography/chapter-3/找到该书。
如果你没有为Pbkdf2PasswordEncoder的实现指定第二个或第三个值,默认的迭代次数为185000,结果长度为256。你可以通过选择另外两个重载构造函数中的一个来指定迭代次数和结果长度的自定义值:没有参数的构造函数Pbkdf2PasswordEncoder(),或者只接收秘钥值作为参数的构造函数Pbkdf2PasswordEncoder("密钥")。
Spring Security提供的另一个出色的选择是BCryptPasswordEncoder,它使用bcrypt强散列函数对密码进行编码。你可以通过调用无参数构造函数来实例化BCryptPasswordEncoder。但你也可以选择指定一个强度系数,代表编码过程中使用的对数轮(logarithmic rounds)。此外,你也可以改变用于编码的SecureRandom实例。
PasswordEncoder p = new BCryptPasswordEncoder();
PasswordEncoder p = new BCryptPasswordEncoder(4);
SecureRandom s = SecureRandom.getInstanceStrong();
PasswordEncoder p = new BCryptPasswordEncoder(4, s);
你提供的log rounds值会影响散列操作使用的迭代次数。使用的迭代数是2^(log rounds)。对于迭代数的计算,log rounds的值只能在4到31之间。你可以通过调用第二个或第三个重载构造函数之一来指定,如前面的代码片断所示。
我向你介绍的最后一个选项是SCryptPasswordEncoder(图4.2)。这个密码编码器使用一个scrypt散列函数。对于ScryptPasswordEncoder,你有两个选择来创建其实例:
PasswordEncoder p = new SCryptPasswordEncoder();
PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);
前面的例子中的值是你通过调用无参数构造函数创建实例时使用的值。
图4.2 SCryptPasswordEncoder构造器需要五个参数,允许你配置CPU成本、内存成本、密钥长度和salt的长度。
4.1.4 使用DelegatingPasswordEncoder的多种编码策略
在这一节中,我们将讨论在哪些情况下认证流程必须应用各种实现来匹配密码。你还将学习如何应用一个有用的工具,在你的应用程序中充当一个PasswordEncoder。这个工具没有自己的实现,而是委托给实现PasswordEncoder接口的其他对象。
在一些应用程序中,你可能会发现拥有各种密码编码器并根据一些特定的配置从这些编码器中选择是很有用的。我在生产应用中发现DelegatingPasswordEncoder的一个常见情况是,当编码算法被改变时,从应用的一个特定版本开始。想象一下,有人在当前使用的算法中发现了一个漏洞,而你想为新注册的用户改变它,但你不想为现有的证书改变它。所以你最终会有多种哈希值。你如何管理这种情况呢?虽然它不是这种情况的唯一方法,但一个好的选择是使用DelegatingPasswordEncoder对象。
DelegatingPasswordEncoder是PasswordEncoder接口的一个实现,它不是实现它的编码算法,而是委托给同一合约的另一个实现实例。哈希值以命名用于定义该哈希值的算法的前缀开始。DelegatingPasswordEncoder根据密码的前缀委托给PasswordEncoder的正确实现。
这听起来很复杂,但通过一个例子,你可以观察到它是很容易的。图4.3展示了PasswordEncoder实例之间的关系。DelegatingPasswordEncoder有一个PasswordEncoder实现的列表,它委托给这些实现。DelegatingPasswordEncoder将每个实例存储在一个Map中。NoOpPasswordEncoder被分配到密钥noop,而BCryptPasswordEncoder实现被分配到密钥bcrypt。当密码的前缀是{noop}时,DelegatingPasswordEncoder将该操作委托给NoOpPasswordEncoder实现。如果前缀是{bcrypt},那么该操作被委托给BCryptPasswordEncoder实现,如图4.4所示。
图4.3 在这种情况下,DelegatingPasswordEncoder为前缀{noop}注册一个NoOpPasswordEncoder,为前缀{bcrypt}注册一个BCryptPasswordEncoder,为前缀{scrypt}注册一个SCryptPasswordEncoder。如果密码的前缀是{noop},DelegatingPasswordEncoder会将操作转发给NoOpPasswordEncoder实现。
图4.4 在这种情况下,DelegatingPasswordEncoder为前缀{noop}注册一个NoOpPasswordEncoder,为前缀{bcrypt}注册一个BCryptPasswordEncoder,为前缀{scrypt}注册一个SCryptPasswordEncoder。当密码的前缀为{bcrypt}时,DelegatingPasswordEncoder将操作转发给BCryptPasswordEncoder实现。
接下来,让我们看看如何定义一个DelegatingPasswordEncoder。你首先要创建一个你想要的PasswordEncoder实现的实例集合,然后把这些实例放在一个DelegatingPasswordEncoder中,如下表所示。
清单4.4 创建一个DelegatingPasswordEncoder的实例
DelegatingPasswordEncoder只是一个充当PasswordEncoder的工具,所以当你必须从一个实现集合中选择时,你可以使用它。在清单4.4中,DelegatingPasswordEncoder的声明实例包含对NoOpPasswordEncoder、BCryptPasswordEncoder和SCryptPasswordEncoder的引用,并将默认值委托给BCryptPasswordEncoder实现。根据哈希值的前缀,DelegatingPasswordEncoder使用正确的PasswordEncoder实现来匹配密码。这个前缀有一个密钥,可以从编码器的Map中识别要使用的密码编码器。如果没有前缀,DelegatingPasswordEncoder会使用默认的编码器。默认的PasswordEncoder是在构建DelegatingPasswordEncoder实例时作为第一个参数给出的。对于清单4.4中的代码,默认的PasswordEncoder是bcrypt。
注意 大括号是散列前缀的一部分,这些应该围绕着键的名称。例如,如果提供的哈希值是{noop}12345,DelegatingPasswordEncoder会委托给我们为前缀noop注册的NoOpPasswordEncoder。同样,别忘了大括号在前缀中是必须的。
如果哈希值看起来像下一个代码片段,那么密码编码器就是我们分配给前缀{bcrypt}的那个,它是BCryptPasswordEncoder。如果没有前缀,应用程序也会委托给这个编码器,因为我们把它定义为默认实现。
{bcrypt}$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG
为了方便起见,Spring Security提供了一种创建DelegatingPasswordEncoder的方法,它有一个映射到所有实现了PasswordEncoder接口的Map。PasswordEncoderFactories类提供了一个createDelegatingPasswordEncoder()静态方法,返回DelegatingPasswordEncoder的实现,并将bcrypt作为默认编码器。
PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
编码vs.加密vs.散列
在前面的章节中,我经常使用编码、加密和散列这些术语。我想简单澄清一下这些术语以及我们在本书中使用它们的方式。
编码是指对一个给定的输入的任何转换。例如,如果我们有一个函数x可以反转一个字符串,函数x->y应用于ABCD产生DCBA。
加密是一种特殊类型的编码,为了获得输出,你需要提供输入值和一个密钥。密钥使得事后选择谁应该能够逆转函数(从输出中获得输入)成为可能。用函数表示加密的最简单的形式是这样的:
(x, k) -> y
其中x是输入,k是密钥,y是加密的结果。这样一来,知道密钥的人可以用一个已知的函数从输出(y,k)->x中获得输入。如果用于加密的密钥与用于解密的密钥相同,我们通常称其为对称密钥。如果我们有两个不同的密钥用于加密((x,k1)->y)和解密((y,k2)->x),那么我们说,加密是用非对称密钥完成的。那么(k1, k2)就被称为密钥对。用于加密的密钥k1也被称为公钥,而k2则被称为私钥。这样一来,只有优先密钥的拥有者才能解密数据。
散列是一种特殊的编码类型,只是功能只有一个方向。也就是说,从散列函数的输出y中,你不能取回输入x。然而,应该总是有一种方法来检查输出y是否对应于输入x,所以我们可以把散列理解为一对编码和匹配的函数。如果散列是x->y,那么我们也应该有一个匹配函数(x,y)->boolean。
有时,散列函数也可以使用一个随机值添加到输入中:(x, k) -> y,我们把这个值称为salt。salt使函数更强大,增强应用反向函数从结果中获得输入的难度。
为了总结我们在本书中到目前为止所讨论和应用的合同,表4.1简要介绍了每个组成部分。
表4.1 代表Spring Security中认证流程的主要合同的接口
接口 | 描述 |
---|---|
UserDetails | 代表Spring Security所看到的用户。 |
GrantedAuthority | 在应用程序的目的范围内定义了一个用户可以允许的动作(例如,读、写、删除等)。 |
UserDetailsService | 代表用于按用户名检索用户详细信息的对象。 |
UserDetailsManager | UserDetailsService的一个更特殊的契约。除了按用户名检索用户外,它还可以用来改变一个用户集合或一个特定用户。 |
PasswordEncoder | 指定密码如何加密或散列,以及如何检查给定的编码字符串是否与明文密码匹配。 |
4.2 关于Spring Security Crypto模块的更多信息
在本节中,我们将讨论Spring Security Crypto模块(SSCM),它是Spring Security中处理密码学的部分。使用加密和解密函数以及生成密钥并不是Java语言所提供的开箱即用的。而这也制约了开发者在添加依赖关系时,为这些功能提供更方便的方法。
为了让我们的生活更轻松,Spring Security也提供了自己的解决方案,通过消除使用单独库的需要,可以减少项目的依赖性。密码编码器也是SSCM的一部分,即使我们在之前的章节中对它们进行了单独处理。在本节中,我们将讨论SSCM还提供哪些与密码学有关的选项。你会看到如何使用SSCM的两个基本功能的例子。
- 密钥生成器-用于生成散列和加密算法的密钥的对象
- 加密器-用于加密和解密数据的对象
4.2.1 使用密钥生成器
在本节中,我们将讨论密钥生成器。密钥生成器是一个用于生成特定类型的密钥的对象,通常需要用于加密或散列算法。Spring Security提供的密钥生成器的实现是伟大的实用工具。你会更喜欢使用这些实现,而不是为你的应用程序添加另一个依赖项,这就是为什么我建议你了解它们。让我们看看一些关于如何创建和应用密钥生成器的代码例子。
两个接口代表了两种主要的密钥生成器类型。BytesKeyGenerator和StringKeyGenerator。我们可以通过利用工厂类KeyGenerators直接建立它们。你可以使用由StringKeyGenerator契约代表的字符串密钥生成器,以获得一个字符串的密钥。通常情况下,我们把这个密钥作为散列或加密算法的salt。你可以在这个代码片断中找到StringKeyGenerator合约的定义。
public interface StringKeyGenerator {
String generateKey();
}
生成器只有一个 generateKey() 方法,返回一个代表密钥值的字符串。下一个代码片断介绍了如何获得一个StringKeyGenerator实例以及如何使用它来获得一个salt的例子。
StringKeyGenerator keyGenerator = KeyGenerators.string();
String salt = keyGenerator.generateKey();
生成器创建一个8字节的密钥,并将其编码为一个十六进制的字符串。该方法将这些操作的结果作为一个字符串返回。描述密钥生成器的第二个接口是BytesKeyGenerator,它的定义如下。
public interface BytesKeyGenerator {
int getKeyLength(); byte[] generateKey();
}
除了以字节[]形式返回密钥的generateKey()方法外,该接口还定义了另一个方法,以字节数形式返回密钥的长度。默认的ByteKeyGenerator生成的密钥长度为8字节。
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom();
byte [] key = keyGenerator.generateKey();
int keyLength = keyGenerator.getKeyLength();
在前面的代码片断中,密钥生成器生成的密钥长度为8字节。如果你想指定一个不同的密钥长度,你可以在获取密钥生成器实例时,通过向KeyGenerators.secureRandom()方法提供所需的值来实现。
BytesKeyGenerator keyGenerator = KeyGenerators.secureRandom(16);
用KeyGenerators.secureRandom()方法创建的BytesKeyGenerator所生成的密钥对于每次调用generateKey()方法都是唯一的。
在某些情况下,我们更喜欢这样的实现,即每次调用同一个密钥生成器时返回相同的密钥值。在这种情况下,我们可以用KeyGenerators.shared(int length)方法创建一个BytesKeyGenerator。在这个代码片段中,key1和key2的值是一样的:
BytesKeyGenerator keyGenerator = KeyGenerators.shared(16);
byte [] key1 = keyGenerator.generateKey();
byte [] key2 = keyGenerator.generateKey();
4.2.2 使用加密器进行加密和解密操作
在本节中,我们通过代码实例来应用Spring Security提供的加密器的实现。加密器是一个实现加密算法的对象。当谈到安全问题时,加密和解密是常见的操作,所以预计在你的应用程序中需要这些操作。
我们经常需要对数据进行加密,无论是在系统组件之间发送数据还是在持久化数据时。加密器所提供的操作是加密和解密。SSCM定义了两种类型的加密器:BytesEncryptor和TextEncryptor。虽然它们有类似的职责,但它们处理不同的数据类型。TextEncryptor将数据作为字符串来管理。它的方法接收字符串作为输入,并返回字符串作为输出,你可以从其接口的定义中看到。
public interface TextEncryptor {
String encrypt(String text);
String decrypt(String encryptedText);
}
BytesEncryptor更为通用。你把它的输入数据作为一个字节数组来提供。
public interface BytesEncryptor {
byte[] encrypt(byte[] byteArray);
byte[] decrypt(byte[] encryptedByteArray);
}
让我们来看看我们有哪些选择来构建和使用一个加密器。工厂类Encryptors为我们提供了多种可能性。对于BytesEncryptor,我们可以像这样使用Encryptors.standard()或Encryptors.stronger()方法。
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
//创建一个使用盐和密码的TextEncryptor对象。
BytesEncryptor e = Encryptors.standard(password, salt);
byte [] encrypted = e.encrypt(valueToEncrypt.getBytes());
byte [] decrypted = e.decrypt(encrypted);
在幕后,标准的字节加密器使用256字节的AES加密来加密输入。要建立一个更强大的字节加密器的实例,你可以调用Encryptors.stronger()方法。
BytesEncryptor e = Encryptors.stronger(password, salt);
这种差异很小,而且发生在幕后,256位的AES加密使用Galois/Counter模式(GCM)作为操作模式。标准模式使用密码块链(CBC),这被认为是一种较弱的方法。
TextEncryptors有三种主要类型。你通过调用Encryptors.text(), Encryptors.delux(), 或 Encryptors.queryableText()方法来创建这三种类型。除了这些创建加密器的方法,还有一个方法可以返回一个假的TextEncryptor,它不对值进行加密。你可以将假的TextEncryptor用于演示例子或你想测试你的应用程序的性能而不花时间在加密上的情况。返回这个无操作加密器的方法是Encryptors.noOpText()。在下面的代码片断中,你会发现一个使用TextEncryptor的例子。即使是对一个加密器的调用,在这个例子中,encrypted和valueToEncrypt是一样的。
String valueToEncrypt = "HELLO";
TextEncryptor e = Encryptors.noOpText();
String encrypted = e.encrypt(valueToEncrypt);
Encryptors.text()加密器使用Encryptors.standard()方法来管理加密操作,而Encryptors.delux()方法使用一个Encryptors.stronger()实例,像这样。
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
TextEncryptor e = Encryptors.text(password, salt);
String encrypted = e.encrypt(valueToEncrypt);
String decrypted = e.decrypt(encrypted);
对于Encryptors.text()和Encryptors.delux()来说,对同一输入重复调用encrypt()方法会产生不同的输出。这些不同的输出是由于在加密过程中使用了随机生成的初始化向量而发生的。在现实世界中,你会发现你不希望发生这种情况,例如在OAuth API密钥的情况下。我们将在第12章到第15章中更多地讨论OAuth 2。这种输入被称为可查询文本,对于这种情况,你可以使用Encryptors.queryableText()实例。这个加密器保证连续的加密操作会对相同的输入产生相同的输出。在下面的例子中,encrypted1变量的值等于encrypted2变量的值。
String salt = KeyGenerators.string().generateKey();
String password = "secret";
String valueToEncrypt = "HELLO";
//创建一个可查询的文本加密器
TextEncryptor e = Encryptors.queryableText(password, salt);
String encrypted1 = e.encrypt(valueToEncrypt);
String encrypted2 = e.encrypt(valueToEncrypt);
总结
- 在认证逻辑中,PasswordEncoder有一个最关键的职责--处理密码。
- Spring Security在散列算法方面提供了几种选择,这使得实现只是一个选择问题。
- Spring Security Crypto模块(SSCM)为密钥生成器和加密器的实现提供了多种选择。
- 密钥生成器是帮助你生成用于加密图形算法的密钥的实用对象。
- 加密器是帮助你应用数据加密和解密的实用对象。
来源:https://hashnode.blog.csdn.net/article/details/128998549