龙空技术网

理解 PasswordEncoder 约定

粥屋与包屋 164

前言:

今天朋友们对“私有密码算法 pbkdf2”大体比较讲究,咱们都想要剖析一些“私有密码算法 pbkdf2”的相关资讯。那么小编在网络上汇集了一些对于“私有密码算法 pbkdf2””的相关资讯,希望大家能喜欢,各位老铁们一起来了解一下吧!

在 上一篇 《 管理用户系列》 中,我们讨论了在使用 Spring Security 实现的应用程序中管理用户。 但是密码呢? 它们无疑是身份验证流程中必不可少的部分。 在本章中,您将学习如何在使用 Spring Security 实现的应用程序中管理密码和机密。 我们将讨论 PasswordEncoder 合同以及 Spring Security Crypto 模块(SSCM)提供的用于管理密码的工具。

从《 管理用户系列》 开始,您现在应该对 UserDetails 接口是什么以及使用它实现的多种方式有了一个清晰的概念。但是正如您在《 spring security 哪些是默认配置?》学到的,不同的参与者在身份验证和授权过程中管理用户表示。您还了解了其中一些具有默认值,如 UserDetailsService 和 PasswordEncoder。现在您知道可以覆盖默认值。我们将继续深入理解这些 bean 以及实现它们的方法,因此在本系列中,我们将分析 PasswordEncoder。图 1 提醒您 PasswordEncoder 在身份验证过程的位置。

图 1

Spring Security 认证过程。 AuthenticationProvider 在认证过程中使用 PasswordEncoder 来验证用户的密码。

因为一般来说,系统不会以明文方式管理密码,这些密码通常会经过某种转换,这使得读取和窃取它们变得更加困难。为此,Spring Security定义了一个单独的约定。为了更容易地解释它,我提供了大量与PasswordEncoder实现相关的代码示例。我们将从理解约定开始,然后在项目中编写我们的实现。然后,我将为您提供Spring Security提供的PasswordEncoder最知名和使用最广泛的实现列表。

PasswordEncoder 约定的定义

在本节中,我们将讨论 PasswordEncoder 约定的定义。您可以实现这个约定来告诉 Spring Security 如何验证用户的密码。在身份验证过程中,PasswordEncoder 决定密码是否有效。每个系统都存储以某种方式编码的密码。最好是散列存储,这样别人就不可能读到密码。PasswordEncoder 也可以对密码进行编码。约定声明的方法 encode() 和 matches() 实际上是其职责的定义。它们都是同一约定的一部分,因为它们之间有很强的联系。应用程序编码密码的方式与验证密码的方式相关。让我们先看看 PasswordEncoder 接口的内容:

public interface PasswordEncoder {  String encode(CharSequence rawPassword);  boolean matches(CharSequence rawPassword, String encodedPassword);  default boolean upgradeEncoding(String encodedPassword) {     return false;   }}

该接口定义了两个抽象方法和一个具有默认实现的方法。在处理 PasswordEncoder 实现时,抽象的 encode() 和 matches() 方法也是您最常听到的方法。

encode(CharSequence rawPassword) 方法的目的是返回所提供字符串的转换。就 Spring Security 功能而言,它用于为给定的密码提供加密或散列。之后您可以使用 matches(CharSequence rawPassword, String encodedPassword) 方法来检查经过编码的字符串是否与原始密码匹配。在身份验证过程中使用 matches() 方法来根据一组已知凭据测试所提供的密码。第三个方法称为 upgradeEncoding(CharSequence encodedPassword),在约定中默认为 false。如果您覆盖它以返回 true,那么将再次对已编码的密码进行编码,以提高安全性。

在某些情况下,对已编码的密码进行编码会使从结果中获取明文密码变得更加困难。总的来说,这是一种我个人不喜欢的含糊。但是框架为你提供了这种可能性如果你认为它适用于你的情况。

实现 PasswordEncoder 约定

正如您所看到的,matches() 和 encode() 这两个方法具有很强的关系。如果你重写它们,它们应该在功能上始终是对应的: encode() 方法返回的字符串应该始终可以用相同 PasswordEncoder 的 matches() 方法进行验证。在本节中,您将实现 PasswordEncoder 约定,并定义接口声明的两个抽象方法。了解了如何实现 PasswordEncoder 后,您可以选择应用程序如何为身份验证过程管理密码。最简单的实现是密码编码器,它以纯文本的形式考虑密码:也就是说,它不对密码进行任何编码。

