Spring Security PasswordEncoder 密碼校驗(yàn)和密碼加密流程

Spring Security PasswordEncoder 密碼校驗(yàn)和密碼加密流程

PasswordEncoder 使用

首先我們先來看看一個(gè)創(chuàng)建密碼編碼器工廠方法

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
上面靜態(tài)工廠方法可以看出赞厕,默認(rèn)是創(chuàng)建并返回一個(gè) BCryptPasswordEncoder艳狐,同時(shí)該 BCryptPasswordEncoder( PasswordEncoder 子類)也是 Spring Security 推薦的默認(rèn)密碼編碼器,其中 noop 就是不做處理默認(rèn)保存原密碼皿桑。

一般我們代碼中 @Autowired 注入并使用 PasswordEncoder 接口的實(shí)例毫目,然后調(diào)用其 matches 方法去匹配原密碼和數(shù)據(jù)庫(kù)中保存的“密碼”蔬啡;密碼的校驗(yàn)方式有多種,從 PasswordEncoder 接口實(shí)現(xiàn)的類是可以知道镀虐。

業(yè)務(wù)代碼中注入 PasswordEncoder

@Autowired
private PasswordEncoder passwordEncoder;

科普一些基本知識(shí)

BCrypt 密碼散列函數(shù)的概念介紹可以看一下維基百科或百度百科的內(nèi)容 (因?yàn)橹皇鞘褂孟潴。瑫簳r(shí)不需要對(duì)算法的了解和實(shí)現(xiàn))

維基百科:BCrypt

百度百科:BCrypt

知識(shí)混淆點(diǎn)

加密/解密 與 Hash 這兩個(gè)概念不能混淆,比如:SHA 系列是 Hash 算法刮便,不是加密算法顽腾,加密意味著可以解密,但是 Hash 是不可逆的(無法通過 Hash 值還原得到密碼诺核,只能比對(duì) Hash 值看看是否相等)抄肖。

安全性問題

目前很大一部分存在安全問題的系統(tǒng)一般僅僅使用密碼的 MD5 值進(jìn)行保存,可以通過 MD5 查詢庫(kù)去匹配對(duì)大部分的密碼(可以直接從彩虹表里反推出來)窖杀,而且 MD5 計(jì)算 Hash 值碰撞容易構(gòu)造漓摩,安全性大大降低。MD5 加鹽在本地計(jì)算速度也是很快入客,也是密碼短也是極其容易破解管毙;更好的選擇是 SHA-256、BCrypt 等等等

密碼匹配流程的源碼解釋

本文簡(jiǎn)單說一下 BCryptPasswordEncoder 密碼匹配的一個(gè)簡(jiǎn)單流程或者過程桌硫。

重點(diǎn)

如果是使用 BCryptPasswordEncoder 調(diào)用 encode() 方法編碼輸入密碼的話夭咬,其實(shí)這個(gè)編碼后的“密碼”并不是我們平時(shí)輸入的真正密碼,而是密碼加鹽后的通過單向 Hash 算法(BCrypt)得到值铆隘。

這里面細(xì)心的同學(xué)可能會(huì)發(fā)現(xiàn)一些問題:

  • 同一個(gè)密碼計(jì)算 Hash 不應(yīng)該是一樣的嗎卓舵?每次使用 BCryptPasswordEncoder 編碼同一個(gè)密碼都是不一樣的?

  • BCryptPasswordEncoder 編碼同一個(gè)密碼后結(jié)果都不一樣膀钠,怎么進(jìn)行匹配掏湾?

下面通過源碼簡(jiǎn)單說一下這個(gè)匹配的流程:
matches(CharSequence rawPassword, String encodedPassword) 方法根據(jù)兩個(gè)參數(shù)都可以知道

  • 第一個(gè)參數(shù)是原密碼
  • 第二個(gè)參數(shù)就是用 PasswordEncoder 調(diào)用 encode(CharSequence rawPassword) 編碼過后保存在數(shù)據(jù)庫(kù)的密碼。

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

