SpringSecurity中PasswordEncoder的使用
概述
任何一個(gè)登錄系統(tǒng)的密碼不能明文存儲(chǔ),萬(wàn)一發(fā)生數(shù)據(jù)庫(kù)泄漏事故(不管是內(nèi)部人員導(dǎo)出數(shù)據(jù)庫(kù)數(shù)據(jù)還是被黑客攻擊破解數(shù)據(jù)庫(kù)實(shí)例節(jié)點(diǎn)拿到數(shù)據(jù)庫(kù)數(shù)據(jù)等,又或者是其他情況造成的),將產(chǎn)生巨大的損失。因此明文密碼在存儲(chǔ)到數(shù)據(jù)庫(kù)之前需要加密處理。
加密算法有很多,大致有如下分類:
- 哈希函數(shù)算法:包括消息摘要算法(MD4,MD5等),消息摘要算法是一種特殊類型的哈希函數(shù)算法,用于將任意長(zhǎng)度的數(shù)據(jù)映射為固定長(zhǎng)度的哈希值或摘要。摘要值通常用于驗(yàn)證數(shù)據(jù)的完整性、數(shù)字簽名、身份驗(yàn)證等用途。算法包括:
- MD5(Message Digest Algorithm 5):已經(jīng)不推薦使用,存在碰撞攻擊漏洞
- SHA-1(Secure Hash Algorithm 1):也存在碰撞攻擊漏洞,逐漸被淘汰
- SHA-256、SHA-384、SHA-512:SHA-2系列,目前被廣泛應(yīng)用,提供更高的安全性
- 對(duì)稱加密算法使用相同的密鑰進(jìn)行加密和解密。常見(jiàn)的對(duì)稱加密算法包括:
- DES(Data Encryption Standard):已經(jīng)不推薦使用,因?yàn)槊荑€長(zhǎng)度較短易受到攻擊
- 3DES(Triple DES):DES增強(qiáng)版,使用三個(gè)密鑰提高安全性
- AES(Advanced Encryption Standard):目前廣泛應(yīng)用的對(duì)稱加密算法,具有較高的安全性和性能
- 非對(duì)稱加密算法(公鑰加密算法):
非對(duì)稱加密算法使用一對(duì)密鑰,分別是公鑰和私鑰,公鑰用于加密,私鑰用于解密。常見(jiàn)的非對(duì)稱加密算法包括: - RSA(Rivest-Shamir-Adleman):基于大數(shù)分解難題,被廣泛用于數(shù)字簽名和密鑰交換
- ECC(Elliptic Curve Cryptography):利用橢圓曲線上的離散對(duì)數(shù)問(wèn)題,相比RSA,提供相同安全級(jí)別下更短的密鑰長(zhǎng)度和更高的性能
反查表、彩虹表
上文提到一些已經(jīng)不推薦使用、逐漸被淘汰的算法,如MD5、SHA-1。因?yàn)椴还苁荕D5還是SHA-1算法,對(duì)于給定的某個(gè)字符串(密碼),經(jīng)過(guò)哈希函數(shù)計(jì)算之后得到的結(jié)果都是固定的。比如admin經(jīng)過(guò)MD5計(jì)算(有16位和32位之分,這里用的是16位)結(jié)果始終是7a57a5a743894a0e,root經(jīng)過(guò)SHA-1計(jì)算后結(jié)果始終是dc76e9f0c0006e8f919e0c515c66dbba3982f785。
那黑客們就可以維護(hù)一個(gè)數(shù)據(jù)庫(kù),其字段包括加密后的密文、加密算法、明文密碼,意味著可以根據(jù)密文反查明文密碼。這就是反查表。
基于反查表,黑客們后來(lái)發(fā)明更高級(jí)的彩虹表。
在Java Web開(kāi)發(fā)中,我們會(huì)遇到各種各樣的安全問(wèn)題。作為最基本的,數(shù)據(jù)庫(kù)密碼的安全性如何得到保證呢?此時(shí)Spring Security隆重登場(chǎng),可以幫助我們解決這個(gè)問(wèn)題。
Spring Security
實(shí)例
加密密碼的配置類(代碼片段):
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity()
public class WebSecurityConfig implements SecurityFilterChain {
@Resource
private UserDetailsService userDetailsService;
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
.userDetailsService(this.userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
PasswordEncoder
PasswordEncoder接口定義如下:
public interface PasswordEncoder {
// 用來(lái)對(duì)明文密碼進(jìn)行加密
String encode(CharSequence rawPassword);
// 用來(lái)進(jìn)行密碼比對(duì)
boolean matches(CharSequence rawPassword, String encodedPassword);
// 用來(lái)判斷當(dāng)前密碼是否需要升級(jí),默認(rèn)返回false表示不需要升級(jí)
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
尚未廢棄的實(shí)現(xiàn)類,都是自適應(yīng)單向函數(shù)(Adaptive One-way Functions)來(lái)處理密碼問(wèn)題,這種函數(shù)在進(jìn)行密碼匹配時(shí),會(huì)有意占用大量系統(tǒng)資源(例如CPU、內(nèi)存等),可以增加惡意用戶攻擊系統(tǒng)的難度。包括:bcrypt、PBKDF2、scrypt以及argon2。
因此實(shí)現(xiàn)類包括:
- BCryptPasswordEncoder:使用bcrypt強(qiáng)散列算法對(duì)密碼進(jìn)行加密,為提高密碼的安全性,bcrypt算法故意降低運(yùn)行速度,以增強(qiáng)密碼破解的難度。BCryptPasswordEncoder自帶salt加鹽機(jī)制,即使相同的明文每次生成的加密字符串都不相同。默認(rèn)強(qiáng)度為10(參考源碼里的strength字段),開(kāi)發(fā)者可以根據(jù)自己的服務(wù)器性能進(jìn)行調(diào)整,以確保密碼驗(yàn)證時(shí)間約為1秒鐘(官方建議密碼驗(yàn)證時(shí)間為1秒鐘,既可以提高系統(tǒng)安全性,又不會(huì)過(guò)多影響系統(tǒng)運(yùn)行性能)
- Argon2PasswordEncoder:使用Argon2算法對(duì)密碼進(jìn)行加密,Argon2曾在Password Hashing Competition競(jìng)賽中獲勝。為了解決在定制硬件上密碼容易被破解的問(wèn)題,Argon2也是故意降低運(yùn)算速度,同時(shí)需要大量?jī)?nèi)存
- Pbkdf2PasswordEncoder:使用PBKDF2算法對(duì)密碼進(jìn)行加密,可用于FIPS(Federal Information Processing Standard,美國(guó)聯(lián)邦信息處理標(biāo)準(zhǔn))認(rèn)證
- SCryptPasswordEncoder:使用scrypt算法對(duì)密碼進(jìn)行加密
幾個(gè)已經(jīng)被廢棄的基于消息摘要算法的實(shí)現(xiàn)類:
- NoOpPasswordEncoder:密碼明文存儲(chǔ),不可用于生產(chǎn)環(huán)境
- Md4PasswordEncoder:使用Md4算法加密密碼
- LdapShaPasswordEncoder:使用SHA算法
- StandardPasswordEncoder:使用SHA-256算法
- MessageDigestPasswordEncoder:使用MD5算法
BCryptPasswordEncoder
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
String salt = getSalt();
return BCrypt.hashpw(rawPassword.toString(), salt);
}
private String getSalt() {
if (this.random != null) {
return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
}
return BCrypt.gensalt(this.version.getVersion(), this.strength);
}
使用Spring Security提供的BCrypt工具類生成鹽(salt);然后,根據(jù)鹽和明文密碼生成最終的密文。所謂加鹽,就是在初始化明文數(shù)據(jù)時(shí),由系統(tǒng)自動(dòng)向該明文里添加一些附加數(shù)據(jù),然后散列。引入加鹽機(jī)制的目的是進(jìn)一步提高加密數(shù)據(jù)的安全性,單向散列加密及加鹽思想廣泛應(yīng)用于系統(tǒng)登錄過(guò)程中的密碼生成和校驗(yàn)。
構(gòu)造方法:
public BCryptPasswordEncoder(BCryptVersion version, int strength, SecureRandom random) {
if (strength != -1 && (strength < BCrypt.MIN_LOG_ROUNDS || strength > BCrypt.MAX_LOG_ROUNDS)) {
throw new IllegalArgumentException("Bad strength");
}
this.version = version;
this.strength = (strength == -1) ? 10 : strength;
this.random = random;
}
從構(gòu)造函數(shù)可知,strength長(zhǎng)度默認(rèn)為10,最小值為BCrypt.MIN_LOG_ROUNDS=4,最大值為BCrypt.MAX_LOG_ROUNDS=31。顯而易見(jiàn),長(zhǎng)度越長(zhǎng),加密算法越復(fù)雜,被惡意破解攻擊的難度越大,但是也會(huì)增加系統(tǒng)負(fù)載,增加加密計(jì)算時(shí)長(zhǎng)和存儲(chǔ)空間。因此需要取得權(quán)衡,默認(rèn)情況下使用Spring Security建議的長(zhǎng)度10即可。
PasswordEncoderFactories
瀏覽一下spring-security-crypto-6.2.3源碼結(jié)構(gòu):

