Spring Security 實戰乾貨:如何保護用戶密碼

1. 前言

上一文(https://www.felord.cn)咱們對Spring Security中的重要用戶信息主體UserDetails進行了探討。中間例子咱們使用了明文密碼,規則是經過對密碼明文添加{noop}前綴。那麼本節將對 Spring Security 中的密碼編碼進行一些探討。html

2. 不推薦使用md5

首先md5 不是加密算法,是哈希摘要。之前一般使用其做爲密碼哈希來保護密碼。因爲彩虹表的出現,md5sha1之類的摘要算法都已經不安全了。若是有不相信的同窗 能夠到一些解密網站 如 cmd5 網站嘗試解密 你會發現 md5sha1 是真的很是容易被破解。java

3. Spring Security中的密碼算法

上一文(https://www.felord.cn)咱們提到了InMemoryUserDetailsManager 初始化Bean 須要傳輸一個ObjectProvider<PasswordEncoder> 參數。這裏的PasswordEncoder就是咱們對密碼進行編碼的工具接口。該接口只有兩個功能: 一個是匹配驗證。另外一個是密碼編碼。算法

PasswordEncoderUML.png

上圖就是Spring Security 提供的org.springframework.security.crypto.password.PasswordEncoder一些實現,有的已通過時。其中咱們注意到一個叫委託密碼編碼器的實現spring

3.1 委託密碼編碼器 DelegatingPasswordEncoder

什麼是委託(Delegate)? 就是甲方交給乙方的活。乙方呢手裏又不少的渠道,可是乙方光想賺差價又不想幹活。因此乙方根據一些規則又把活委託給了別人,讓別人來幹。這裏的乙方就是DelegatingPasswordEncoder 。該類維護瞭如下清單:數據庫

  • final String idForEncode 經過id來匹配編碼器,該id不能是{} 包括的。DelegatingPasswordEncoder 初始化傳入,用來提供默認的密碼編碼器。
  • final PasswordEncoder passwordEncoderForEncode 經過上面idForEncode所匹配到的PasswordEncoder 用來對密碼進行編碼
  • final Map<String, PasswordEncoder> idToPasswordEncoder 用來維護多個idForEncode與具體PasswordEncoder的映射關係。DelegatingPasswordEncoder 初始化時裝載進去,會在初始化時進行一些規則校驗。
  • PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder() 默認的密碼匹配器,上面的Map中都不存在就用它來執行matches方法進行匹配驗證。這是一個內部類實現。

DelegatingPasswordEncoder 編碼方法:安全

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

從上面源碼能夠看出來經過DelegatingPasswordEncoder 編碼後的密碼是遵循必定的規則的,遵循{idForEncode}encodePassword 。也就是前綴{} 包含了編碼的方式再拼接上該方式編碼後的密碼串。app

DelegatingPasswordEncoder 密碼匹配方法:ide

@Override
  public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
      if (rawPassword == null && prefixEncodedPassword == null) {
          return true;
      }
      String id = extractId(prefixEncodedPassword);
      PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
      if (delegate == null) {
          return this.defaultPasswordEncoderForMatches
              .matches(rawPassword, prefixEncodedPassword);
      }
      String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
      return delegate.matches(rawPassword, encodedPassword);
  }

密碼匹配經過傳入原始密碼和遵循{idForEncode}encodePassword規則的密碼編碼串。經過獲取編碼方式id (idForEncode) 來從 DelegatingPasswordEncoder中的映射集合idToPasswordEncoder中獲取具體的PasswordEncoder進行匹配校驗。找不到就使用UnmappedIdPasswordEncoder工具

這就是 DelegatingPasswordEncoder 的工做流程。那麼DelegatingPasswordEncoder 在哪裏實例化呢?oop

3.2 密碼器靜態工廠PasswordEncoderFactories

從名字上就看得出來這是個工廠啊,專門製造 PasswordEncoder 。並且仍是個靜態工廠只提供了初始化DelegatingPasswordEncoder的方法:

@SuppressWarnings("deprecation")
    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);
    }

