Spring Security 加密

以前密码的密文保存都是自己写的MD5,这次用到了Spring Security,一开始对加密不太了解,于是看了下源码了解个大概,这里记录下。

Spring Security 为我们提供了一套加密规则和密码比对规则。org.springframework.security.crypto.password.PasswordEncoder 接口,该接口里面定义了三个方法。

package org.springframework.security.crypto.password;

public interface PasswordEncoder {
    String encode(CharSequence var1);

    boolean matches(CharSequence var1, String var2);

    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

可以看到,非常简单的提供了3个接口:
encode:对密码进行加密,返回加密后字符串。
matches:将密码和加密的密文进行比对,返回比对结果,即登录的密码校验。
upgradeEncoding:是否需要再次进行编码, 默认不需要。

一开始看到encode这个接口表示不太理解,因为一般加密都是需要混合,确保同样的密码密文也是不同的。也正是因为这个困惑,才决定一看究竟,了解清楚Spring Security是如何做加密密文的。
在这里插入图片描述
查看实现类的时候发现,还是封装了很多实现的。其中常用到的分别有下面这么几个
BCryptPasswordEncoder:Spring Security 推荐使用的,使用BCrypt强哈希方法来加密。
MessageDigestPasswordEncoder:用作传统的加密方式加密(支持 MD5、SHA-1、SHA-256…)
DelegatingPasswordEncoder:最常用的,根据加密类型id进行不同方式的加密,兼容性强
NoOpPasswordEncoder:明文, 不做加密
其他等等

而我们项目里用的就是BCryptPasswordEncoder,所以这次也就重点看BCryptPasswordEncoder。

    public String encode(CharSequence rawPassword) {
        if (rawPassword == null) {
            throw new IllegalArgumentException("rawPassword cannot be null");
        } else {
            String salt;
            if (this.random != null) {
                salt = BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
            } else {
                salt = BCrypt.gensalt(this.version.getVersion(), this.strength);
            }

            return BCrypt.hashpw(rawPassword.toString(), salt);
        }
    }

这是加密的源码,看到这里明白了为什么可以单密码加密,因为这里加密的时候有做加“盐”的操作,即同样的密码密文也是不同的。

    public static String gensalt(String prefix, int log_rounds, SecureRandom random) throws IllegalArgumentException {
        StringBuilder rs = new StringBuilder();
        byte[] rnd = new byte[16];
        if (!prefix.startsWith("$2") || prefix.charAt(2) != 'a' && prefix.charAt(2) != 'y' && prefix.charAt(2) != 'b') {
            throw new IllegalArgumentException("Invalid prefix");
        } else if (log_rounds >= 4 && log_rounds <= 31) {
            random.nextBytes(rnd);
            rs.append("$2");
            rs.append(prefix.charAt(2));
            rs.append("$");
            if (log_rounds < 10) {
                rs.append("0");
            }

            rs.append(log_rounds);
            rs.append("$");
            encode_base64(rnd, rnd.length, rs);
            return rs.toString();
        } else {
            throw new IllegalArgumentException("Invalid log_rounds");
        }
    }

但是紧着又有个疑问了,这里加“盐”是随机的,那密码匹配的时候怎么办呢?必须得知道之前的“盐”才能匹配上呀,所以接着往下看。