不難發(fā)現(xiàn)PasswordEncoderFactories這個(gè)類,采用工廠方法模式,源碼:
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
靜態(tài)方法createDelegatingPasswordEncoder,encoders中存儲(chǔ)每一種密碼加密方案的id和所對(duì)應(yīng)的加密類,如bcrypt對(duì)應(yīng)BcryptPassword。最后,返回代理類DelegatingPasswordEncoder實(shí)例,并且默認(rèn)使用的加密方案是BCryptPasswordEncoder。
DelegatingPasswordEncoder
DelegatingPasswordEncoder,采用代理模式,Spring Security 5.0版本后默認(rèn)的密碼加密方案,主要考慮如下三方面的因素:
- 兼容性:使用DelegatingPasswordEncoder可以幫助許多使用舊密碼加密方式的系統(tǒng)順利遷移到Spring Security中,它允許在同一個(gè)系統(tǒng)中同時(shí)存在多種不同的密碼加密方案
- 便捷性:密碼存儲(chǔ)的最佳方案不可能一直不變,使用DelegatingPasswordEncoder作為默認(rèn)的密碼加密方案,當(dāng)需要修改加密方案時(shí),只需要修改很小一部分代碼即可實(shí)現(xiàn)
- 穩(wěn)定性:作為一個(gè)框架,Spring Security不能經(jīng)常進(jìn)行重大更改,使用Delegating PasswordEncoder可以方便地對(duì)密碼進(jìn)行升級(jí)(自動(dòng)從一個(gè)加密方案升級(jí)到另外一個(gè)加密方案)
屬性如下:
// 默認(rèn)的前綴和后綴,用于包裹將來(lái)生成的加密方案的id
private static final String DEFAULT_ID_PREFIX = "{";
private static final String DEFAULT_ID_SUFFIX = "}";
// 構(gòu)造方法里支持傳入用戶自定義的前綴和后綴
private final String idPrefix;
private final String idSuffix;
// 默認(rèn)的加密方案id
private final String idForEncode;
// 根據(jù)idForEncode從idToPasswordEncoder map中提取出來(lái)的
private final PasswordEncoder passwordEncoderForEncode;
// 保存id和加密方案之間的映射
private final Map<String, PasswordEncoder> idToPasswordEncoder;
// 默認(rèn)的密碼比對(duì)器,當(dāng)根據(jù)密碼加密方案的id無(wú)法找到對(duì)應(yīng)的加密方案時(shí),就會(huì)使用默認(rèn)的密碼比對(duì)器。默認(rèn)類型是UnmappedIdPasswordEncoder
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
UnmappedIdPasswordEncoder是一個(gè)內(nèi)部私有類:
private class UnmappedIdPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
// 直接拋出異常
throw new UnsupportedOperationException("encode is not supported");
}
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
// 并不會(huì)做任何密碼比對(duì)操作,直接拋出異常
String id = extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}
核心方法encode:
public String encode(CharSequence rawPassword) {
return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}
作為一個(gè)代理類,不負(fù)責(zé)具體的加密工作,由加密類來(lái)完成,最后加上類似于{bcrypt}這樣的前綴,不同的前綴表示使用不同的加密算法,即不同的PasswordEncoder實(shí)現(xiàn)類,當(dāng)然也包括自定義的加密類。
核心方法matches:
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);
}
extractId方法用于從加密字符串中提取出具體的加密方案id,也就是前綴和后綴包裹的字符串,如bcrypt,此方法就不貼出來(lái)了。根據(jù)加密方案id從map集合查找對(duì)應(yīng)的加密算法實(shí)現(xiàn)類,查找失敗則使用默認(rèn)的加密類,即UnmappedIdPasswordEncoder,然后就會(huì)拋出異常。
核心方法upgradeEncoding:
public boolean upgradeEncoding(String prefixEncodedPassword) {
String id = extractId(prefixEncodedPassword);
if (!this.idForEncode.equalsIgnoreCase(id)) {
return true;
} else {
String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
}
}
如果當(dāng)前加密字符串所采用的加密方案不是默認(rèn)的BcryptPasswordEncoder ,就會(huì)自動(dòng)進(jìn)行密碼升級(jí),否則就調(diào)用默認(rèn)加密方案的upgradeEncoding方法判斷密碼是否需要升級(jí)。
自定義加密方案
業(yè)務(wù)開(kāi)發(fā)中,如果Spring Security自帶的幾個(gè)加密類都不能滿足需求,或者業(yè)務(wù)場(chǎng)景比較復(fù)雜,需要兼容數(shù)據(jù)庫(kù)歷史未加密字段或加密算法不夠好的字段,則可能需要自定義加密類。
具體來(lái)說(shuō),實(shí)現(xiàn)PasswordEncoder接口類,并重寫3個(gè)方法。比如自定義一個(gè)使用SHA-512加密算法的加密類:
public class Sha512PasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return hashWithSha512(rawPassword.toString());
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
String hashedPassword = encode(rawPassword);
return encodedPassword.equals(hashedPassword);
}
@Override
public boolean upgradeEncoding(String prefixEncodedPassword) {
// 不需要升級(jí)
return false;
}
private String hashWithSha512(String input) {
StringBuilder result = new StringBuilder();
try {
MessageDigest md = MessageDigest.getInstance("SHA-512");
byte [] digested = md.digest(input.getBytes());
for (int i = 0; i < digested.length; i++) {
result.append(Integer.toHexString(0xFF & digested[i]));
}
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Bad algorithm");
}
return result.toString();
}
}
最后需要配置一下使用此自定義類,使其生效。
參考
- 深入淺出Spring Security
到此這篇關(guān)于SpringSecurity中PasswordEncoder的使用的文章就介紹到這了,更多相關(guān)SpringSecurity PasswordEncoder內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java數(shù)組操作經(jīng)典例題大總結(jié)
數(shù)組是在內(nèi)存中存儲(chǔ)相同數(shù)據(jù)類型的連續(xù)的空間,聲明一個(gè)數(shù)組就是在內(nèi)存空間中劃出一串連續(xù)的空間,下面這篇文章主要給大家介紹了關(guān)于Java數(shù)組操作經(jīng)典例題的相關(guān)資料,需要的朋友可以參考下2022-03-03
java數(shù)組的三種擴(kuò)容方式以及程序?qū)崿F(xiàn)詳解
這篇文章主要介紹了java數(shù)組的三種擴(kuò)容方式以及程序?qū)崿F(xiàn)詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12
java實(shí)現(xiàn)身份證號(hào)碼驗(yàn)證的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用java語(yǔ)言實(shí)現(xiàn)身份證號(hào)碼驗(yàn)證的功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-09-09
JDK生成WebService客戶端代碼以及調(diào)用方式
WebService 是一種跨編程語(yǔ)言和跨操作系統(tǒng)平臺(tái)的遠(yuǎn)程調(diào)用技術(shù),下面這篇文章主要給大家介紹了關(guān)于JDK生成WebService客戶端代碼以及調(diào)用方式的相關(guān)資料,文中通過(guò)代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-08-08
logback使用MDCFilter日志過(guò)濾源碼解讀
這篇文章主要介紹了logback使用MDCFilter日志過(guò)濾源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-11-11
SpringSecurity學(xué)習(xí)之自定義過(guò)濾器的實(shí)現(xiàn)代碼
這篇文章主要介紹了SpringSecurity學(xué)習(xí)之自定義過(guò)濾器的實(shí)現(xiàn)代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-01-01

