BCryptPasswordEncoder的使用及原理

本文涉及的产品
密钥管理服务KMS,1000个密钥,100个凭据,1个月
简介: BCryptPasswordEncoder的使用及原理

      在 Spring Security 中有一个加密的类 BCryptPasswordEncoder ,它的使用非常的简单而且也比较有趣。让我们来看看它的使用。

BCryptPasswordEncoder 的使用

       首先创建一个 SpringBoot 的项目,在创建项目的时候添加 Spring Security 的依赖。然后我们添加一个测试类,写如下的代码:

final private String password = "123456";
@Test
public void TestCrypt()
{
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode1 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode1:" + encode1);
    String encode2 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode2:" + encode2);
}

       上面的代码中,首先实例化了一个 BCryptPasswordEncoder 类,然后使用该类的 encode 方法对同一个明文字符串进行了加密,并输出。运行上面的代码,查看输出。

encode1:$2a$10$SqbQb0pD3KYrH7ZVTWdRZOhPAelQqa..lUnysXoWag6RvMkyC5SE6
encode2:$2a$10$0sjBLlwrrch2EjgYls197e9dGRCMbQ7KUIt/ODPTSU0W.mEPaGkfG

       从上面的输出可以看出,同一个明文加密两次,却输出了不同的结果。是不是很神奇?但是这样有一个问题,如果使用 BCryptPasswordEncoder 去加密登录密码的话,还能进行验证么?当然是可以验证的。验证的话,使用的是 BCryptPasswordEncoder 的 matches 方法,代码如下。

final private String password = "123456";
@Test
public void TestCrypt()
{
    BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
    String encode1 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode1:" + encode1);
    boolean matches1 = bCryptPasswordEncoder.matches(password, encode1);
    System.out.println("matches1:" + matches1);
    String encode2 = bCryptPasswordEncoder.encode(password);
    System.out.println("encode2:" + encode2);
    boolean matches2 = bCryptPasswordEncoder.matches(password, encode2);
    System.out.println("matches2:" + matches2);
}

     使用 matches 方法可以对加密前和加密后是否匹配进行验证。输出如下:

encode1:$2a$10$qxU.rFLeTmZg47FyqJlZwu.QNX9RpEvqBUJiwUvUE0p4ENR.EndfS
matches1:true
encode2:$2a$10$NyGEOsQ1Hxv2gvYRmaEENueORlVDtSqoB/fHN76KkvQDeg7fbTy22
matches2:true

    可以看到两次加密后的字符串虽然不同,但是通过 matches 方法都可以匹配出它们是 “123456” 这个明文加密的结果。同样很神奇,这是为什么呢?

encode 和 matches 方法的原理

      我们通过源码来看看它们的原理吧。


      首先我们将依赖的源码下载到本地,方便我们进行调试。开始我使用 IDEA 进行调试时是没有源码的,后来下载了源码,发现没有源码时调试的是 Class 文件,IDEA 提供了 Class 文件与源码等价的反编译代码。而源码逻辑性更好一些。


      首先在 encode 代码处下断点,然后我们单步步入,去查看 encode 的实现,代码如下:

@Override
public String encode(CharSequence rawPassword) {
  if (rawPassword == null) {
    throw new IllegalArgumentException("rawPassword cannot be null");
  }
  String salt = getSalt();
  return BCrypt.hashpw(rawPassword.toString(), salt);
}

 直接运行到 return 语句处,查看一下 salt 的值,该值如下:

$2a$10$H73jYFFLWWf.VV/mwonqru

       然后继续单步步入到 BCrypt.hashpw 方法内,该方法代码如下:

public static String hashpw(String password, String salt) {
  byte passwordb[];
  passwordb = password.getBytes(StandardCharsets.UTF_8);
  return hashpw(passwordb, salt);
}

    该方法的重点同样是 hashpw 方法,没有做什么处理,继续进入 hashpw 方法中。代码如下:

