spring security的BCryptPasswordEncoder加密和對密碼驗證的原理

BCryptPasswordEncoder加密和對密碼驗證的原理

上一篇:spring security進階2 添加帳戶並對帳戶密碼進行加密java

spring security中提供了一個加密類BCryptPasswordEncoder,能夠用來對密碼字符串進行加密,獲得加密後的字符串。它採用哈希算法 SHA-256 +隨機鹽+密鑰對密碼進行加密算法

1、加密算法和hash算法的區別

加密算法是一種可逆的算法,基本過程就是對原來爲明文的文件或數據按某種算法進行處理,使其成爲不可讀的一段代碼爲「密文」,但在用相應的密鑰進行操做以後就能夠獲得原來的內容 。spring

哈希算法是一種不可逆的算法,是把任意長度的輸入經過散列算法變換成固定長度的輸出,輸出就是散列值,不一樣的輸入可能會散列成相同的輸出,因此不可能從散列值來肯定惟一的輸入值。app

2、源碼解析

BCryptPasswordEncoder類實現了PasswordEncoder接口,這個接口中定義了兩個方法dom

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

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

下面咱們來看下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方法中作了什麼code

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來進行,最終獲得加密字符串。htm

因此這裏有一個重點:傳入的鹽值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, hashpw(plaintext, hashed))這裏,第一個參數是加密後的字符串,而第二個參數是用剛纔提過的hashpw方法對明文字符串進行加密。

hashpw(plaintext, hashed)第一個參數是明文,第二個參數是加密字符串,可是在這裏是做爲鹽值salt傳入的,因此就用到了剛纔說的 hashpw 內部經過傳入的salt獲得real_salt,這樣就保證了對如今要校驗的明文的加密和獲得已有密文的加密用的是一樣的加密策略,算法和鹽值都相同,這樣若是新產生的密文和原來的密文相同,則這兩個密文對應的明文字符串就是相等的。

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

3、總結

BCryptPasswordEncoder使用哈希算法+隨機鹽來對字符串加密。由於哈希是一種不可逆算法,因此密碼認證時須要使用相同的算法+鹽值來對待校驗的明文進行加密,而後比較這兩個密文來進行驗證。BCryptPasswordEncoder在加密時經過從傳入的salt中獲取real_salt用來加密,保證了這一點。

相關文章
相關標籤/搜索