NoOpPasswordEncoder实例正是以明文形式管理密码。 我们在前面文章的示例中使用了该类。如果您要编写自己的类,它将看起来像下面的清单。

清单 1 PasswordEncoder 的最简单实现

public class PlainTextPasswordEncoder   implements PasswordEncoder {  @Override  public String encode(CharSequence rawPassword) {    // 我们不会更改密码,只需按原样返回即可。    return rawPassword.toString();  }  @Override  public boolean matches(    CharSequence rawPassword, String encodedPassword) {      // 检查两个字符串是否相等      return rawPassword.equals(encodedPassword);  }}

编码的结果总是与密码相同。因此,要检查这些是否匹配,只需要将字符串与 equals() 进行比较。使用散列算法 SHA-512 的 PasswordEncoder 的简单实现如下所示。

清单 2 实现使用SHA-512的PasswordEncoder

public class Sha512PasswordEncoder   implements PasswordEncoder {  @Override  public String encode(CharSequence rawPassword) {    return hashWithSHA512(rawPassword.toString());  }  @Override  public boolean matches(    CharSequence rawPassword, String encodedPassword) {    String hashedPassword = encode(rawPassword);    return encodedPassword.equals(hashedPassword);  }  // 忽略代码}

在清单 2 中,我们使用一种方法对 SHA-512 提供的字符串值进行哈希处理。 我在清单4.2中省略了该方法的实现,但是在清单 3 中可以找到它。 我们从 encode() 方法调用此方法,该方法现在返回其输入的哈希值。 为了根据输入验证哈希,matches() 方法在其输入中对原始密码进行哈希处理,并将其与对其进行验证的哈希进行相等性比较。

清单 3 SHA-512 对输入进行哈希处理的方法的实现

private String hashWithSHA512(String input) {  StringBuilder result = new StringBuilder();  try {    MessageDigest md = MessageDigest.getInstance("SHA-512");    byte [] digested = md.digest(input.getBytes());    for (int i = 0; i < digested.length; i++) {       result.append(Integer.toHexString(0xFF & digested[i]));    }  } catch (NoSuchAlgorithmException e) {    throw new RuntimeException("Bad algorithm");  }  return result.toString();}

在下一节中,您将学到更好的方法,所以现在不要对这段代码费心太多。

从提供的 PasswordEncoder 实现中进行选择

在知道如何实现 PasswordEncoder 的功能很强大的同时,您还必须知道 Spring Security已经为您提供了一些很好的实现。 如果其中之一与您的应用程序匹配,则无需重写它。 在本节中,我们讨论 Spring Security 提供的 PasswordEncoder 实现选项。 这些都有:

NoOpPasswordEncoder不对密码进行编码,但将其保留为明文形式。 我们仅将此实现用作示例。 因为它不散列密码,你不应该在真实的场景中使用它。StandardPasswordEncoder使用 SHA-256 对密码进行哈希处理。 该实现现已弃用,您不应将其用于新的实现。 不建议使用该算法的原因是,它使用了我们认为不够强大的哈希算法,但是您仍然可以在现有应用程序中找到该实现。Pbkdf2PasswordEncoder使用基于密码的密钥派生函数2 (PBKDF2)。BCryptPasswordEncoder使用bcrypt强散列函数对密码进行编码。SCryptPasswordEncoder—使用scrypt哈希函数对密码进行编码。

关于哈希和这些算法的更多信息,你可以在 David Wong 的《真实世界密码学》(Manning, 2020) 的第 2 章中找到很好的讨论。

让我们看一些例子,看看如何创建这些 PasswordEncoder 实现类型的实例。NoOpPasswordEncoder 不编码密码。它的实现类似于清单 1 中的示例中的 PlainTextPasswordEncoder。因此,我们只在理论示例中使用这个密码编码器。同样,NoOpPasswordEncoder 类被设计为一个单例。你不能直接从类外部调用它的构造函数,但是你可以使用 NoOpPasswordEncoder.getInstance() 方法来获得类的实例,如下所示:

PasswordEncoder p = NoOpPasswordEncoder.getInstance();

Spring Security 提供的 StandardPasswordEncoder 实现使用 SHA-256 散列密码。对于 StandardPasswordEncoder,您可以提供一个用于散列过程的秘钥。您可以通过构造函数的参数设置这个 secret 的值。如果选择调用无参数构造函数,则实现将使用空字符串作为键的值。但是,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)。那本书的第三章提供了关于 HMAC 的详细信息。你可以在 上找到这本书。

如果没有为 Pbkdf2PasswordEncoder 实现指定第二个或第三个值之一,则迭代次数的默认值为 185000,结果长度的默认值为 256。您可以通过选择另外两个重载构造函数中的一个来指定迭代次数和结果长度的自定义值:没有参数的构造函数 Pbkdf2PasswordEncoder(),或者只接收 secret 值作为参数的构造函数 Pbkdf2PasswordEncoder(“secret”)。

Spring Security 提供的另一个优秀的选择是 BCryptPasswordEncoder,它使用 bcrypt 强哈希函数对密码进行编码。 您可以通过调用无参数构造函数来实例化 BCryptPasswordEncoder。 但是,您也可以选择指定强度系数,以表示编码过程中使用的对数轮(对数轮)。 此外,您还可以更改用于编码的 SecureRandom 实例:

PasswordEncoder p = new BCryptPasswordEncoder();PasswordEncoder p = new BCryptPasswordEncoder(4);SecureRandom s = SecureRandom.getInstanceStrong();PasswordEncoder p = new BCryptPasswordEncoder(4, s);

您提供的对数轮会影响散列操作使用的迭代次数。使用的迭代次数是2对数轮次幂。对于迭代数计算,对数轮数的值只能在 4 到 31 之间。可以通过调用第二个或第三个重载构造函数之一来指定这个函数,如前面的代码片段所示。

我提供给您的最后一个选项是 SCryptPasswordEncoder (图 2)。这个密码编码器使用一个密码散列函数。对于 ScryptPasswordEncoder,你有两个选项来创建它的实例:

PasswordEncoder p = new SCryptPasswordEncoder();PasswordEncoder p = new SCryptPasswordEncoder(16384, 8, 1, 32, 64);

如果您通过调用无参数构造函数来创建实例,那么前面示例中的值就是所使用的值。

图 2

SCryptPasswordEncoder 构造函数采用五个参数,并允许您配置CPU开销,内存开销,键长和盐长。

使用DelegatingPasswordEncoder的多种编码策略

在本节中,我们将讨论身份验证流程中必须应用各种实现来匹配密码的情况。您还将学习如何在您的应用程序中应用一个充当 PasswordEncoder 的有用工具。这个工具没有自己的实现,而是委托其他实现 PasswordEncoder 接口的对象。

在某些应用程序中,您可能会发现使用各种密码编码器并根据某些特定配置进行选择是很有用的。我发现在生产应用程序中 DelegatingPasswordEncoder 的一个常见问题是,编码算法发生了改变,从应用程序的特定版本开始。假设有人在当前使用的算法中发现了漏洞,您希望为新注册的用户更改该漏洞,但不希望为现有凭据更改该漏洞。所以最终会有多种哈希。你是如何处理这种情况的?虽然这不是此场景的唯一方法,但一个好的选择是使用 DelegatingPasswordEncoder 对象。

DelegatingPasswordEncoder 是 PasswordEncoder 接口的一个实现,它不是实现自己的编码算法,而是委托给相同约定的另一个实现实例。散列以一个前缀开始,该前缀命名用于定义该散列的算法。DelegatingPasswordEncoder 根据密码的前缀委托给 PasswordEncoder 的正确实现。

这听起来很复杂,但是通过一个例子,你可以发现它非常简单。图 3 展示了 PasswordEncoder 实例之间的关系。DelegatingPasswordEncoder 有一个它所委托的 PasswordEncoder 实现列表。DelegatingPasswordEncoder 将每个实例存储在一个map中。NoOpPasswordEncoder 被分配给键 noop,而 BCryptPasswordEncoder 实现被分配给键 bcrypt。当密码有前缀 {noop} 时,DelegatingPasswordEncoder 将操作委托给 NoOpPasswordEncoder 实现。如果前缀是 {bcrypt},则该操作被委托给 BCryptPasswordEncoder 实现,如图 4 所示。

图 3

在本例中,DelegatingPasswordEncoder 为前缀 {noop} 注册了一个 NoOpPasswordEncoder ,为前缀 {bcrypt} 注册了一个 BCryptPasswordEncoder,为前缀 {scrypt} 注册了一个 SCryptPasswordEncoder。如果密码有前缀 {noop}, DelegatingPasswordEncoder 将操作转发给 NoOpPasswordEncoder 实现。

图 4

在这种情况下,DelegatingPasswordEncoder 为前缀 {noop} 注册 NoOpPasswordEncoder,为前缀 {bcrypt} 注册 BCryptPasswordEncoder,并为前缀 {scrypt} 注册 SCryptPasswordEncoder。 当密码具有前缀 {bcrypt} 时,DelegatingPasswordEncoder 将操作转发到 BCryptPasswordEncoder 实现。

接下来,让我们了解如何定义 DelegatingPasswordEncoder。 首先,创建所需的 PasswordEncoder 实现的实例集合,然后将它们放到 DelegatingPasswordEncoder 中,如下面的清单所示。

清单 4 创建 DelegatingPasswordEncoder 的实例

@Configurationpublic class ProjectConfig {  / Omitted code  @Bean  public PasswordEncoder passwordEncoder() {    Map<String, PasswordEncoder> encoders = new HashMap<>();    encoders.put("noop", NoOpPasswordEncoder.getInstance());    encoders.put("bcrypt", new BCryptPasswordEncoder());    encoders.put("scrypt", new SCryptPasswordEncoder());    return new DelegatingPasswordEncoder("bcrypt", encoders);  }}

DelegatingPasswordEncoder 只是一个作为 PasswordEncoder 的工具,所以当你必须从一组实现中选择时,你可以使用它。在清单 4 中,DelegatingPasswordEncoder 声明的实例包含了对 NoOpPasswordEncoder、BCryptPasswordEncoder 和 SCryptPasswordEncoder的引用,并将默认值委托给 BCryptPasswordEncoder 实现。基于散列的前缀,DelegatingPasswordEncoder 使用正确的 PasswordEncoder 实现来匹配密码。该前缀具有标识从编码器映射中使用的密码编码器的键。如果没有前缀,DelegatingPasswordEncoder 使用默认编码器。默认的 PasswordEncoder 是在构造 DelegatingPasswordEncoder 实例时作为第一个参数给出的。对于清单 4 中的代码,默认的 PasswordEncoder 是 bcrypt。

花括号是散列前缀的一部分,它们应该围绕在键名周围。例如,如果提供的散列是 {noop}12345, DelegatingPasswordEncoder 就会委托给 NoOpPasswordEncoder,我们为前缀 noop 注册了 NoOpPasswordEncoder。同样,不要忘记在前缀中必须使用大括号。

如果哈希值类似于下一个代码片段,那么密码编码器就是我们分配给前缀 {bcrypt} 的那个,它是 BCryptPasswordEncoder。如果没有任何前缀,这也是应用程序将委托的,因为我们将它定义为默认实现:

{bcrypt}$2a$10$xn3LI/AjqicFYZFruSwve.681477XaVNaUQbr1gioaWPn4t1KsnmG

为了方便起见,Spring Security 提供了一种创建 DelegatingPasswordEncoder 的方法,该方法有一个映射到 PasswordEncoder 的所有标准提供的实现。PasswordEncoderFactories 类提供了一个 createDelegatingPasswordEncoder() 静态方法,该方法将以bcrypt作为默认编码器返回 DelegatingPasswordEncoder 的实现:

PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

Encoding vs. encrypting vs. hashing

在前几节中,我经常使用编码、加密和散列等术语。我想简要地澄清这些术语以及我们在整个系列中使用它们的方式。

编码指的是对给定输入的任何转换。例如,如果我们有一个反转字符串的函数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) ->布尔值。

有时,哈希函数还可以使用添加到输入中的随机值:(x,k)-> y。 我们将此值称为盐。 盐使函数更强大,从而增加了应用反向函数以从结果中获取输入的难度。

为了总结我们到目前为止在本系列中讨论和应用的约定,表 1 简要地描述了每一个组件。

表 1 在Spring Security中表示身份验证流的主要约定的接口

标签: #私有密码算法 pbkdf2