Java-Security(三):加密的用法、PasswordEncoder类源码分析

在第一篇文章,我们展示了一个demo,其中讲到了对用户的密码进行了明文展示的用法,其实那么做是不安全的,在实际项目中往往会采用各种加密方法(比如:bcrypt,md5,sha1,sha2等)来实现对密码的保护。

本片文章将会主要讲解如何在Spring Security实现对密码加密的各种用法,以及对BCrypt的用法进一步分析。

概念

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

public interface PasswordEncoder {
    //加密(外面调用一般在注册的时候加密前端传过来的密码保存进数据库)
    String encode(CharSequence rawPassword);

    //加密前后对比(一般用来比对前端提交过来的密码和数据库存储密码, 也就是明文和密文的对比)
    boolean matches(CharSequence rawPassword, String encodedPassword);

    //是否需要再次进行编码, 默认不需要
    default boolean upgradeEncoding(String encodedPassword) {
        return false;
    }
}

在Spring Security下 PasswordEncoder 的实现类包含:

 其中常用到的分别有下面这么几个:   

    BCryptPasswordEncoder:Spring Security 推荐使用的,使用BCrypt强哈希方法来加密。
    MessageDigestPasswordEncoder:用作传统的加密方式加密(支持 MD5、SHA-1、SHA-256...)
    DelegatingPasswordEncoder:最常用的,根据加密类型id进行不同方式的加密,兼容性强
    NoOpPasswordEncoder:明文, 不做加密
    其他

Spring Security中加密的用法:

使用bcrypt bean

applicationContext-shiro.xml中配置:

    <bean id="secureRandom" class="java.security.SecureRandom"/>
    <bean id="bCryptPasswordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
        <constructor-arg name="version" value="$2A" /> <!-- salt随机生成版本 默认$2A-->
        <constructor-arg name="strength" value="10"/> <!-- 使用salt进行加密迭代次数,默认10-->
        <constructor-arg name="random" ref="secureRandom"/> <!-- 随机算法 -->
    </bean>

    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <security:user name="user" password="$2a$10$LCe6jsoHUrEvWI1KURrqbu/xfuPU5aZj2RkPTVS0d7MUJiT55Lt/y"
                                   authorities="ROLE_USER"/>
                <security:user name="admin" password="$2a$10$BR3Np37NbmtWHqpSZE6AMeCMG4Rm.UOUEZ3dYrW3oUXHNuSBXjDwi"
                               authorities="ROLE_USER, ROLE_ADMIN"/>
            </security:user-service>
            <security:password-encoder ref="bCryptPasswordEncoder"/>
        </security:authentication-provider>
    </security:authentication-manager>

说明:

1)需要配置 bCryptPasswordEncoder的bean,在该bean配置时,可以指定其构造函数相关参数:

version:salt随机生成版本,默认:采用 BCryptVersion.$2A.getVersion();

strength:使用salt进行加密迭代次数,默认:10;

random:随机算法,默认:new SecureRandom()

2)需要在<authentication-provider>标签下的<password-encoder ref=''/>指定该bean。

密码加密用法:

        // BCrypt加密与验证,内部默认:
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
        System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
        // BCrypt密文解析

        //参数解释
        //1)2a:加密算法版本号。
        //2)10:加密轮次,默认为10,数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入。
        //3)密码加密:前面的内容是盐,后面的内容才是真正的密文。
        //以下方式可以更清晰的看出盐和全文。
        String salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2A.getVersion(), 10, new SecureRandom());
        String result = BCrypt.hashpw("123456", salt);//全文
        System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29
        System.out.println("result:" + result);

在对密码加密时,可以采用上边这3种方法:

1)BCryptPasswordEncoder的实例,直接调用 encode方法,此时version,strlength,random都采用默认值。

2)也可以使用BCrypt来实现,实际上上边BCypt的操作就是BCryptPasswordEncoder#encode内部的方法实现。

3)另外,也可以直接在代码中引入applicaitonContext-security.xml中的md5 bean到代码中 @Resources("bCryptPasswordEncoder") private PasswordEncoder bCryptPasswordEncoder;

