Spring Security PasswordEncoder 密碼校驗和密碼加密


Spring Security PasswordEncoder 密碼校驗和密碼加密流程

PasswordEncoder 使用

首先我們先來看看一個創建密碼編碼器工廠方法

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
上面靜態工廠方法可以看出,默認是創建並返回一個 BCryptPasswordEncoder,同時該 BCryptPasswordEncoder( PasswordEncoder 子類)也是 Spring Security 推薦的默認密碼編碼器,其中 noop 就是不做處理默認保存原密碼。

一般我們代碼中 @Autowired 注入並使用 PasswordEncoder 接口的實例,然后調用其 matches 方法去匹配原密碼和數據庫中保存的“密碼”;密碼的校驗方式有多種,從 PasswordEncoder 接口實現的類是可以知道。

業務代碼中注入 PasswordEncoder

@Autowired
private PasswordEncoder passwordEncoder;

 

 

知識混淆點

加密/解密 與 Hash 這兩個概念不能混淆,比如:SHA 系列是 Hash 算法,不是加密算法,加密意味着可以解密,但是 Hash 是不可逆的(無法通過 Hash 值還原得到密碼,只能比對 Hash 值看看是否相等)。

安全性問題

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

密碼匹配流程的源碼解釋

本文簡單說一下 BCryptPasswordEncoder 密碼匹配的一個簡單流程或者過程。

重點

如果是使用 BCryptPasswordEncoder 調用 encode() 方法編碼輸入密碼的話,其實這個編碼后的“密碼”並不是我們平時輸入的真正密碼,而是密碼加鹽后的通過單向 Hash 算法(BCrypt)得到值。

這里面細心的同學可能會發現一些問題:

  • 同一個密碼計算 Hash 不應該是一樣的嗎?每次使用 BCryptPasswordEncoder 編碼同一個密碼都是不一樣的?

  • BCryptPasswordEncoder 編碼同一個密碼后結果都不一樣,怎么進行匹配?

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

  • 第一個參數是原密碼
  • 第二個參數就是用 PasswordEncoder 調用 encode(CharSequence rawPassword) 編碼過后保存在數據庫的密碼。

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

 

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


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

 

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


再繼續看 hashpw 里面的部分代碼(內容過長,省略部分代碼,看看代碼中的中文注釋):

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));
    
    // 關鍵點:上面***一大堆就是校驗是否符合相應格式,然后下面這行就是取出密碼的鹽,real_salt就是 Hash 計算前的密碼鹽(關於鹽的介紹: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();
}

 

其實上面代碼就是從數據庫得到的“密碼”(參數: salt )進行一系列校驗(長度校驗等)並截取“密碼”中相應的密碼鹽,利用這個密碼鹽進行同樣的一系列計算 Hash 操作和 Base64 編碼拼接一些標識符 生成所謂的“密碼”,最后 equalsNoEarlyReturn 方法對同一個密碼鹽生成的兩個“密碼”進行匹配。

上述大致就是密碼匹配流程了,對於問題“ BCryptPasswordEncoder 編碼同一個密碼后結果都不一樣,怎么進行匹配”的簡單解答:

因為密碼鹽是隨機生成的,但是可以根據數據庫查詢出來的“密碼”拿到密碼鹽,同一個密碼鹽+原密碼計算 Hash 結果值是能匹配的。

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

看看加密的一個過程,

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

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

 

encode 方法傳入是原密碼,其中 int strength, SecureRandom random 這兩個構造參數是 BCryptPasswordEncoder(int strength, SecureRandom random) 構造方法按需傳入,如果不指定strength和random,默認執行 BCrypt.gensalt() 這行代碼生成也相應密碼隨機鹽。


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

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

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

    // 生成隨機字節並將其置於rnd字節數組
    random.nextBytes(rnd);

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

 

最終上面的 gensalt 方法得到一個 隨機密碼鹽+無用字符串(這個字符串可以理解為你輸入的密碼) 計算 Hash 操作和 Base64 編碼拼接一些標識符 生成假“密碼”(這個假“密碼”為了兼容方便調用 hashpw 方法),最后關鍵點就是調用 BCrypt.hashpw 方法取到密碼鹽生成相應的真實“密碼”(這個得到的密碼可以用於保存在數據庫中了)。

對於問題“同一個密碼計算 Hash 不應該是一樣的嗎?每次使用 BCryptPasswordEncoder 編碼同一個密碼都是不一樣的?”的簡單解答:

因為用到的隨機密碼鹽每次都是不一樣的,同一個密碼和不同的密碼鹽組合計算出來的 Hash 值肯定不一樣啦,所以編碼同一個密碼得到的結果都是不一樣。

建議和想法

本文主要講解一些安全性防護的思想,學習的過程思想很重要。

登錄注冊是每個系統都具備的功能,開發的同學記住一定不能保存明文密碼,否則被脫庫就會造成嚴重的后果。如果是通過上述的方法進行密碼保存,即便拿到“密碼”也非常難還原密碼。

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

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

  2. Spring Security 每次 Hash 之前用的鹽都是隨機,鹽可以保存在最終生成的“密碼”中,這樣每個密碼都是用了相應不同的隨機鹽+原密碼計算 Hash 值得到,暴力破解難度也變大了。




原鏈接:https://www.jianshu.com/p/922963106729


免責聲明!

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



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