1. 程式人生 > 程式設計 >Spring Security保護使用者密碼常用方法詳解

Spring Security保護使用者密碼常用方法詳解

1. 前言

本節將對 Spring Security 中的密碼編碼進行一些探討。

2. 不推薦使用md5

首先md5 不是加密演算法,是雜湊摘要。以前通常使用其作為密碼雜湊來保護密碼。由於彩虹表的出現,md5 和sha1之類的摘要演算法都已經不安全了。如果有不相信的同學 可以到一些解密網站 如 cmd5 網站嘗試解密 你會發現 md5 和 sha1 是真的非常容易被破解。

3. Spring Security中的密碼演算法

ObjectProvider<PasswordEncoder>引數。這裡的PasswordEncoder`就是我們對密碼進行編碼的工具介面。該介面只有兩個功能:一個是匹配驗證。另一個是密碼編碼。

Spring Security保護使用者密碼常用方法詳解

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

3.1 委託密碼編碼器 DelegatingPasswordEncoder

什麼是委託(Delegate)?就是甲方交給乙方的活。乙方呢手裡又很多的渠道,但是乙方光想賺差價又不想幹活。所以乙方根據一些規則又把活委託給了別人,讓別人來幹。這裡的乙方就是DelegatingPasswordEncoder 。該類維護了以下清單:

  • final String idForEncode 通過id來匹配編碼器,該id不能是{} 包括的。DelegatingPasswordEncoder 初始化傳入,用來提供預設的密碼編碼器。
  • final PasswordEncoder passwordEncoderForEncode 通過上面idForEncode所匹配到的PasswordEncoder 用來對密碼進行編碼。
  • final Map&lt;String,PasswordEncoder&gt; 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 。也就是字首{} 包含了編碼的方式再拼接上該方式編碼後的密碼串。

DelegatingPasswordEncoder 密碼匹配方法:

  @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 在哪裡例項化呢?

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 進行編碼。我們可終於明白了為什麼上一文中我們使用 {noop12345} 能和我們前臺輸入的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 編碼演算法

這裡簡單提一下bcrypt, bcrypt使用的是布魯斯·施內爾在1993年釋出的 Blowfish 加密演算法。bcrypt 演算法將salt隨機並混入最終加密後的密碼,驗證時也無需單獨提供之前的salt,從而無需單獨處理salt問題。加密後的格式一般為:

$2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa
其中:$是分割符,無意義;2a是bcrypt加密版本號;10是cost的值;而後的前22位是salt值;再然後的字串就是密碼的密文了。

5.1 bcrypt 特點

bcrypt有個特點就是非常慢。這大大提高了使用彩虹表進行破解的難度。也就是說該型別的密碼暗文擁有讓破解者無法忍受的時間成本。同時對於開發者來說也需要注意該時長是否能超出系統忍受範圍內。通常是MD5的數千倍。
同樣的密碼每次使用bcrypt編碼,密碼暗文都是不一樣的。 也就是說你有兩個網站如果都使用了bcrypt 它們的暗文是不一樣的,這不會因為一個網站洩露密碼暗文而使另一個網站也洩露密碼暗文。
所以從bcrypt的特點上來看,其安全強度還是非常有保證的。

6. 總結

今天我們對Spring Security中的密碼編碼進行分析。發現了預設情況下使用bcrypt進行編碼。而密碼驗證匹配則通過密碼暗文字首中的加密方式id控制。你也可以向Spring IoC容器注入一個PasswordEncoder型別的Bean 來達到自定義的目的。我們還對bcrypt演算法進行一些簡單瞭解,對其特點進行了總結。後面我們會Spring Security進行進一步學習。關於上一篇文章的demo我也已經替換成了資料庫管理使用者。

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支援我們。