使用md5 bean

applicationContext-shiro.xml中配置

    <bean id="md5" class="org.springframework.security.crypto.password.MessageDigestPasswordEncoder">
        <constructor-arg name="algorithm" value="MD5"/>
        <property name="iterations" value="10"/>
    </bean>

    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <security:user name="user" password="{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                               authorities="ROLE_USER"/>
                <security:user name="admin" password="{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                               authorities="ROLE_USER, ROLE_ADMIN"/>
            </security:user-service>
            <security:password-encoder ref="md5"/>
        </security:authentication-provider>
    </security:authentication-manager>

说明:

1)需要配置md5 bean,在配置bean时,必须指定MessageDigestPasswordEncoder的构造函数参数:algorithm:指定算法类型,这里是MD5;

2)另外,md5#iterations参数:迭代次数如果不指定,默认为1,这里指定为10;

2)需要在<authentication-provider>标签下的<password-encoder ref=''/>指定该bean。

密码加密用法:

        MessageDigestPasswordEncoder md5 = new MessageDigestPasswordEncoder("MD5");
        md5.setIterations(10);
        md5Password = "{MD5}" + md5.encode("password");
        System.out.println("MD5密码:" + md5Password);
        System.out.println("MD5密码对比:" + passwordEncoder.matches("password", md5Password));

在对密码加密时,可以采用上边方法:

1)MessageDigestPasswordEncoder的实例,可以设置其迭代次数。

2)另外,也可以直接在代码中引入applicaitonContext-security.xml中的md5 bean到代码中 @Resources("md5") private PasswordEncoder md5;

缺省password-encoder(DelegatingPasswordEncoder)

当缺省<security:password-encoder ref="xxx"/>时,Spring Security会使用系统内置的DelegatingPasswordEncoder,自动动适配 PasswordEncoder。

applicationContext-shiro.xml中配置:

    <security:authentication-manager>
        <security:authentication-provider>
            <security:user-service>
                <!-- noop NoOpPasswordEncoder.getInstance()-->
                <security:user name="user" password="{noop}userpwd" authorities="ROLE_USER"/>
                <security:user name="admin" password="{noop}adminpwd" authorities="ROLE_USER, ROLE_ADMIN"/>
                  <!-- bcrypt new BCryptPasswordEncoder() -->
                <security:user name="user1" password="{bcrypt}$2a$10$LCe6jsoHUrEvWI1KURrqbu/xfuPU5aZj2RkPTVS0d7MUJiT55Lt/y"
                               authorities="ROLE_USER"/>
                <security:user name="admin1" password="{bcrypt}$2a$10$BR3Np37NbmtWHqpSZE6AMeCMG4Rm.UOUEZ3dYrW3oUXHNuSBXjDwi"
                               authorities="ROLE_USER, ROLE_ADMIN"/>
                <!-- MD5 new MessageDigestPasswordEncoder("MD5") -->
                <security:user name="user2" password="{MD5}{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                               authorities="ROLE_USER"/>
                <security:user name="admin2" password="{MD5}{sBNW6rB991DqeGbH6ikVJcTe6XwPoHtPW/iyWkwbrF4=}38dee1075a2eaa458bc3fb7e7a945ef8"
                               authorities="ROLE_USER, ROLE_ADMIN"/>
            </security:user-service>
            <security:password-encoder ref="md5"/>
        </security:authentication-provider>
    </security:authentication-manager>

1)如果在<security:authentication-provider>下指定了<security:password-encoder ref="xxx"/>就不需要在<security:user name="xxx" password="yyy"authorities="zzz"/>中的 password 前边加上加密类型({noop}{bcrypt}{MD5}等),否则会导致密码验证失败;
2)如果在<security:authentication-provider>下未指定<security:password-encoder ref="xxx"/>就必须要在<security:user name="xxx" password="yyy" authorities="zzz"/>中的 password 前边加上加密类型({noop}、{bcrypt}、{MD5}等),否则会导致密码验证失败。因为此时验证密码是否成功,会调用org.springframework.security.crypto.password.DelegatingPasswordEncoder.java中的#encode方法、#matches方法,而DelegatingPasswordEncoder中查找密码加密对应的PasswordEncoder时,会根据密码前缀的加密类型查找:如果查找失败,会导致查找不到delegate,也就是delegate为null。