    public static String hashpw(byte[] passwordb, String salt) {
        char minor = 0;
        StringBuilder rs = new StringBuilder();
        if (salt == null) {
            throw new IllegalArgumentException("salt cannot be null");
        } else {
            int saltLength = salt.length();
            if (saltLength < 28) {
                throw new IllegalArgumentException("Invalid salt");
            } else if (salt.charAt(0) == '$' && salt.charAt(1) == '2') {
                byte off;
                if (salt.charAt(2) == '$') {
                    off = 3;
                } else {
                    minor = salt.charAt(2);
                    if (minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b' || salt.charAt(3) != '$') {
                        throw new IllegalArgumentException("Invalid salt revision");
                    }

                    off = 4;
                }

                if (salt.charAt(off + 2) > '$') {
                    throw new IllegalArgumentException("Missing salt rounds");
                } else if (off == 4 && saltLength < 29) {
                    throw new IllegalArgumentException("Invalid salt");
                } else {
                    int rounds = Integer.parseInt(salt.substring(off, off + 2));
                    String real_salt = salt.substring(off + 3, off + 25);
                    byte[] saltb = decode_base64(real_salt, 16);
                    if (minor >= 'a') {
                        passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
                    }

                    BCrypt B = new BCrypt();
                    byte[] hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 65536 : 0);
                    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();
                }
            } else {
                throw new IllegalArgumentException("Invalid salt version");
            }
        }
    }

这里可以看出,其实是把“盐”和密文拼接在一起了。所以做密码匹配的时候就可以拿到“盐”从而进行匹配校验。
做一次加密测试如下:

        String salt = BCrypt.gensalt();	
        String result = BCrypt.hashpw("Hello World", salt);
        System.out.println("盐的长度:" + salt.length());
        System.out.println("盐:" + salt);
        System.out.println("加密后密文:" + result);

盐的长度:29
盐: $2a 10 10 10MhiZmSmGUPKAMDuDowztOO
加密后密文:$2a 10 10 10MhiZmSmGUPKAMDuDowztOOdG/VtGGpucBM1nZ9YHeXsg0x.Wj.6yi

在密文中包含四段内容:
: 是 分 隔 符 。 2 a : 加 密 算 法 版 本 号 。 10 : 加 密 轮 次 , 默 认 为 10 , 数 值 越 大 , 加 密 时 间 和 越 难 破 解 呈 指 数 增 长 。 可 在 B C r y p t P a s s w o r d E n c o d e r 构 造 参 数 传 入 。 第 3 个 :是分隔符。 2a:加密算法版本号。 10:加密轮次,默认为10,数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入。 第3个 2a1010BCryptPasswordEncoder3之后:前面的内容是盐,后面的内容才是真正的密文。

密码匹配校验的核心代码如下:

    public static String hashpw(byte[] passwordb, String salt) {
        char minor = 0;
        StringBuilder rs = new StringBuilder();
        if (salt == null) {
            throw new IllegalArgumentException("salt cannot be null");
        } else {
            int saltLength = salt.length();
            if (saltLength < 28) {
                throw new IllegalArgumentException("Invalid salt");
            } else if (salt.charAt(0) == '$' && salt.charAt(1) == '2') {
                byte off;
                if (salt.charAt(2) == '$') {
                    off = 3;
                } else {
                    minor = salt.charAt(2);
                    if (minor != 'a' && minor != 'x' && minor != 'y' && minor != 'b' || salt.charAt(3) != '$') {
                        throw new IllegalArgumentException("Invalid salt revision");
                    }

                    off = 4;
                }

                if (salt.charAt(off + 2) > '$') {
                    throw new IllegalArgumentException("Missing salt rounds");
                } else if (off == 4 && saltLength < 29) {
                    throw new IllegalArgumentException("Invalid salt");
                } else {
                    int rounds = Integer.parseInt(salt.substring(off, off + 2));
                    String real_salt = salt.substring(off + 3, off + 25);
                    byte[] saltb = decode_base64(real_salt, 16);
                    if (minor >= 'a') {
                        passwordb = Arrays.copyOf(passwordb, passwordb.length + 1);
                    }

                    BCrypt B = new BCrypt();
                    byte[] hashed = B.crypt_raw(passwordb, saltb, rounds, minor == 'x', minor == 'a' ? 65536 : 0);
                    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();
                }
            } else {
                throw new IllegalArgumentException("Invalid salt version");
            }
        }
    }

可以看出,是从密码里取出“盐”,然后对要校验的密码用同样的“盐”进行加密,得到密文和保存的密文进行比对。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值