中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

Spring Security 5.0的DelegatingPasswordEncoder詳解

2018-10-31    來源:importnew

容器云強(qiáng)勢(shì)上線!快速搭建集群,上萬Linux鏡像隨意使用

本文參考自Spring Security 5.0.4.RELEASE 的官方文檔,結(jié)合源碼介紹了 DelegatingPasswordEncoder,對(duì)其工作過程進(jìn)行分析并解決其中遇到的問題。包括 There is no PasswordEncoder mapped for the id “null” 非法參數(shù)異常的正確處理方法。

PasswordEncoder

首先要理解 DelegatingPasswordEncoder 的作用和存在意義,明白官方為什么要使用它來取代原先的 NoOpPasswordEncoder

DelegatingPasswordEncoderNoOpPasswordEncoder 都是 PasswordEncoder 接口的實(shí)現(xiàn)類。根據(jù)官方的定義,Spring Security 的 PasswordEncoder 接口用于執(zhí)行密碼的單向轉(zhuǎn)換,以便安全地存儲(chǔ)密碼。

關(guān)于密碼存儲(chǔ)的演變歷史這里我不多做介紹,簡(jiǎn)單來說就是現(xiàn)在數(shù)據(jù)庫存儲(chǔ)的密碼基本都是經(jīng)過編碼的,而決定如何編碼以及判斷未編碼的字符序列和編碼后的字符串是否匹配就是 PassswordEncoder 的責(zé)任。

這里我們可以看一下 PasswordEncoder 接口的源碼:

public interface PasswordEncoder {

    /**
     * Encode the raw password. Generally, a good encoding algorithm applies a SHA-1 or
     * greater hash combined with an 8-byte or greater randomly generated salt.
     */
    String encode(CharSequence rawPassword);

    /**
     * Verify the encoded password obtained from storage matches the submitted raw
     * password after it too is encoded. Returns true if the passwords match, false if
     * they do not. The stored password itself is never decoded.
     *
     * @param rawPassword the raw password to encode and match
     * @param encodedPassword the encoded password from storage to compare with
     * @return true if the raw password, after encoding, matches the encoded password from
     * storage
     */
    boolean matches(CharSequence rawPassword, String encodedPassword);

}

根據(jù)源碼,我們可以直觀地看到 PassswordEncoder 接口只有兩個(gè)方法,一個(gè)是 String encode(CharSequence rawPassword),用于將字符序列(即原密碼)進(jìn)行編碼;另一個(gè)方法是 boolean matches(CharSequence rawPassword, String encodedPassword),用于比較字符序列和編碼后的密碼是否匹配。

理解了 PasswordEncoder 的作用后我們來 Spring Security 5.0 之前默認(rèn) PasswordEncoder 實(shí)現(xiàn)類 NoOpPasswordEncoder。這個(gè)類因?yàn)椴话踩呀?jīng)被標(biāo)記為過時(shí)了。下面就讓我們來看看它是如何地不安全的:

1 NoOpPasswordEncoder

事實(shí)上,NoOpPasswordEncoder 就是沒有編碼的編碼器,源碼如下:

@Deprecated
public final class NoOpPasswordEncoder implements PasswordEncoder {

    public String encode(CharSequence rawPassword) {
        return rawPassword.toString();
    }

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return rawPassword.toString().equals(encodedPassword);
    }

    /**
     * Get the singleton {@link NoOpPasswordEncoder}.
     */
    public static PasswordEncoder getInstance() {
        return INSTANCE;
    }

    private static final PasswordEncoder INSTANCE = new NoOpPasswordEncoder();

    private NoOpPasswordEncoder() {
    }

}

可以看到,NoOpPasswordEncoderencode 方法就只是簡(jiǎn)單地把字符序列轉(zhuǎn)成字符串。也就是說,你輸入的密碼 ”123456” 存儲(chǔ)在數(shù)據(jù)庫里仍然是 ”123456”,這樣如果數(shù)據(jù)庫被攻破的話密碼就直接泄露了,十分不安全。而且 NoOpPasswordEncoder 也就失去了所謂密碼編碼器的意義了。