從上面能夠很是具體地看出來DelegatingPasswordEncoder提供的密碼編碼方式。默認採用了bcrypt 進行編碼。咱們可終於明白了爲何上一文中咱們使用 {noop}12345 能和咱們前臺輸入的12345匹配上。這麼搞有什麼好處呢?這能夠實現一個場景,若是有一天咱們對密碼編碼規則進行替換或者輪轉。現有的用戶不會受到影響。 那麼Spring Security 是如何配置密碼編碼器PasswordEncoder 呢?

4. Spring Security 加載 PasswordEncoder 的規則

咱們在Spring Security配置適配器WebSecurityConfigurerAdapter(該類我之後的文章會仔細分析 可經過https://felord.cn 來及時獲取相關信息)找到了引用PasswordEncoderFactories的地方,一個內部 PasswordEncoder實現 LazyPasswordEncoder。從源碼上看該類是懶加載的只有用到了纔去實例化。在該類的內部方法中發現了 PasswordEncoder 的規則。

// 獲取最終幹活的PasswordEncoder
        private PasswordEncoder getPasswordEncoder() {
            if (this.passwordEncoder != null) {
                return this.passwordEncoder;
            }
            PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
            if (passwordEncoder == null) {
                passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
            }
            this.passwordEncoder = passwordEncoder;
            return passwordEncoder;
        }
        // 從Spring IoC容器中獲取Bean 有可能獲取不到
        private <T> T getBeanOrNull(Class<T> type) {
            try {
                return this.applicationContext.getBean(type);
            } catch(NoSuchBeanDefinitionException notFound) {
                return null;
            }
        }

上面的兩個方法總結:若是能從從Spring IoC容器中獲取PasswordEncoder的Bean就用該Bean做爲編碼器,沒有就使用DelegatingPasswordEncoder 。默認是 bcrypt 方式。文中屢次提到該算法。並且仍是Spring Security默認的。那麼它究竟是什麼呢?

5. bcrypt 編碼算法

這裏簡單提一下bcryptbcrypt使用的是布魯斯·施內爾在1993年發佈的 Blowfish 加密算法。bcrypt 算法將salt隨機並混入最終加密後的密碼,驗證時也無需單獨提供以前的salt,從而無需單獨處理salt問題。加密後的格式通常爲:

$2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa

其中:$是分割符,無心義;2abcrypt加密版本號;10cost的值;然後的前22位是salt值;再而後的字符串就是密碼的密文了。

5.1 bcrypt 特色

  • bcrypt有個特色就是很是慢。這大大提升了使用彩虹表進行破解的難度。也就是說該類型的密碼暗文擁有讓破解者沒法忍受的時間成本。同時對於開發者來講也須要注意該時長是否能超出系統忍受範圍內。一般是MD5的數千倍。
  • 一樣的密碼每次使用bcrypt編碼,密碼暗文都是不同的。 也就是說你有兩個網站若是都使用了bcrypt 它們的暗文是不同的,這不會由於一個網站泄露密碼暗文而使另外一個網站也泄露密碼暗文。

因此從bcrypt的特色上來看,其安全強度仍是很是有保證的。

6. 總結

今天咱們對Spring Security中的密碼編碼進行分析。發現了默認狀況下使用bcrypt進行編碼。而密碼驗證匹配則經過密碼暗文前綴中的加密方式id控制。你也能夠向Spring IoC容器注入一個PasswordEncoder類型的Bean 來達到自定義的目的。咱們還對bcrypt算法進行一些簡單瞭解,對其特色進行了總結。後面咱們會Spring Security進行進一步學習。關於上一篇文章的demo我也已經替換成了數據庫管理用戶。相關的代碼你能夠經過關注我公衆號:Felordcn 回覆 ss02 獲取。

關注公衆號:Felordcn或者https://felord.cn獲取更多資訊

相關文章
相關標籤/搜索