密码加密、解密代码示例:

        PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

        // 此时encode内部使用的就是 BCryptPasswordEncoder
        String encode = passwordEncoder.encode("password");
        System.out.println("bcrypt密码对比:" + passwordEncoder.matches("password", encode));

        // 不带salt,迭代
        String md5NoSaltPassword = "{MD5}" + DigestUtils.md5DigestAsHex("password".getBytes());
        System.out.println("MD5(不含salt、iterations)密码对比:" + passwordEncoder.matches("password", md5NoSaltPassword));

        // 待salt,迭代
        MessageDigestPasswordEncoder md5SaltIterationsPassword = new MessageDigestPasswordEncoder("MD5");
        md5SaltIterationsPassword.setIterations(1);
        String md5Password = "{MD5}" + md5SaltIterationsPassword.encode("password");
        System.out.println("MD5(包含salt、iterations)密码对比:" + passwordEncoder.matches("password", md5Password));

        String noopPassword = "{noop}password";
        System.out.println("noop密码对比:" + passwordEncoder.matches("password", noopPassword));

输出结果:

bcrypt密码对比:true
MD5(不含salt、iterations)密码对比:true
MD5(包含salt、iterations)密码对比:true
noop密码对比:true

DelegatingPasswordEncoder类讲解

构造函数初始化

DelegatingPasswordEncoder本身就是继承了 PasswordEncoder 类,因此也可以在applicationContext-shiro.xml中定义为bean,在<security:authentication-provider>下指定<security:password-encoder ref="xxx"/>的 ref 为该bean。

但是,实际上这么做是没有意义,因为在<security:authentication-provider>下不指定<security:password-encoder ref="xxx"/>时,系统会缺省的采用DelegatingPasswordEncoder作为PasswordEncoder的实现。

    public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
        if (idForEncode == null) {
            throw new IllegalArgumentException("idForEncode cannot be null");
        } else if (!idToPasswordEncoder.containsKey(idForEncode)) {
            throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
        } else {
            Iterator var3 = idToPasswordEncoder.keySet().iterator();

            while(var3.hasNext()) {
                String id = (String)var3.next();
                if (id != null) {
                    if (id.contains("{")) {
                        throw new IllegalArgumentException("id " + id + " cannot contain " + "{");
                    }

                    if (id.contains("}")) {
                        throw new IllegalArgumentException("id " + id + " cannot contain " + "}");
                    }
                }
            }

            this.idForEncode = idForEncode;
            this.passwordEncoderForEncode = (PasswordEncoder)idToPasswordEncoder.get(idForEncode);
            this.idToPasswordEncoder = new HashMap(idToPasswordEncoder);
        }
    }

说明:

1)调用DelegatingPasswordEncoder#constructor()类是PasswordEncoderFactories.java

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

    private PasswordEncoderFactories() {
    }
}

上边代码是spring security系统中唯一用来初始化DelegatingPasswordEncoder的地方。

  • 1)在Spring Security系统将AuthenticationProvider的bean初始化到Spring容器时会调用PasswordEncoderFactories#createDelegatingPasswordEncoder()方法初始化DelegatingPasswordEncoder;
  • 2)这个过程也就是给AutheticationProvider#passwordEncoder赋值的触发点;
  • 3)当然如果在<security:authentication-provider>下指定<security:password-encoder ref="xxx"/>的 ref 不为DelegatingPasswordEncoder时,也将不会调用PasswordEncoderFactories#createDelegatingPasswordEncoder()方法

2)idToPasswordEncoder属性:DelegatingPasswordEncoder是一个能适配多种PasswordEncoder的委托类,其内部定义了一个Map<String,PasswordEncoder>集合:

