Spring security中的BCryptPasswordEncoder方法對密碼進行加密與密碼匹配以及MD5util工具類

一般記錄用戶密碼,我們都是通過MD5加密配置的形式。這裏記錄一下,MD5加密的工具類。

package com.mms.utils;

import java.security.MessageDigest;

/**
 * Created by codermen on 2017/10/26.
 */
public class MD5Util {
    public static void main(String[] args) {
        String pwd = getMD5("99991");
        System.out.println(pwd);
    }

    //生成MD5
    public static String getMD5(String message) {
        String md5 = "";
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");  // 創建一個md5算法對象
            byte[] messageByte = message.getBytes("UTF-8");
            byte[] md5Byte = md.digest(messageByte);              // 獲得MD5字節數組,16*8=128位
            md5 = bytesToHex(md5Byte);                            // 轉換爲16進制字符串
        } catch (Exception e) {
            e.printStackTrace();
        }
        return md5;
    }

    // 二進制轉十六進制
    public static String bytesToHex(byte[] bytes) {
        StringBuffer hexStr = new StringBuffer();
        int num;
        for (int i = 0; i < bytes.length; i++) {
            num = bytes[i];
            if(num < 0) {
                num += 256;
            }
            if(num < 16){
                hexStr.append("0");
            }
            hexStr.append(Integer.toHexString(num));
        }
        return hexStr.toString().toUpperCase();
    }
}

淺談使用springsecurity中的BCryptPasswordEncoder方法對密碼進行加密(encode)與密碼匹配(matches)

spring security中的BCryptPasswordEncoder方法採用SHA-256 +隨機鹽+密鑰對密碼進行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(這個與編碼/解碼一樣),但是採用Hash處理,其過程是不可逆的。

(1)加密(encode):註冊用戶時,使用SHA-256+隨機鹽+密鑰把用戶輸入的密碼進行hash處理,得到密碼的hash值,然後將其存入數據庫中。

(2)密碼匹配(matches):用戶登錄時,密碼匹配階段並沒有進行密碼解密(因爲密碼經過Hash處理,是不可逆的),而是使用相同的算法把用戶輸入的密碼進行hash處理,得到密碼的hash值,然後將其與從數據庫中查詢到的密碼hash值進行比較。如果兩者相同,說明用戶輸入的密碼正確。

這正是爲什麼處理密碼時要用hash算法,而不用加密算法。因爲這樣處理即使數據庫泄漏,黑客也很難破解密碼(破解密碼只能用彩虹表)。

學習到這一塊,查看了一些源碼。以BCryptPasswordEncoder爲例

public class BCryptPasswordEncoderTest {
    public static void main(String[] args) {
        String pass = "admin";
        BCryptPasswordEncoder bcryptPasswordEncoder = new BCryptPasswordEncoder();
        String hashPass = bcryptPasswordEncoder.encode(pass);
        System.out.println(hashPass);

        boolean f = bcryptPasswordEncoder.matches("admin",hashPass);
        System.out.println(f);

    }
}

可以看到,每次輸出的hashPass 都不一樣,但是最終的f都爲 true,即匹配成功。查看代碼,可以看到,其實每次的隨機鹽,都保存在hashPass中。在進行matchs進行比較時,調用BCrypt 的String hashpw(String password, String salt)方法。兩個參數即”admin“和 hashPass

//******BCrypt.java******salt即取出要比較的DB中的密碼*******
real_salt = salt.substring(off + 3, off + 25);
try {
// ***************************************************
    passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8");
}
catch (UnsupportedEncodingException uee) {}
saltb = decode_base64(real_salt, BCRYPT_SALT_LEN);
B = new BCrypt();
hashed = B.crypt_raw(passwordb, saltb, rounds);

假定一次hashPass爲:10$AxafsyVqK51p.s9WAEYWYeIY9TKEoG83LTEOSB3KUkoLtGsBKhCwe隨機鹽即爲 AxafsyVqK51p.s9WAEYWYe(salt = BCrypt.gensalt();中有描述)
可見,隨機鹽(AxafsyVqK51p.s9WAEYWYe),會在比較的時候,重新被取出。即,加密的hashPass中,前部分已經包含了鹽信息。

如果只是想使用SpringSecurity + SpringBoot完成密碼加密/解密操作,而不使用SpringSecurty提供的其它權證驗證功能。具體步驟如下:
1 BCrypt密碼加密
1.1 準備工作
任何應用考慮到安全,絕不能明文的方式保存密碼。密碼應該通過哈希算法進行加密。
有很多標準的算法比如SHA或者MD5,結合salt(鹽)是一個不錯的選擇。 Spring Security
提供了BCryptPasswordEncoder類,實現Spring的PasswordEncoder接口使用BCrypt強
哈希方法來加密密碼。
BCrypt強哈希方法 每次加密的結果都不一樣。
(1)tensquare_user工程的pom引入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring‐boot‐starter‐security</artifactId>
</dependency>

(2)添加配置類 (資源/工具類中提供)

我們在添加了spring security依賴後,所有的地址都被spring security所控制了,我們目
前只是需要用到BCrypt密碼加密的部分,所以我們要添加一個配置類,配置爲所有地址
都可以匿名訪問

/**
* 安全配置類
*/
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
      .authorizeRequests()
      .antMatchers("/**").permitAll()
      .anyRequest().authenticated()
      .and().csrf().disable();
  }
}

(3)修改tensquare_user工程的Application, 配置bean

@Bean
public BCryptPasswordEncoder bcryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}

之後就可以使用BCryptPasswordEncoder中的方法完成加密/解密操作:

加密:
bcryptPasswordEncoder.encoder(password)
解密:
bcrytPasswordEncoder.matches(rawPassword,encodedPassword)

在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) 方法進行密碼匹配。

再詳細看看 BCrypt  checkpw(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

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章