上述代碼解讀:首先判斷是否數(shù)據(jù)庫(kù)保存的“密碼”(后面簡(jiǎn)稱:“密碼”)是否為空或者 null 肿嘲,在通過正則表達(dá)式匹配“密碼”是否符合格式融击,最后通過 BCryptcheckpw(String plaintext, String hashed) 方法進(jìn)行密碼匹配


再詳細(xì)看看 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));
}

第二個(gè)參數(shù) hashed 表明其實(shí)數(shù)據(jù)庫(kù)查詢出來的“密碼”也就是 Hash 值;equalsNoEarlyReturn(hashed, hashpw(plaintext, hashed)) 代碼中通過調(diào)用 hashpw 計(jì)算輸入密碼的 Hash 值(參數(shù)分別是輸入的密碼和保存在數(shù)據(jù)庫(kù)的“密碼”)


再繼續(xù)看 hashpw 里面的部分代碼(內(nèi)容過長(zhǎng)雳窟,省略部分代碼尊浪,看看代碼中的中文注釋):

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));
    
    // 關(guān)鍵點(diǎn):上面***一大堆就是校驗(yàn)是否符合相應(yīng)格式,然后下面這行就是取出密碼的鹽封救,real_salt就是 Hash 計(jì)算前的密碼鹽(關(guān)于鹽的介紹: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' ? "\000" : "")).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();
}

其實(shí)上面代碼就是從數(shù)據(jù)庫(kù)得到的“密碼”(參數(shù): salt )進(jìn)行一系列校驗(yàn)(長(zhǎng)度校驗(yàn)等)并截取“密碼”中相應(yīng)的密碼鹽拇涤,利用這個(gè)密碼鹽進(jìn)行同樣的一系列計(jì)算 Hash 操作和 Base64 編碼拼接一些標(biāo)識(shí)符 生成所謂的“密碼”,最后 equalsNoEarlyReturn 方法對(duì)同一個(gè)密碼鹽生成的兩個(gè)“密碼”進(jìn)行匹配兴泥。

上述大致就是密碼匹配流程了工育,對(duì)于問題“ BCryptPasswordEncoder 編碼同一個(gè)密碼后結(jié)果都不一樣,怎么進(jìn)行匹配”的簡(jiǎn)單解答:

因?yàn)槊艽a鹽是隨機(jī)生成的搓彻,但是可以根據(jù)數(shù)據(jù)庫(kù)查詢出來的“密碼”拿到密碼鹽如绸,同一個(gè)密碼鹽+原密碼計(jì)算 Hash 結(jié)果值是能匹配的嘱朽。

密碼“加密”保存源碼解釋

看看加密的一個(gè)過程,

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

public String encode(CharSequence rawPassword) {
    String salt;
    if (strength > 0) {
        if (random != null) {
            // 生成隨機(jī)密碼鹽
            salt = BCrypt.gensalt(strength, random);
        }
        else {
            // 生成隨機(jī)密碼鹽
            salt = BCrypt.gensalt(strength);
        }
    }
    else {
        // 生成隨機(jī)密碼鹽
        salt = BCrypt.gensalt();
    }
    return BCrypt.hashpw(rawPassword.toString(), salt);
}

encode 方法傳入是原密碼怔接,其中 int strength, SecureRandom random 這兩個(gè)構(gòu)造參數(shù)是 BCryptPasswordEncoder(int strength, SecureRandom random) 構(gòu)造方法按需傳入搪泳,如果不指定strength和random,默認(rèn)執(zhí)行 BCrypt.gensalt() 這行代碼生成也相應(yīng)密碼隨機(jī)鹽扼脐。


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

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

public static String gensalt(int log_rounds, SecureRandom random) {
    // 一些檢驗(yàn)
    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];

    // 生成隨機(jī)字節(jié)并將其置于rnd字節(jié)數(shù)組
    random.nextBytes(rnd);

    rs.append("$2a$");
    if (log_rounds < 10) {
        // 不夠長(zhǎng)度補(bǔ)夠
        rs.append("0");
    }
    // 拼接字符串得到相應(yīng)的格式
    rs.append(log_rounds);
    rs.append("$");
    encode_base64(rnd, rnd.length, rs);
    return rs.toString();
}

