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;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM