BCryptPasswordEncoder的使用及原理

简介: 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,因此验证过程和生成过程也是相同的。

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

相关文章
|
4天前
|
算法 数据安全/隐私保护
bcrypt加密
bcrypt加密
10 0
|
23天前
|
存储 算法 Java
加密解密(3DES)DES的加强版
加密解密(3DES)DES的加强版
|
2月前
|
算法 前端开发 JavaScript
SpringBoot+随机盐值+双重MD5实现加密登录
SpringBoot+随机盐值+双重MD5实现加密登录
248 1
|
4月前
|
存储 算法 数据安全/隐私保护
为什么选择bcrypt而非MD5:安全性在实际应用中的重要性
为什么选择bcrypt而非MD5:安全性在实际应用中的重要性
25 1
|
6月前
|
数据安全/隐私保护
BCryptPasswordEncoder
BCryptPasswordEncoder
31 0
|
存储 安全 算法
2021年你还不会Shiro?----4.使用MD5+盐+hash散列进行密码加密
上一篇文章里介绍了使用自定义的Realm来实现数据的获取,不过,数据的获取依然不是来源于真实的数据库或者是nosql,只是包装了一个方法,假装从这个方法里获取了数据库中的用户信息,然后我们返回了一个SimpleAccount,这个对象携带了用户名与密码,当时我们是明文返回的密码。这样做很显然是不安全的,一旦数据来源被攻破,所有的用户信息都会被泄露。所以这里我们介绍下,常用的密码加密策略。
153 0
2021年你还不会Shiro?----4.使用MD5+盐+hash散列进行密码加密
|
存储 算法 安全
分布式服务器框架之Servers.Core库实现 DES对称加密算法;SHA1信息摘要算法;MD5信息摘要算法
通信双方(通信主体)同时掌握一个钥匙,加解密都由这一个钥匙完成。通信双方通信前共同拟定一个密钥,不向第三方公开,发送前加密和接受后解密都由此密钥完成。即钥匙如果泄露,将暴露自己的全部信息。
|
缓存 安全 网络安全
Shiro框架02权限认证+MD5加盐加密+散列1024+Hex/Base64(源码)
Shiro框架02权限认证+MD5加盐加密+散列1024+Hex/Base64(源码)
Shiro框架02权限认证+MD5加盐加密+散列1024+Hex/Base64(源码)
|
安全 Java API
Java中使用Shiro实现对密码加盐并使用MD5加密处理
我们在保存用户密码等敏感信息的时候,需要进行加密处理保存,才能更安全地保护用户个人信息安全
231 0
|
Java 数据安全/隐私保护
java 密码MD5加密 加盐加密工具类
java 密码MD5加密 加盐加密工具类