最終上面的 gensalt 方法得到一個(gè) 隨機(jī)密碼鹽+無用字符串(這個(gè)字符串可以理解為你輸入的密碼) 計(jì)算 Hash 操作和 Base64 編碼拼接一些標(biāo)識(shí)符 生成假“密碼”(這個(gè)假“密碼”為了兼容方便調(diào)用 hashpw 方法)岸军,最后關(guān)鍵點(diǎn)就是調(diào)用 BCrypt.hashpw 方法取到密碼鹽生成相應(yīng)的真實(shí)“密碼”(這個(gè)得到的密碼可以用于保存在數(shù)據(jù)庫(kù)中了)。

對(duì)于問題“同一個(gè)密碼計(jì)算 Hash 不應(yīng)該是一樣的嗎瓦侮?每次使用 BCryptPasswordEncoder 編碼同一個(gè)密碼都是不一樣的艰赞?”的簡(jiǎn)單解答:

因?yàn)橛玫降碾S機(jī)密碼鹽每次都是不一樣的,同一個(gè)密碼和不同的密碼鹽組合計(jì)算出來的 Hash 值肯定不一樣啦肚吏,所以編碼同一個(gè)密碼得到的結(jié)果都是不一樣方妖。

建議和想法

本文主要講解一些安全性防護(hù)的思想,學(xué)習(xí)的過程思想很重要罚攀。

登錄注冊(cè)是每個(gè)系統(tǒng)都具備的功能党觅,開發(fā)的同學(xué)記住一定不能保存明文密碼,否則被脫庫(kù)就會(huì)造成嚴(yán)重的后果斋泄。如果是通過上述的方法進(jìn)行密碼保存杯瞻,即便拿到“密碼”也非常難還原密碼。

上述在密碼編碼的過程中的思想還是需要掌握:

  1. 只是保存散列碼是不安全的炫掐,但是我們可以為密碼加鹽再通過一些 Hash 值 低概率碰撞且計(jì)算速度慢 的散列算法計(jì)算 Hash 值保存魁莉。

  2. Spring Security 每次 Hash 之前用的鹽都是隨機(jī),鹽可以保存在最終生成的“密碼”中卒废,這樣每個(gè)密碼都是用了相應(yīng)不同的隨機(jī)鹽+原密碼計(jì)算 Hash 值得到沛厨,暴力破解難度也變大了宙地。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末摔认,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子宅粥,更是在濱河造成了極大的恐慌参袱,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件秽梅,死亡現(xiàn)場(chǎng)離奇詭異抹蚀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)企垦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門环壤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人钞诡,你說我怎么就攤上這事郑现∨缺溃” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵接箫,是天一觀的道長(zhǎng)攒读。 經(jīng)常有香客問我,道長(zhǎng)辛友,這世上最難降的妖魔是什么薄扁? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮废累,結(jié)果婚禮上邓梅,老公的妹妹穿的比我還像新娘。我一直安慰自己邑滨,他們只是感情好震放,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著驼修,像睡著了一般殿遂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上乙各,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天墨礁,我揣著相機(jī)與錄音,去河邊找鬼耳峦。 笑死恩静,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的蹲坷。 我是一名探鬼主播驶乾,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼循签!你這毒婦竟也來了级乐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤县匠,失蹤者是張志新(化名)和其女友劉穎风科,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乞旦,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贼穆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了兰粉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片故痊。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖玖姑,靈堂內(nèi)的尸體忽然破棺而出愕秫,到底是詐尸還是另有隱情浊仆,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布豫领,位于F島的核電站抡柿,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏等恐。R本人自食惡果不足惜洲劣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望课蔬。 院中可真熱鬧囱稽,春花似錦、人聲如沸二跋。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扎即。三九已至吞获,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谚鄙,已是汗流浹背各拷。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留闷营,地道東北人烤黍。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像傻盟,于是被迫代替她去往敵國(guó)和親速蕊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容