Spring Security 實戰(zhàn)干貨:如何保護(hù)用戶密碼
文章目錄
1. 前言
上一文(https://www.felord.cn)我們對Spring Security中的重要用戶信息主體UserDetails
進(jìn)行了探討。中間例子我們使用了明文密碼,規(guī)則是通過對密碼明文添加{noop}
前綴。那么本節(jié)將對 Spring Security 中的密碼編碼進(jìn)行一些探討。
2. 不推薦使用md5
首先md5
不是加密算法,是哈希摘要。以前通常使用其作為密碼哈希來保護(hù)密碼。由于彩虹表的出現(xiàn),md5
和sha1
之類的摘要算法都已經(jīng)不安全了。如果有不相信的同學(xué) 可以到一些解密網(wǎng)站 如 cmd5 網(wǎng)站嘗試解密 你會發(fā)現(xiàn) md5
和 sha1
是真的非常容易被破解。
3. Spring Security中的密碼算法
上一文(https://www.felord.cn)我們提到了InMemoryUserDetailsManager
初始化Bean 需要傳輸一個ObjectProvider<PasswordEncoder>
參數(shù)。這里的PasswordEncoder
就是我們對密碼進(jìn)行編碼的工具接口。該接口只有兩個功能: 一個是匹配驗證。另一個是密碼編碼。
上圖就是Spring Security 提供的org.springframework.security.crypto.password.PasswordEncoder
一些實現(xiàn),有的已經(jīng)過時。其中我們注意到一個叫委托密碼編碼器的實現(xiàn) 。
3.1 委托密碼編碼器 DelegatingPasswordEncoder
什么是委托(Delegate)? 就是甲方交給乙方的活。乙方呢手里又很多的渠道,但是乙方光想賺差價又不想干活。所以乙方根據(jù)一些規(guī)則又把活委托給了別人,讓別人來干。這里的乙方就是DelegatingPasswordEncoder
。該類維護(hù)了以下清單:
final String idForEncode
通過id來匹配編碼器,該id不能是{}
包括的。DelegatingPasswordEncoder
初始化傳入,用來提供默認(rèn)的密碼編碼器。final PasswordEncoder passwordEncoderForEncode
通過上面idForEncode
所匹配到的PasswordEncoder
用來對密碼進(jìn)行編碼。final Map<String, PasswordEncoder> idToPasswordEncoder
用來維護(hù)多個idForEncode
與具體PasswordEncoder
的映射關(guān)系。DelegatingPasswordEncoder
初始化時裝載進(jìn)去,會在初始化時進(jìn)行一些規(guī)則校驗。PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder()
默認(rèn)的密碼匹配器,上面的Map
中都不存在就用它來執(zhí)行matches
方法進(jìn)行匹配驗證。這是一個內(nèi)部類實現(xiàn)。
DelegatingPasswordEncoder
編碼方法:
@Override
public String encode(CharSequence rawPassword) {
return PREFIX this.idForEncode SUFFIX this.passwordEncoderForEncode.encode(rawPassword);
}
從上面源碼可以看出來通過DelegatingPasswordEncoder
編碼后的密碼是遵循一定的規(guī)則的,遵循{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
規(guī)則的密碼編碼串。通過獲取編碼方式id (idForEncode
) 來從 DelegatingPasswordEncoder
中的映射集合idToPasswordEncoder
中獲取具體的PasswordEncoder
進(jìn)行匹配校驗。找不到就使用UnmappedIdPasswordEncoder
。
這就是 DelegatingPasswordEncoder
的工作流程。那么DelegatingPasswordEncoder
在哪里實例化呢?
3.2 密碼器靜態(tài)工廠PasswordEncoderFactories
從名字上就看得出來這是個工廠啊,專門制造 PasswordEncoder
。而且還是個靜態(tài)工廠只提供了初始化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
提供的密碼編碼方式。默認(rèn)采用了bcrypt 進(jìn)行編碼。我們可終于明白了為什么上一文中我們使用 {noop12345}
能和我們前臺輸入的12345
匹配上。這么搞有什么好處呢?這可以實現(xiàn)一個場景,如果有一天我們對密碼編碼規(guī)則進(jìn)行替換或者輪轉(zhuǎn)?,F(xiàn)有的用戶不會受到影響。 那么Spring Security 是如何配置密碼編碼器PasswordEncoder
呢?
4. Spring Security 加載 PasswordEncoder 的規(guī)則
我們在Spring Security配置適配器WebSecurityConfigurerAdapter
(該類我以后的文章會仔細(xì)分析 可通過https://felord.cn
來及時獲取相關(guān)信息)找到了引用PasswordEncoderFactories
的地方,一個內(nèi)部 PasswordEncoder
實現(xiàn) LazyPasswordEncoder
。從源碼上看該類是懶加載的只有用到了才去實例化。在該類的內(nèi)部方法中發(fā)現(xiàn)了 PasswordEncoder
的規(guī)則。
// 獲取最終干活的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;
}
}
上面的兩個方法總結(jié):如果能從從Spring IoC容器中獲取PasswordEncoder
的Bean就用該Bean作為編碼器,沒有就使用DelegatingPasswordEncoder
。默認(rèn)是 bcrypt
方式。文中多次提到該算法。而且還是Spring Security默認(rèn)的。那么它到底是什么呢?
5. bcrypt 編碼算法
這里簡單提一下bcrypt
, bcrypt
使用的是布魯斯·施內(nèi)爾在1993年發(fā)布的 Blowfish
加密算法。bcrypt
算法將salt
隨機(jī)并混入最終加密后的密碼,驗證時也無需單獨提供之前的salt
,從而無需單獨處理salt
問題。加密后的格式一般為:
$2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa
其中:$
是分割符,無意義;2a
是bcrypt
加密版本號;10
是cost
的值;而后的前22
位是salt
值;再然后的字符串就是密碼的密文了。
5.1 bcrypt 特點
bcrypt
有個特點就是非常慢。這大大提高了使用彩虹表進(jìn)行破解的難度。也就是說該類型的密碼暗文擁有讓破解者無法忍受的時間成本。同時對于開發(fā)者來說也需要注意該時長是否能超出系統(tǒng)忍受范圍內(nèi)。通常是MD5
的數(shù)千倍。- 同樣的密碼每次使用
bcrypt
編碼,密碼暗文都是不一樣的。 也就是說你有兩個網(wǎng)站如果都使用了bcrypt
它們的暗文是不一樣的,這不會因為一個網(wǎng)站泄露密碼暗文而使另一個網(wǎng)站也泄露密碼暗文。
所以從bcrypt
的特點上來看,其安全強度還是非常有保證的。
6. 總結(jié)
今天我們對Spring Security中的密碼編碼進(jìn)行分析。發(fā)現(xiàn)了默認(rèn)情況下使用bcrypt
進(jìn)行編碼。而密碼驗證匹配則通過密碼暗文前綴中的加密方式id控制。你也可以向Spring IoC容器注入一個PasswordEncoder
類型的Bean 來達(dá)到自定義的目的。我們還對bcrypt
算法進(jìn)行一些簡單了解,對其特點進(jìn)行了總結(jié)。后面我們會Spring Security進(jìn)行進(jìn)一步學(xué)習(xí)。關(guān)于上一篇文章的demo我也已經(jīng)替換成了數(shù)據(jù)庫管理用戶。相關(guān)的代碼你可以通過關(guān)注我公眾號:Felordcn
回復(fù) ss02
獲取。
作者:碼農(nóng)小胖哥
歡迎關(guān)注:碼農(nóng)小胖哥