spring security的BCryptPasswordEncoder加密和对密码验证的原理分析

 更新时间:2024年11月28日 16:39:49   作者:农码天下  
文章介绍了加密算法和hash算法的基本概念,以及BCryptPasswordEncoder加密和解密的原理,加密算法是可逆的,需要加盐以保证安全性,BCryptPasswordEncoder通过生成盐值并在加密和解密过程中使用,确保相同的明文每次加密结果不同,从而提高安全性

一、加密算法和hash算法

很多项目中有些机密的信息需要进行加密来保护用户或者公司的信息安全,这时这些信息会采用加密以密文的形式暴露在外面。

加密算法是一种可逆的算法,是通过一定的规则对明文进行各种计算的到的密文从而实现加密的效果。

  • hash算法是不可逆的,常见的MD5加密采用的就是hash的算法进行加密。加密算法是可逆的,所以很多情况下加密规则是很重要的,一旦暴露就可以根据规则进行逆推得到明文,所以加密算法通常会加盐,对称加密和非对称加密就是加盐的一种。
  • hash算法虽然不可逆,但通过大数据进行匹配很多数据可以被找到,所以hash算法也是需要加盐来保证一定的机密性。

二、BCryptPasswordEncoder 加密和解密的原理

BCryptPasswordEncoder对同样的数据比如“11111”进行加密,每次加密的结果是不相同的,此时就要思考一个问题,同样的数据每次加密不同,那么它是如何进行解密的呢?

下面来分析一下此方式的加密源码,参考了网上的一些资料:

三、源码解析

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, hashpw(plaintext, hashed))

这里,第一个参数是加密后的字符串,而第二个参数是用刚才提过的hashpw方法对明文字符串进行加密。

hashpw(plaintext, hashed)

第一个参数是明文,第二个参数是加密字符串,但是在这里是作为盐值salt传入的,所以就用到了刚才说的 hashpw 内部通过传入的salt得到real_salt,这样就保证了对现在要校验的明文的加密和得到已有密文的加密用的是同样的加密策略,算法和盐值都相同,这样如果新产生的密文和原来的密文相同,则这两个密文对应的明文字符串就是相等的。

这也说明了加密时使用的盐值被写在了最终生成的加密字符串中。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java设计模式之单例模式的详解及优点

    java设计模式之单例模式的详解及优点

    这篇文章主要介绍了java设计模式之单例模式的详解及优点的相关资料,如果一个类始终只能创建一个实例,那么这个类被称为单例类,这种设计模式被称为单例模式,需要的朋友可以参考下
    2017-08-08
  • Springcloud hystrix服务熔断和dashboard如何实现

    Springcloud hystrix服务熔断和dashboard如何实现

    这篇文章主要介绍了Springcloud hystrix服务熔断和dashboard如何实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-12-12
  • Java如何使用字符流读写非文本文件

    Java如何使用字符流读写非文本文件

    这篇文章主要介绍了Java如何使用字符流读写非文本文件,以Java的字符流读取文件为例:它只能读取0-65535之间的字符,可以看出来字符都是正数,但是二进制的byte是可以为负数的,需要的朋友可以参考下
    2023-04-04
  • java如何读取超大文件

    java如何读取超大文件

    这篇文章主要为大家详细介绍了java如何读取超大文件,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-08-08
  • SpringCloud Feign客户端使用流程

    SpringCloud Feign客户端使用流程

    在springcloud中,openfeign是取代了feign作为负载均衡组件的,feign最早是netflix提供的,他是一个轻量级的支持RESTful的http服务调用框架,内置了ribbon,而ribbon可以提供负载均衡机制,因此feign可以作为一个负载均衡的远程服务调用框架使用
    2023-01-01
  • Security6.4.2 自定义异常中统一响应遇到的问题

    Security6.4.2 自定义异常中统一响应遇到的问题

    本文主要介绍了Security6.4.2 自定义异常中统一响应遇到的问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2025-03-03
  • Java实现数组转字符串及字符串转数组的方法分析

    Java实现数组转字符串及字符串转数组的方法分析

    这篇文章主要介绍了Java实现数组转字符串及字符串转数组的方法,结合实例形式分析了Java字符串及数组相关的分割、遍历、追加等操作技巧,需要的朋友可以参考下
    2018-06-06
  • Java中private关键字详细用法实例以及解释

    Java中private关键字详细用法实例以及解释

    这篇文章主要给大家介绍了关于Java中private关键字详细用法实例以及解释的相关资料,在Java中private是一种访问修饰符,它可以用来控制类成员的访问权限,文中将用法介绍的非常详细,需要的朋友可以参考下
    2024-01-01
  • java基础之接口组成更新的实现

    java基础之接口组成更新的实现

    本文主要介绍了java基础之接口组成更新的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-04-04
  • 详解Java的初始化与清理

    详解Java的初始化与清理

    这篇文章主要介绍了Java的初始化与清理,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07

最新评论