SpringSecurity中的Bcrypt加密方法源码解析

spring Security中的BCryptPasswordEncoder类采用SHA-256 +随机盐+密钥对密码进行加密。SHA是一系列的加密算法,有SHA-1、SHA-2、SHA-3三大类,SHA-256是SHA-2下细分出的一种算法,此算法发生哈希碰撞的概率几乎为0,安全性高。

  • BCryptPasswordEncoder类实现了PasswordEncoder接口的encode和matches方法,来进行密码加密和匹配
    1. 加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。
      • BCryptPasswordEncoder类定义了两个final变量,用来控制encode方法的加密规则。strength是一个取值在-1或者4~31之间的int变量,而继承了java.util.random的SecureRandom类则提供了一种强加密RNG手段(PRNG),random是一个SecureRandom类的final变量,为后续生成salt起作用。
        private final int strength;
        private final SecureRandom random;
      
      • encode方法根据strength值的不同和有无SecureRandom对象使用了三种方式生成salt,但这三种方式本质其实是类似的,底层都是调用BCrypt类的gensalt(this.strength, this.random)方法,只是如果没有传入自定义的strength和SecureRandom对象,BCrypt类会自动帮我们将strength设为10和实例化SecureRandom对象传入方法中:
      public String encode(CharSequence rawPassword) {
      //声明一个“盐”变量
      String salt;
      //生成随机盐
      if (this.strength > 0) {
          if (this.random != null) {
              salt = BCrypt.gensalt(this.strength, this.random);
          } else {
              salt = BCrypt.gensalt(this.strength);
          }
            } else {
          salt = BCrypt.gensalt();
                }
            return BCrypt.hashpw(rawPassword.toString(), salt);
          }
      
      • 只有当strength在[4,31]取值时,gensalt方法才会返回“盐”值,此方法通过调用random.nextBytes()和encode_base64()方法编码生成随机盐字符串;nextBytes()方法会调用SecureRandomSpi抽象类的engineNextBytes方法生成一串长度为16随机的byte数组,而encode_base64()方法通过多次借助byte数组和长度为64的char数组base64_code(包含大部分ASCII字符)进行Base64编码,最终生成长度为29的随机盐salt字符串。
      public static String gensalt(int log_rounds, SecureRandom random) {
         if (log_rounds >= 4 && log_rounds <= 31) {
         StringBuilder rs = new StringBuilder();
         byte[] rnd = new byte[16];
         random.nextBytes(rnd);
         rs.append("$2a$");
         if (log_rounds < 10) {
             rs.append("0");
         }
         rs.append(log_rounds);
         rs.append("$");
         encode_base64(rnd, rnd.length, rs);
         return rs.toString();
      } else {
         throw new IllegalArgumentException("Bad number of rounds");
      }
      }
      
      static void encode_base64(byte[] d, int len, StringBuilder rs) throws IllegalArgumentException {
      int off = 0;
      if (len > 0 && len <= d.length) {
         while(off < len) {
             int c1 = d[off++] & 255;
             rs.append(base64_code[c1 >> 2 & 63]);
             c1 = (c1 & 3) << 4;
             if (off >= len) {
                 rs.append(base64_code[c1 & 63]);
                 break;
             }
             int c2 = d[off++] & 255;
             c1 |= c2 >> 4 & 15;
             rs.append(base64_code[c1 & 63]);
             c1 = (c2 & 15) << 2;
             if (off >= len) {
                 rs.append(base64_code[c1 & 63]);
                 break;
             }
             c2 = d[off++] & 255;
             c1 |= c2 >> 6 & 3;
             rs.append(base64_code[c1 & 63]);
             rs.append(base64_code[c2 & 63]);
             }
         } else {
         throw new IllegalArgumentException("Invalid len");
         }
      }
      
      • 将生成的盐值和原始密码传入BCrypt类的hashpw()方法进行加密,该方法对传入的盐进行了一系列校验(长度、版本等等),确保是有效salt。同时将原始密码转成passwordb字节数组。一个有效的salt前7位是校验位,包含了盐版本、盐rounds,第8位到30位为real_salt,将real_salt传入decode_base64方法进行转码,字符串real_salt被转换成字节数组输出给长度为16的saltb,接着将saltb和passwordb字节数组传入crypt_raw()方法进行SHA-256加密生成伪随机hash值,最后将saltb和hash值分别进行encode_base64方法进行Base64编码(其中saltb字节数组通过编码重新变成realSalt字符串,这也是后续matches方法匹配密码的),产生的结果拼接成60位的随机密码,前7位同样是校验位,第8位到30位为real_salt。
      public static String hashpw(String password, String salt) throws IllegalArgumentException {
                  .....dosomework.....
          int rounds = Integer.parseInt(salt.substring(off, off + 2));
           String real_salt = salt.substring(off + 3, off + 25);
           byte[] passwordb;
           try {
            passwordb = (password + (minor >= 'a' ? "\u0000" : "")).getBytes("UTF-8");
        } catch (UnsupportedEncodingException var13) {
              throw new AssertionError("UTF-8 is not supported");
           }
            byte[] saltb = decode_base64(real_salt, 16);
            BCrypt B = new BCrypt();
            byte[] hashed = B.crypt_raw(passwordb, saltb, rounds);
            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();    
        }
      
    2. 密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是将输入的密码与数据库查出的密码同样传入BCrypt类的pwhash()中进行加密,由于算法将加密后密码的第8位到30位作为real_salt,第一次执行pwhash方法传入的盐和第二次传入的盐值(数据库密码)是包含关系,两者的前30位是相同的。那么根据相同的real_salt和相同的password生成的加密密码很显然也是相同的。
       public boolean matches(CharSequence rawPassword, String encodedPassword) {
            if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
              this.logger.warn("Encoded password does not look like BCrypt");
              return false;
          } else {
              return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
              }
          } else {
              this.logger.warn("Empty encoded password");
              return false;
          }
      }
      

你可能感兴趣的:(SpringSecurity中的Bcrypt加密方法源码解析)