不過正因其十分簡(jiǎn)單,在 Spring Security 5.0 之前 NoOpPasswordEncoder 是作為默認(rèn)的密碼編碼器而存在到,它可以是你沒有主動(dòng)加密時(shí)的一個(gè)默認(rèn)選擇。

另外,NoOpPasswordEncoder 的實(shí)現(xiàn)是一個(gè)標(biāo)準(zhǔn)的餓漢單例模式,關(guān)于單例模式可以看這一篇文章:?jiǎn)卫J郊捌?種推薦寫法和3類保護(hù)手段。

2 DelegatingPasswordEncoder

通過上面的學(xué)習(xí)我們可以知道,隨著安全要求的提高之前的默認(rèn)密碼編碼器 NoOpPasswordEncoder 已經(jīng)被 “不推薦”了,那我們有理由推測(cè)現(xiàn)在的默認(rèn)密碼編碼器換成了使用某一特定算法的編碼器?墒沁@樣便會(huì)帶來三個(gè)問題:

  • 有許多使用舊密碼編碼的應(yīng)用程序無法輕松遷移;
  • 密碼存儲(chǔ)的最佳做法(算法)可能會(huì)再次發(fā)生變化;
  • 作為一個(gè)框架,Spring Security 不能經(jīng)常發(fā)生突變。

簡(jiǎn)單來說,就是新的密碼編碼器和舊密碼的兼容性、自身的穩(wěn)健性以及需要一定的可變性(切換到更好的算法)。聽起來是不是十分矛盾?那我們就來看看 DelegatingPasswordEncoder 是怎么解決這個(gè)問題的。在看解決方法之前先看使用 DelegatingPasswordEncoder 能達(dá)到的效果:

1 構(gòu)造方法

下面我們來看看 DelegatingPasswordEncoder 的構(gòu)造方法

public DelegatingPasswordEncoder(String idForEncode,
    Map<String, PasswordEncoder> idToPasswordEncoder) {
    if(idForEncode == null) {
        throw new IllegalArgumentException("idForEncode cannot be null");
    }
    if(!idToPasswordEncoder.containsKey(idForEncode)) {
        throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
    }
    for(String id : idToPasswordEncoder.keySet()) {
        if(id == null) {
            continue;
        }
        if(id.contains(PREFIX)) {
            throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
        }
        if(id.contains(SUFFIX)) {
            throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
        }
    }
    this.idForEncode = idForEncode;
    this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
    this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
}

idForEncode 決定密碼編碼器的類型,idToPasswordEncoder 決定判斷匹配時(shí)兼容的類型,而且 idToPasswordEncoder 必須包含 idForEncode (不然加密后就無法匹配了)。

圍繞這個(gè)構(gòu)造方法通常有如下兩種創(chuàng)建思路:

工廠構(gòu)造

首先是工廠構(gòu)造。

PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();

其具體實(shí)現(xiàn)如下:

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

    return new DelegatingPasswordEncoder(encodingId, encoders);
}

這個(gè)可以簡(jiǎn)單地理解為,遇到新密碼 DelegatingPasswordEncoder 會(huì)委托給 BCryptPasswordEncoder(encodingId為bcryp*) 進(jìn)行加密。同時(shí),對(duì)歷史上使用 ldap、MD4、MD5 等等加密算法的密碼認(rèn)證保持兼容(如果數(shù)據(jù)庫里的密碼使用的是MD5算法,那使用matches方法認(rèn)證仍可以通過,但新密碼會(huì)使bcrypt進(jìn)行儲(chǔ)存)。十分神奇,原理后面會(huì)講。

定制構(gòu)造

接下來是定制構(gòu)造,其實(shí)和工廠方法是一樣的,一般情況下推薦直接使用工廠方法。這里給一個(gè)小例子:

String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());

PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);

2 密碼存儲(chǔ)格式

密碼的標(biāo)準(zhǔn)存儲(chǔ)格式是:

