1. 程式人生 > 程式設計 >Spring security BCryptPasswordEncoder密碼驗證原理詳解

Spring security BCryptPasswordEncoder密碼驗證原理詳解

一、加密演算法和hash演算法的區別

加密演算法是一種可逆的演算法,基本過程就是對原來為明文的檔案或資料按某種演算法進行處理,使其成為不可讀的一段程式碼為“密文”,但在用相應的金鑰進行操作之後就可以得到原來的內容 。

雜湊演算法是一種不可逆的演算法,是把任意長度的輸入通過雜湊演算法變換成固定長度的輸出,輸出就是雜湊值,不同的輸入可能會雜湊成相同的輸出,所以不可能從雜湊值來確定唯一的輸入值。

二、原始碼解析

BCryptPasswordEncoder類實現了PasswordEncoder介面,這個介面中定義了兩個方法

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

其中encode(...)是對字串進行加密的方法,matches使用來校驗傳入的明文密碼rawPassword是否和加密密碼encodedPassword相匹配的方法。即對密碼進行加密時呼叫encode,登入認證時呼叫matches

下面我們來看下BCryptPasswordEncoder類中這兩個方法的具體實現

1. encode方法

public String encode(CharSequence rawPassword) {
  String salt;
  if (strength > 0) {
    if (random != null) {
      salt = BCrypt.gensalt(strength,random);
    }
    else {
      salt = BCrypt.gensalt(strength);
    }
  }
  else {
    salt = BCrypt.gensalt();
  }
  return BCrypt.hashpw(rawPassword.toString(),salt);
}

可以看到,這個方法中先基於某種規則得到了一個鹽值,然後在呼叫BCrypt.hashpw方法,傳入明文密碼和鹽值salt。所以我們再看下BCrypt.hashpw方法中做了什麼

2. BCrypt.hashpw方法

public static String hashpw(String password,String salt) throws IllegalArgumentException {
    BCrypt B;
    String real_salt;
    byte passwordb[],saltb[],hashed[];
    char minor = (char) 0;
    int rounds,off = 0;
    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' || salt.charAt(3) != '$') {
        throw new IllegalArgumentException("Invalid salt revision");
      }
      off = 4;
    }

    if (saltLength - off < 25) {
      throw new IllegalArgumentException("Invalid salt");
    }

    // Extract number of rounds
    if (salt.charAt(off + 2) > '$') {
      throw new IllegalArgumentException("Missing salt rounds");
    }
    rounds = Integer.parseInt(salt.substring(off,off + 2));

    real_salt = salt.substring(off + 3,off + 25);
    try {
      passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
    }
    catch (UnsupportedEncodingException uee) {
      throw new AssertionError("UTF-8 is not supported");
    }

    saltb = decode_base64(real_salt,BCRYPT_SALT_LEN);

    B = new BCrypt();
    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();
  }

可以看到,這個方法中先根據傳入的鹽值salt,然後基於某種規則從salt得到real_salt,後續的操作都是用這個real_salt來進行,最終得到加密字串。

所以這裡有一個重點:傳入的鹽值salt並不是最終用來加密的鹽,方法中通過salt得到了real_salt,記住這一點,因為後邊的匹配方法matches中要用到這一點。

3. matches方法

matches方法用來判斷一個明文是否和一個加密字串對應。

public boolean matches(CharSequence rawPassword,String encodedPassword) {
  if (encodedPassword == null || encodedPassword.length() == 0) {
    logger.warn("Empty encoded password");
    return false;
  }

  if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
    logger.warn("Encoded password does not look like BCrypt");
    return false;
  }
  return BCrypt.checkpw(rawPassword.toString(),encodedPassword);
}

這個方法中先對密文字串進行了一些校驗,如果不符合規則直接返回不匹配,然後呼叫校驗方法BCrypt.checkpw,第一個引數是明文,第二個引數是加密後的字串。

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

static boolean equalsNoEarlyReturn(String a,String b) {
  char[] caa = a.toCharArray();
  char[] cab = b.toCharArray();

  if (caa.length != cab.length) {
    return false;
  }

  byte ret = 0;
  for (int i = 0; i < caa.length; i++) {
    ret |= caa[i] ^ cab[i];
  }
  return ret == 0;
}

注意 equalsNoEarlyReturn(hashed,hashed))這裡,第一個引數是加密後的字串,而第二個引數是用剛才提過的hashpw方法對明文字串進行加密。

hashpw(plaintext,hashed)第一個引數是明文,第二個引數是加密字串,但是在這裡是作為鹽值salt傳入的,所以就用到了剛才說的 hashpw 內部通過傳入的salt得到real_salt,這樣就保證了對現在要校驗的明文的加密和得到已有密文的加密用的是同樣的加密策略,演算法和鹽值都相同,這樣如果新產生的密文和原來的密文相同,則這兩個密文對應的明文字串就是相等的。

這也說明了加密時使用的鹽值被寫在了最終生成的加密字串中。

三、總結

BCryptPasswordEncoder使用雜湊演算法+隨機鹽來對字串加密。因為雜湊是一種不可逆演算法,所以密碼認證時需要使用相同的演算法+鹽值來對待校驗的明文進行加密,然後比較這兩個密文來進行驗證。BCryptPasswordEncoder在加密時通過從傳入的salt中獲取real_salt用來加密,保證了這一點。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。