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用来加密,保证了这一点。

相关文章
相关标签/搜索