{id}encodedPassword

其中,id 標(biāo)識(shí)使用 PaswordEncoder 的種類,encodedPassword?是原密碼被編碼后的密碼。

注意:
rawPassword、encodedPassword、 密碼存儲(chǔ)格式 (prefixEncodedPassword)這三者是不同的概念!
rawPassword 相當(dāng)于字符序列”123456” ;
encodedPassword 是使用 id 為 “mycrypt” 對(duì)應(yīng)的密碼編碼器 “123456” 編碼后的字符串,假設(shè)為”qwertyuiop” ;
存儲(chǔ)的密碼 prefixEncodedPassword 是在數(shù)據(jù)庫中,我們所能見到的形式,如“{mycrypt}qwertyuiop” ;
這個(gè)概念在后面講matches方法的源碼時(shí)會(huì)用到,請(qǐng)留意。

例如 rawPasswordpassword 在使用不同編碼算法的情況下在數(shù)據(jù)庫的存儲(chǔ)如下:

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password 
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc 
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc=  
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0

這里需要指明:
密碼的可靠性并不依賴于加密算法的保密。即密碼的可靠在于,就算你知道我使用的是什么算法你也無法還原出原密碼(當(dāng)然,對(duì)于本身就可逆的編碼算法來說就不是這樣了,但這樣的算法我們通常不會(huì)認(rèn)為是可靠的)。而且,即使沒有標(biāo)明使用的是什么算法,攻擊者也很容易根據(jù)一些規(guī)律從編碼后的密碼字符串中推測(cè)出編碼算法,如 bcrypt 算法通常是以 $2a$ 開頭。

3 密碼編碼與匹配

從上文可知,idForEncode 這個(gè)構(gòu)造參數(shù)決定使用哪個(gè)PasswordEncoder進(jìn)行密碼的編碼。編碼的方法如下:

private static final String PREFIX = "{";
private static final String SUFFIX = "}";

@Override
public String encode(CharSequence rawPassword) {
    return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}

所以用上文構(gòu)造的 DelegatingPasswordEncoder 默認(rèn)使用 BCryptPasswordEncoder,結(jié)果格式如下:

{bcrypt}2a10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

密碼編碼方法比較簡(jiǎn)單,重點(diǎn)在于匹配.匹配方法源碼如下:

@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
    if(rawPassword == null && prefixEncodedPassword == null) {
        return true;
    }
    //取出編碼算法的id
    String id = extractId(prefixEncodedPassword);
    //根據(jù)編碼算法的id從支持的密碼編碼器Map(構(gòu)造時(shí)傳入)中取出對(duì)應(yīng)編碼器
    PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
    if(delegate == null) {
    //如果找不到對(duì)應(yīng)的密碼編碼器則使用默認(rèn)密碼編碼器進(jìn)行匹配判斷,此時(shí)比較的密碼字符串是 prefixEncodedPassword
        return this.defaultPasswordEncoderForMatches
            .matches(rawPassword, prefixEncodedPassword);
    }
    //從 prefixEncodedPassword 中提取獲得 encodedPassword 
    String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
    //使用對(duì)應(yīng)編碼器進(jìn)行匹配判斷,此時(shí)比較的密碼字符串是 encodedPassword ,不攜帶編碼算法id頭
    return delegate.matches(rawPassword, encodedPassword);
}

這個(gè)匹配方法其實(shí)也挺好理解的。唯一需要特別注意的就是找不到對(duì)應(yīng)密碼編碼器時(shí)使用的默認(rèn)密碼編碼器,我們來看看 defaultPasswordEncoderForMatches 是什么。

4 defaultPasswordEncoderForMatches 及 id 為 null 異常

DelegatingPasswordEncoder 的源碼里對(duì)應(yīng)內(nèi)容如下:

private static final String PREFIX = "{";
private static final String SUFFIX = "}";
private final String idForEncode;
private final PasswordEncoder passwordEncoderForEncode;
private final Map<String, PasswordEncoder> idToPasswordEncoder;

private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();

