在實際開發中, 我們往往需要對密碼進行加密存儲。
在Spring Security中是通過一種自適應單向函數來處理密碼問題,這種自適應單向函數的方式在進行密碼匹配時會有意占用大量系統資源(CPU,內存等),這樣就可以增加惡意用戶攻擊系統的難度。當然開發者也可以將用戶名/密碼的方式換成會話,OAuth2令牌等方式既可以快速驗證用戶憑證信息,也不損失系統安全性
PasswordEncoder
在SpringSecurity中通過PasswordEncoder接口定義了密碼加密和對比的操作,我們看下源碼
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
一共有3個方法,encode方法對明文進行加密,matches方法進行密碼比對,upgraderEncoding方法判斷當前密碼是否需要再次加密默認為false
PasswordEncoder常見實現類
Spring Security提供了一些PasswordEncoder的實現類,比如BCryptPasswordEncoder,Argon2PasswordEncoder,Pbkdf2PasswordEncoder,SCryptPasswordEncoder
下面着重介紹一下BCryptPasswordEncoder
BCryptPasswordEncoder
BCryptPasswordEncoder使用bcrypt算法對密碼進行加密,同時會為密碼加上“鹽”,開發者不需要自己加“鹽”,即使相同的明文字段生成的加密字符串也不同。匹配時,從密文中取出“鹽”,用該鹽值加密明文和最終密文作對比。
BCryptPasswordEncoder的默認強度為10,開發者可以根據自己服務器的速度進行調整,以確保密碼驗證的時間約為1秒(官方建議)
PasswordEncoder 中的 encode 方法,是我們在用戶注冊的時候手動調用。
matches 方法,則是由系統調用,默認是在 DaoAuthenticationProvider#additionalAuthenticationChecks 方法中調用的。
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
String presentedPassword = authentication.getCredentials().toString();
if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
}
可以看到,密碼比對就是通過 passwordEncoder.matches 方法來進行的
那么 DaoAuthenticationProvider 中的 passwordEncoder 從何而來呢?是不是就是我們自己在SecurityConfig 中配置的那個 Bean 呢?
我們看下 DaoAuthenticationProvider 中關於 passwordEncoder 的定義
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private PasswordEncoder passwordEncoder;
public DaoAuthenticationProvider() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return passwordEncoder;
}
}
//我們再來看看 DaoAuthenticationProvider 是怎么初始化的。
//DaoAuthenticationProvider 的初始化是在 InitializeUserDetailsManagerConfigurer#configure 方法中完成的,我們一起來看下該方法的定義:
public void configure(AuthenticationManagerBuilder auth) throws Exception {
if (auth.isConfigured()) {
return;
}
UserDetailsService userDetailsService = getBeanOrNull(
UserDetailsService.class);
if (userDetailsService == null) {
return;
}
PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);
UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class);
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
if (passwordEncoder != null) {
provider.setPasswordEncoder(passwordEncoder);
}
if (passwordManager != null) {
provider.setUserDetailsPasswordService(passwordManager);
}
provider.afterPropertiesSet();
auth.authenticationProvider(provider);
}
在 DaoAuthenticationProvider 創建之時,會制定一個默認的 PasswordEncoder,如果我們沒有配置任何 PasswordEncoder,將使用這個默認的 PasswordEncoder,如果我們自定義了 PasswordEncoder 實例,那么會使用我們自定義的 PasswordEncoder 實例!
DelegatingPasswordEncoder
實際上在Spring Security中默認的加密方式是DelegatingPasswordEncoder,
看下源碼:
public DaoAuthenticationProvider() {
setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
DelegatingPasswordEncoder是一個代理類,主要用來代理上面介紹的不同加密方式,它允許系統中存在不同的加密方案,很方便的完成對加密方案的升級
我們可以看下PasswordEncoderFactories.createDelegatingPasswordEncoder(),默認就是通過它創建了DelegatingPasswordEncoder實例
public class PasswordEncoderFactories {
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());
encoders.put("argon2", new Argon2PasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
private PasswordEncoderFactories() {}
}
可以看到:
(1)在 PasswordEncoderFactories 中,首先構建了一個 encoders,然后給所有的加密方式都取了一個名字,再把名字做 key,加密類做 value,統統存入 encoders 中。
(2)最后返回了一個 DelegatingPasswordEncoder 實例,同時傳入默認的 encodingId 就是 bcrypt,以及 encoders 實例,相當於在DelegatingPasswordEncoder默認使用的是BCryptPasswordEncoder加密
我們看下DelegatingPasswordEncoder源碼
public class DelegatingPasswordEncoder implements PasswordEncoder {
private static final String PREFIX = "{";
private static final String SUFFIX = "}";
private final String idForEncode;
private final PasswordEncoder passwordEncoderForEncode;
private final Map<String, PasswordEncoder> idToPasswordEncoder;
private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
public DelegatingPasswordEncoder(String idForEncode,
Map<String, PasswordEncoder> idToPasswordEncoder) {
if (idForEncode == null) {
throw new IllegalArgumentException("idForEncode cannot be null");
}
if (!idToPasswordEncoder.containsKey(idForEncode)) {
throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
}
for (String id : idToPasswordEncoder.keySet()) {
if (id == null) {
continue;
}
if (id.contains(PREFIX)) {
throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
}
if (id.contains(SUFFIX)) {
throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
}
}
this.idForEncode = idForEncode;
this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
}
public void setDefaultPasswordEncoderForMatches(
PasswordEncoder defaultPasswordEncoderForMatches) {
if (defaultPasswordEncoderForMatches == null) {
throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null");
}
this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches;
}
@Override
public String encode(CharSequence rawPassword) {
return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword);
}
@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);
}
private String extractId(String prefixEncodedPassword) {
if (prefixEncodedPassword == null) {
return null;
}
int start = prefixEncodedPassword.indexOf(PREFIX);
if (start != 0) {
return null;
}
int end = prefixEncodedPassword.indexOf(SUFFIX, start);
if (end < 0) {
return null;
}
return prefixEncodedPassword.substring(start + 1, end);
}
@Override
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);
}
}
private String extractEncodedPassword(String prefixEncodedPassword) {
int start = prefixEncodedPassword.indexOf(SUFFIX);
return prefixEncodedPassword.substring(start + 1);
}
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) {
String id = extractId(prefixEncodedPassword);
throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
}
}
}
(1)首先定義了前綴{ 和后綴 }來包裹加密方案的id 比如{MD5}
(2)繼承了PasswordEncoder 接口,所以它里邊的核心方法也是兩個:encode 方法用來對密碼進行編碼,matches 方法用來校驗密碼。
(3)encode方法很簡單,就是使用idForEncode(默認加密方案)加密類加密,加上{加密方法} 前綴
(4)matches方法根據前綴找到對應的加密方案,由對應的加密類匹配,如果沒有找到加密方案,則由defaultPasswordEncoderForMatches(默認是UnmappedIdPasswordEncoder)進行匹配,而這個UnmappedIdPasswordEncoder匹配方法返回一個IllegalArgumentException異常
總之如果我們想同時使用多個密碼加密方案,使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默認還不用配置。
就比如說 如果我們想要更改系統的加密方法,我們不可能讓之前的老用戶重新注冊一次,我們只需要使用 DelegatingPasswordEncoder,同時在我們的數據庫中為密碼加上 之前加密方案的 前綴,就可以使系統完成對之前加密方案的支持,調用DelegatingPasswordEncoder的setDefaultPasswordEncoderForMatches方法就可以重新設置系統的加密方案。
實例
由於Spring Security默認使用DelegatingPasswordEncoder,DelegatingPasswordEncoder默認使用BCryptPasswordEncoder加密。 如果我們需要其他的密碼加密方案,只需要向Spring容器中注入一個PasswordEncoder實例,代代替DelegatingPasswordEncoder即可
比如:
@Bean
PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
用戶注冊時調用PasswordEncoder 的 encode 方法!