zoukankan      html  css  js  c++  java
  • Spring Security PasswordEncoder 密码校验和密码加密

    Spring Security PasswordEncoder 密码校验和密码加密流程

    PasswordEncoder 使用

    首先我们先来看看一个创建密码编码器工厂方法

    org/springframework/security/crypto/factory/PasswordEncoderFactories.java

    public static PasswordEncoder createDelegatingPasswordEncoder() {
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
        encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
        encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
    
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }

    上述代码 encoders 的 Map 包含了很多种密码编码器,有 ldap 、MD4 、 MD5 、noop 、pbkdf2 、scrypt 、SHA-1 、SHA-256
    上面静态工厂方法可以看出,默认是创建并返回一个 BCryptPasswordEncoder,同时该 BCryptPasswordEncoder( PasswordEncoder 子类)也是 Spring Security 推荐的默认密码编码器,其中 noop 就是不做处理默认保存原密码。

    一般我们代码中 @Autowired 注入并使用 PasswordEncoder 接口的实例,然后调用其 matches 方法去匹配原密码和数据库中保存的“密码”;密码的校验方式有多种,从 PasswordEncoder 接口实现的类是可以知道。

    业务代码中注入 PasswordEncoder

    @Autowired
    private PasswordEncoder passwordEncoder;

     

    知识混淆点

    加密/解密 与 Hash 这两个概念不能混淆,比如:SHA 系列是 Hash 算法,不是加密算法,加密意味着可以解密,但是 Hash 是不可逆的(无法通过 Hash 值还原得到密码,只能比对 Hash 值看看是否相等)。

    安全性问题

    目前很大一部分存在安全问题的系统一般仅仅使用密码的 MD5 值进行保存,可以通过 MD5 查询库去匹配对大部分的密码(可以直接从彩虹表里反推出来),而且 MD5 计算 Hash 值碰撞容易构造,安全性大大降低。MD5 加盐在本地计算速度也是很快,也是密码短也是极其容易破解;更好的选择是 SHA-256、BCrypt 等等等

    密码匹配流程的源码解释

    本文简单说一下 BCryptPasswordEncoder 密码匹配的一个简单流程或者过程。

    重点

    如果是使用 BCryptPasswordEncoder 调用 encode() 方法编码输入密码的话,其实这个编码后的“密码”并不是我们平时输入的真正密码,而是密码加盐后的通过单向 Hash 算法(BCrypt)得到值。

    这里面细心的同学可能会发现一些问题:

    • 同一个密码计算 Hash 不应该是一样的吗?每次使用 BCryptPasswordEncoder 编码同一个密码都是不一样的?

    • BCryptPasswordEncoder 编码同一个密码后结果都不一样,怎么进行匹配?

    下面通过源码简单说一下这个匹配的流程:
    matches(CharSequence rawPassword, String encodedPassword) 方法根据两个参数都可以知道

    • 第一个参数是原密码
    • 第二个参数就是用 PasswordEncoder 调用 encode(CharSequence rawPassword) 编码过后保存在数据库的密码。

    org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java

    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);
    }

    上述代码解读:首先判断是否数据库保存的“密码”(后面简称:“密码”)是否为空或者 null ,在通过正则表达式匹配“密码”是否符合格式,最后通过 BCryptcheckpw(String plaintext, String hashed) 方法进行密码匹配


    再详细看看 BCryptcheckpw(String plaintext, String hashed) 方法:

    org/springframework/security/crypto/bcrypt/BCrypt.java

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

    第二个参数 hashed 表明其实数据库查询出来的“密码”也就是 Hash 值;equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed)) 代码中通过调用 hashpw 计算输入密码的 Hash 值(参数分别是输入的密码和保存在数据库的“密码”)


    再继续看 hashpw 里面的部分代码(内容过长,省略部分代码,看看代码中的中文注释):

    org/springframework/security/crypto/bcrypt/BCrypt.java

    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就是 Hash 计算前的密码盐(关于盐的介绍:https://zh.wikipedia.org/wiki/%E7%9B%90_(%E5%AF%86%E7%A0%81%E5%AD%A6))
        
        real_salt = salt.substring(off + 3, off + 25);
        try {
            passwordb = (password + (minor >= 'a' ? "00" : "")).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 )进行一系列校验(长度校验等)并截取“密码”中相应的密码盐,利用这个密码盐进行同样的一系列计算 Hash 操作和 Base64 编码拼接一些标识符 生成所谓的“密码”,最后 equalsNoEarlyReturn 方法对同一个密码盐生成的两个“密码”进行匹配。

    上述大致就是密码匹配流程了,对于问题“ BCryptPasswordEncoder 编码同一个密码后结果都不一样,怎么进行匹配”的简单解答:

    因为密码盐是随机生成的,但是可以根据数据库查询出来的“密码”拿到密码盐,同一个密码盐+原密码计算 Hash 结果值是能匹配的。

    密码“加密”保存源码解释

    看看加密的一个过程,

    org/springframework/security/crypto/bcrypt/BCryptPasswordEncoder.java

    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);
    }

    encode 方法传入是原密码,其中 int strength, SecureRandom random 这两个构造参数是 BCryptPasswordEncoder(int strength, SecureRandom random) 构造方法按需传入,如果不指定strength和random,默认执行 BCrypt.gensalt() 这行代码生成也相应密码随机盐。


    先看看 gensalt(int log_rounds, SecureRandom random) 方法的代码(可以看看中文注释):

    org/springframework/security/crypto/bcrypt/BCrypt.java

    public static String gensalt(int log_rounds, SecureRandom random) {
        // 一些检验
        if (log_rounds < MIN_LOG_ROUNDS || log_rounds > MAX_LOG_ROUNDS) {
            throw new IllegalArgumentException("Bad number of rounds");
        }
        StringBuilder rs = new StringBuilder();
        byte rnd[] = new byte[BCRYPT_SALT_LEN];
    
        // 生成随机字节并将其置于rnd字节数组
        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();
    }

    最终上面的 gensalt 方法得到一个 随机密码盐+无用字符串(这个字符串可以理解为你输入的密码) 计算 Hash 操作和 Base64 编码拼接一些标识符 生成假“密码”(这个假“密码”为了兼容方便调用 hashpw 方法),最后关键点就是调用 BCrypt.hashpw 方法取到密码盐生成相应的真实“密码”(这个得到的密码可以用于保存在数据库中了)。

    对于问题“同一个密码计算 Hash 不应该是一样的吗?每次使用 BCryptPasswordEncoder 编码同一个密码都是不一样的?”的简单解答:

    因为用到的随机密码盐每次都是不一样的,同一个密码和不同的密码盐组合计算出来的 Hash 值肯定不一样啦,所以编码同一个密码得到的结果都是不一样。

    建议和想法

    本文主要讲解一些安全性防护的思想,学习的过程思想很重要。

    登录注册是每个系统都具备的功能,开发的同学记住一定不能保存明文密码,否则被脱库就会造成严重的后果。如果是通过上述的方法进行密码保存,即便拿到“密码”也非常难还原密码。

    上述在密码编码的过程中的思想还是需要掌握:

    1. 只是保存散列码是不安全的,但是我们可以为密码加盐再通过一些 Hash 值 低概率碰撞且计算速度慢 的散列算法计算 Hash 值保存。

    2. Spring Security 每次 Hash 之前用的盐都是随机,盐可以保存在最终生成的“密码”中,这样每个密码都是用了相应不同的随机盐+原密码计算 Hash 值得到,暴力破解难度也变大了。




    原链接:https://www.jianshu.com/p/922963106729

  • 相关阅读:
    JS对文本框值的判断
    PostgreSQL导出表中数据
    postgreSQL中跨库查询在windows下的实现方法
    获取表中每个编号最新一条数据的集合
    开源跨平台声波传输库:Sonic
    Windows下配置cygwin和ndk编译环境
    char的定义在iOS和Android下是不同的
    stdout引发的curl 302跳转 crash
    WebBrowser内嵌页面的跨域调用问题
    【已解决】Ubuntu 12.04 LTS Source安装nodejs时出现"bash ./configure permission denied"
  • 原文地址:https://www.cnblogs.com/yangzhixue/p/12257885.html
Copyright © 2011-2022 走看看