public void setDefaultPasswordEncoderForMatches(
    PasswordEncoder defaultPasswordEncoderForMatches) {
    if(defaultPasswordEncoderForMatches == null) {
        throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
    }
    this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
}

private class UnmappedIdPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        throw new UnsupportedOperationException("encode is not supported");
    }

    @Override
    public boolean matches(CharSequence rawPassword,
        String prefixEncodedPassword) {
        String id = extractId(prefixEncodedPassword);
        throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
    }
}

可以看到,DelegatingPasswordEncoder 里面?PREFIX 和?SUFFIX 是常量,idForEncode、passwordEncoderForEncodeidToPasswordEncoder 是在構(gòu)造方法中傳入決定并不可修改的。只有 defaultPasswordEncoderForMatches 是有一個(gè)setDefaultPasswordEncoderForMatches?方法進(jìn)行設(shè)置的可變對(duì)象。

而且它有一個(gè)私有的默認(rèn)實(shí)現(xiàn) UnmappedIdPasswordEncoder,這個(gè)所謂的默認(rèn)實(shí)現(xiàn)的唯一作用就是拋出異常提醒你要自己選擇一個(gè)默認(rèn)密碼編碼器來取代它。通常我們只會(huì)可能用到它的 matches 方法,這個(gè)時(shí)候就會(huì)報(bào)拋出如下異常:

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

5 解決方法

遇到這個(gè)異常,最簡(jiǎn)單的做法就是明確提供一個(gè) PasswordEncoder 對(duì)密碼進(jìn)行編碼。如果是 從Spring Security 5.0 之前遷移而來的,由于之前默認(rèn)使用的是 NoOpPasswordEncoder 并且數(shù)據(jù)庫的密碼保存格式不帶有加密算法 id 頭,會(huì)報(bào) id 為 null 異常,所以應(yīng)該明確提供一個(gè)NoOpPasswordEncoder 密碼編碼器。

這里有兩種思路:其一就是使用 NoOpPasswordEncoder 取代 DelegatingPasswordEncoder 以恢復(fù)到之前版本的狀態(tài)。這也是筆者在其他博客上看得比較多的一種解決方法;另外就是使用 DelegatingPasswordEncoder 的 setDefaultPasswordEncoderForMatches 方法指定默認(rèn)的密碼編碼器為 NoOpPasswordEncoder。這兩種方法孰優(yōu)孰劣自然不言而喻,官方文檔是這么說的:

Reverting to NoOpPasswordEncoder is not considered to be secure. You should instead migrate to using DelegatingPasswordEncoder to support secure password encoding.
恢復(fù)到 NoOpPasswordEncoder 被認(rèn)為是不安全的。您應(yīng)該轉(zhuǎn)而使用 DelegatingPasswordEncoder 支持安全密碼編碼。

當(dāng)然,你也可以將數(shù)據(jù)庫保存的密碼都加上一個(gè) {noop} 前綴。這樣 DelegatingPasswordEncoder 就知道要使用 NoOpPasswordEncoder了。這確實(shí)是一種方法,但沒必要。這里我們來看一下前面的兩種解決方法的實(shí)現(xiàn):

1 使用NoOpPasswordEncoder取代DelegatingPasswordEncoder
@Bean
 public  static NoOpPasswordEncoder passwordEncoder(){
     return NoOpPasswordEncoder.getInstance();
}
2 使用 DelegatingPasswordEncoder 指定 defaultPasswordEncoderForMatches
@Bean
public  static PasswordEncoder passwordEncoder( ){
    DelegatingPasswordEncoder delegatingPasswordEncoder =
            (DelegatingPasswordEncoder) PasswordEncoderFactories.createDelegatingPasswordEncoder();
    //設(shè)置defaultPasswordEncoderForMatches為NoOpPasswordEncoder
    delegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance());
    return  delegatingPasswordEncoder;
}

標(biāo)簽: 安全 數(shù)據(jù)庫

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:安全的SSH設(shè)置

下一篇:Java: 未來已來