key为:PasswordEncoder的别名;
value为:PasswordEncoder的具体实现类。

    private final Map<String, PasswordEncoder> idToPasswordEncoder;

idToPasswordEncoder用来托管PassswordEncoder的实现,这个类是在DelegatingPasswordEncoder#constructor中被传递初始化的。

3)idForEncode属性通过PasswordEncoderFactories#createDelegatingPasswordEncoder()中初始化DelegatingPasswordEncoder的代码,可以知道idForEncode的值是“bcrypt”;

4)passwordEncoderForEncode属性:就是BCryptPasswordEncoder对象。

encode加密

    public String encode(CharSequence rawPassword) {
        return "{" + this.idForEncode + "}" + this.passwordEncoderForEncode.encode(rawPassword);
    }

说明:

1)通过上边DelegatingPasswordEncoder#constructor()代码可以知道:passwordEncoderForEncode属性就是BCryptPasswordEncoder对象

2)DelegatingPasswordEncoder#encode()方法:实际上就是"bcrypt"加密算法。这点十分重要,往往也是其特殊之处,需要使用者牢记。

3)rawPassword参数:待加密密码明文。

matches匹配密码

    public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
        if (rawPassword == null && prefixEncodedPassword == null) {
            return true;
        } else {
            // 根据密文前缀查找 delegate
            String id = this.extractId(prefixEncodedPassword);
            PasswordEncoder delegate = (PasswordEncoder)this.idToPasswordEncoder.get(id);
            if (delegate == null) { // delegate查找失败
                return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
            } else {
                String encodedPassword = this.extractEncodedPassword(prefixEncodedPassword);
                return delegate.matches(rawPassword, encodedPassword);
            }
        }
    }

说明:

1)rawPassword参数:密码明文;

2)prefixEncodedPassword参数:带有加密类型的密码密文,必须带有使用的PasswordEncoder类型(PasswordEncoderFactories#createDelegatingPasswordEncoder()中map#key);

格式举例:

{noop}password
{bcypt}$2a$10$IK/02aEUVRBaeoQsvN.VluPLqNKZ2ZwwTRmAAWXmlnCU5DAjmjtRC
{MD5}5f4dcc3b5aa765d61d8327deb882cf99
{MD5}{L5M7tjEyGdBtyFCyk0pBXOLLFi3AOMEBZqdRDTAwV6c=}c05b48c699659f56462bbed387485cc6

3)当没有指定密码加密类型({bcypt}等)时,会抛出异常:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:250)
    org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:198)
    org.springframework.security.authentication.dao.DaoAuthenticationProvider.additionalAuthenticationChecks(DaoAuthenticationProvider.java:90)
    org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider.authenticate(AbstractUserDetailsAuthenticationProvider.java:166)
    org.springframework.security.authentication.ProviderManager.authenticate(ProviderManager.java:175)
    。。。

BCryptPasswordEncoder类讲解

属性:

    private Pattern BCRYPT_PATTERN;
    private final Log logger;
    private final int strength;
    private final BCryptPasswordEncoder.BCryptVersion version;
    private final SecureRandom random;

说明:

1)BCRYPT_PATTERN:bcrypt密文格式验证正则表达式;

2)logger:日志操作类;

3)strlength:生成salt迭代次数;

4)version:生成salt采用的版本;

5)random:随机生成slat实现。

构造函数:

    public BCryptPasswordEncoder() {
        this(-1);
    }

    public BCryptPasswordEncoder(int strength) {
        this(strength, (SecureRandom)null);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version) {
        this(version, (SecureRandom)null);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, SecureRandom random) {
        this(version, -1, random);
    }

    public BCryptPasswordEncoder(int strength, SecureRandom random) {
        this(BCryptPasswordEncoder.BCryptVersion.$2A, strength, random);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength) {
        this(version, strength, (SecureRandom)null);
    }

    public BCryptPasswordEncoder(BCryptPasswordEncoder.BCryptVersion version, int strength, SecureRandom random) {
        this.BCRYPT_PATTERN = Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}");
        this.logger = LogFactory.getLog(this.getClass());
        if (strength == -1 || strength >= 4 && strength <= 31) {
            this.version = version;
            this.strength = strength == -1 ? 10 : strength;
            this.random = random;
        } else {
            throw new IllegalArgumentException("Bad strength");
        }
    }