public static String hashpw(byte passwordb[], String salt) {
  BCrypt B;
  String real_salt;
  byte saltb[], hashed[];
  char minor = (char) 0;
  int rounds, off;
  StringBuilder rs = new StringBuilder();
  if (salt == null) {
    throw new IllegalArgumentException("salt cannot be null");
  }
  int saltLength = salt.length();
  if (saltLength < 28) {
    throw new IllegalArgumentException("Invalid salt");
  }
  if (salt.charAt(0) != '$' || salt.charAt(1) != '2') {
    throw new IllegalArgumentException("Invalid salt version");
  }
  if (salt.charAt(2) == '$') {
    off = 3;
  }
  else {
    minor = salt.charAt(2);
    if ((minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b') || salt.charAt(3) != '$') {
      throw new IllegalArgumentException("Invalid salt revision");
    }
    off = 4;
  }
  // Extract number of rounds
  if (salt.charAt(off + 2) > '$') {
    throw new IllegalArgumentException("Missing salt rounds");
  }
  if (off == 4 && saltLength < 29) {
    throw new IllegalArgumentException("Invalid salt");
  }
  rounds = Integer.parseInt(salt.substring(off, off + 2));
  real_salt = salt.substring(off + 3, off + 25);
  saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
  if (minor >= 'a') {
    passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
  }
  B = new BCrypt();
  hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 0x10000 : 0);
  rs.append("$2");
  if (minor >= 'a') {
    rs.append(minor);
  }
  rs.append("$");
  if (rounds < 10) {
    rs.append("0");
  }
  rs.append(rounds);
  rs.append("$");
  encode_base64(saltb, saltb.length, rs);
  encode_base64(hashed, bf_crypt_ciphertext.length * 4 - 1, rs);
  return rs.toString();
}

      代码很长,但是真正关键的代码在上面第 43 、44 和 51 行的位置处,43 行处获取真正的 salt ,44 行是使用 base64 进行解码,然后 51 行用 密码、salt 进行处理。在来看看返回值是 rs,在第 63 行和 64 行,对 salt 进行 base64 编码后放入了 rs 中,然后对 hashed 进行 base64 编码后也放入了 rs 中,最后 rs.toString() 返回。


       虽然上面代码很长,其实真正关键的就只有上面我提到的几句,其余的部分不用看。我们接着看 matches 的源码,同样单步进入,代码如下:

@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
  if (rawPassword == null) {
    throw new IllegalArgumentException("rawPassword cannot be null");
  }
  if (encodedPassword == null || encodedPassword.length() == 0) {
    this.logger.warn("Empty encoded password");
    return false;
  }
  if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
    this.logger.warn("Encoded password does not look like BCrypt");
    return false;
  }
  return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
}

       单步到第 14 行的 return 处,这里调用 BCrypt.checkpw 的方法,rawPassword.toString() 是我们的密码,即 ”123456“, 后面的 encodePassword 是我们加密后的密码,单步步入进去,代码如下:

public static boolean checkpw(String plaintext, String hashed) {
  return equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed));
}

     这里只有一行代码,但是代码中同样调用了前面的 hashpw 这个方法,传入的参数是 plaintext 和 hashed,plaintext 是我们的密码,即 “123456”, hashed 是加密后的密码。到这里基本就明白了。hashed 在进入 hashpw 函数后,会通过前面说到第 43 行代码取出真正的 salt,然后对通过 salt 和 我们的密码进行加密,这样流程就串联起来了。

总结

      当时看到使用 BCryptPasswordEncoder 时,同样的密码可以生成不同的 密文 而且还可以通过 matches 方法进行匹配验证,觉得很神奇。后来经过调试发现,密文中本身包含了很多信息,包括 salt 和 使用 salt 加密后的 hash。因为每次的 salt 不同,因此每次的 hash 也不同。这样就可以使得相同的 明文 生成不同的 密文,而密文中包含 salt 和 hash,因此验证过程和生成过程也是相同的。

       附一张大致的调用关系流程图,供大家参考。

相关文章
|
7月前
|
安全 Java 应用服务中间件
Shiro + JWT 进行登录验证
Shiro + JWT 进行登录验证
61 2
|
2月前
|
存储 安全 Java
shiro学习二:shiro的加密认证详解,加盐与不加盐两个版本。
这篇文章详细介绍了Apache Shiro安全框架中密码的加密认证机制,包括不加盐和加盐两种加密方式的实现和测试。
142 0
|
4月前
|
安全 Java Spring
Spring Security 报:Encoded password does not look like BCrypt
Spring Security 报:Encoded password does not look like BCrypt
127 1
|
4月前
|
存储 安全 Java
什么是 PasswordEncoder?
【8月更文挑战第21天】
98 0
|
7月前
|
算法 数据安全/隐私保护
bcrypt加密
bcrypt加密
129 0
|
数据安全/隐私保护
BCryptPasswordEncoder
BCryptPasswordEncoder
64 0
|
数据安全/隐私保护
Shiro之UsernamePasswordToken&RememberMeAuthenticationToken&AuthenticationToken
Shiro之UsernamePasswordToken&RememberMeAuthenticationToken&AuthenticationToken
|
存储 安全 JavaScript
Spring Security灵活的PasswordEncoder加密方式
本章基于`Spring Security 5.4.1`版本编写,从`5.x`版本开始引入了很多新的特性。 为了适配老系统的安全框架升级,`Spring Security`也是费劲了心思,支持不同的密码加密方式,而且根据不同的用户可以使用不同的加密方式。
|
安全 Java API
Java中使用Shiro实现对密码加盐并使用MD5加密处理
我们在保存用户密码等敏感信息的时候,需要进行加密处理保存,才能更安全地保护用户个人信息安全
329 0
|
JSON Java Maven
Shiro整合JWT实战
Shiro整合JWT实战
1456 0
Shiro整合JWT实战