构造函数重构的比较多,在DelegatingPasswordEncoder中使用的就是第一个构造函数,此时属性会赋值默认值:

1)BCRYPT_PATTERN:bcrypt密文格式验证正则表达式,默认值:Pattern.compile("\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}")

2)strlength:生成salt迭代次数,默认值:10;

3)version:生成salt采用的版本,默认值:BCryptPasswordEncoder.BCryptVersion.$2A

4)random:随机生成slat实现,默认值:空。

encode方法:

    public String encode(CharSequence rawPassword) {
        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);
    }

BCryptPasswordEncoder实际内部是使用 BCrypt 实现;

matches方法:

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
                this.logger.warn("Encoded password does not look like BCrypt");
                return false;
            } else {
                return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
            }
        } else {
            this.logger.warn("Empty encoded password");
            return false;
        }
    }

从BCRYPT_PATTERN的值"\\A\\$2(a|y|b)?\\$(\\d\\d)\\$[./0-9A-Za-z]{53}",可以发现另外一些密文分为3部分:

  • 第一部分:以$2a、$2y、$2b开头;
  • 第二部分:以$数字
  • 第三部分:以$开头后边附加.、/、数字、大写字母、小写字母组成的,且长度为53的字符串。

测试代码:

    @Test
    public void testPwdEncoder() {
        // 
        // BCrypt加密与验证
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
        System.out.println("passwordEncoder 123456:" + passwordEncoder.encode("123456"));
        // BCrypt密文解析

        //在密文中包含3段内容,$是分隔符。
        //1)2a:加密算法版本号。
        //2)10:加密轮次,默认为10,数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入。
        //3)第3个$之后:前面的内容是盐,后面的内容才是真正的密文。
        //以下方式可以更清晰的看出盐和全文。
        String salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2A.getVersion(), 10, new SecureRandom());
        String result = BCrypt.hashpw("123456", salt);//全文
        System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29
        System.out.println("result:" + result);

        salt = BCrypt.gensalt(BCryptPasswordEncoder.BCryptVersion.$2B.getVersion(), 11, new SecureRandom());
        result = BCrypt.hashpw("123456", salt);//全文
        System.out.println("salt:" + salt + ",salt's length:" + salt.length()); // salt长度是29
        System.out.println("result:" + result);
    }

打印:

passwordEncoder 123456:$2a$10$XSpVd/lavtejOXHeDGNMOe1zxgblnsXWoTi0DFD/vN4Z6EjH1r97q
passwordEncoder 123456:$2a$10$I9zV9AbsEdi36s7ovTQ2hOhUczFP5CXybnyJv9aNY6Ae6qky9oouu
salt:$2a$10$yg5TNGzmyNe0di70exM.vO,salt's length:29
result:$2a$10$yg5TNGzmyNe0di70exM.vOWLx.lMiniZ/BOCoecIc5tF/Q0CvYUJa
salt:$2b$11$ncuDpd17nju3d6auOrQAr.,salt's length:29
result:$2b$11$ncuDpd17nju3d6auOrQAr.BZqNeyyVgqhb3gncQyRUvuKHzA2.FOS

从上边代码测试会发现,BCryptPasswordEncoder 实际内部是使用 BCrypt 实现,另外从测试可以发现使用SpringSecurity缺省password encoder生成密文有以下规则:

1)在密文中包含3段内容,
2)2a:salt生成算法版本号;
3)10:salt迭代次数,默认为10(取值范围是:[4,31]),数值越大,加密时间和越难破解呈指数增长。可在BCryptPasswordEncoder构造参数传入;
4)第3个$之后:前面的内容是盐,后面的内容才是真正的密文;
5)随机生成salt,且salt的长度为29;

 

posted @ 2020-01-17 00:10  cctext  阅读(11012)  评论(0编辑  收藏  举报