使用者密碼加密方式更新這種需求,一般在老項目翻版重做或者是遷移了其他第三方的使用者時可能會用到。
以老項目密碼采用MD5,需要轉換為SM3加密方式為例,實作的效果是使用者在登入時,如果加密方式是MD5而不是SM3,且密碼對比成功就更新為SM3。
SpringSecurity 的認證部分是通過 AuthenticationManager 實作的,不考慮我們自定義直接實作AuthenticationManager 進行認證,那麼在架構中實際的認證工作就是AuthenticationProvider 的實作類來完成的。(這裡沒看過源碼的朋友可能不了解,但是并不影響我們下面學習加密方式更新的思路)
在密碼更新的過程中,資料庫中必然同時存在2種甚至2種以上加密方式存儲的密碼,我們先來看看Security是如何處裡多種加密方式并存的。
1 資料庫密碼的相容
1.1 DaoAuthenticationProvider
我們首先來看下 DaoAuthenticationProvider 的源碼,它包含以下方法
方法 retrieveUser() 的作用是根據使用者輸入的賬号或其他可以唯一辨別使用者身份的入參,從資料庫中查詢出整個使用者對象的資訊,一般通過userDetailService實作,這裡不是我們關注的重點。
在擷取到資料庫中的使用者資訊後,接下來就是使用使用者輸入的密碼與查詢到的密碼做對比了。通過源碼可以發現對比的工作是通過 passwordEncoder.matches()方法實作的。我們知道從資料庫中查詢到的密碼的加密方式有兩種可能,MD5和SM3,顯然matches()方法必須同時滿足這兩種加密方式的校驗。
@Override
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
//前台傳的密碼進行加密和資料庫存的密碼做對比
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
我們可以通過DaoAuthenticationProvider 中的 setPasswordEncoder(PasswordEncoder passwordEncoder)方法來設定需要的密碼驗證邏輯,這個邏輯的實作必須同時滿足這MD5和SM3兩種方式的校驗。
也就是說,我們需要一個PasswordEncoder的實作類,能夠同時驗證MD5和SM3。
實際上,security架構在DaoAuthenticationProvider構造器中已經預設為我們設定了一個PasswordEncoder對象,而這個對象就滿足多種加密方式的校驗。
public DaoAuthenticationProvider() {
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
1.2 PasswordEncoderFactories
通過 PasswordEncoderFactories 的實作我們可以看到,它是以bcrypt作為預設的加密方式,然後将其他加密方式的實作放到了一個map中。
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);
}
盡管這并不滿足我們預設SM3 的需求,但從這裡基本可以推測出,最終肯定是從DelegatingPasswordEncoder對象管理的衆多加密實作中選擇和資料庫加密方式比對的進行驗證了。如果我們能将預設的 bcrypt 修改為 SM3 并在map中添加對應的實作,也就完成了新新增賬號的采用SM3加密的工作。
1.3 DelegatingPasswordEncoder
我們回到matches()方法,去看DelegatingPasswordEncoder對象的實作,第一個參數是明文密碼,第二個參數是資料庫中查詢到的密碼,可以看到這段代碼先是從資料庫中查詢到的密碼中提取了一個id,然後又使用這個id從DelegatingPasswordEncoder 對象管理的 map 中提取了對應的加密實作,用這個具體的實作去做密碼比對校驗,這是典型的代理模式應用。
@Override
public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
if (rawPassword == null && prefixEncodedPassword == null) {
return true;
}
//從資料庫中查詢到的密碼中提取id
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);
}
不難知道,這個id就是map的key值。跟蹤extractId()的實作就可以知道資料庫中的密碼前面應該拼接了一個{MD5}或{SM3},這就需要我們遷移原始資料時做一下加密辨別 。eg:{MD5}e10adc3949ba59abbe56e057f20f883e
通過分析上面的代碼,我們隻需要自己new一個DelegatingPasswordEncoder對象,按照需求自定義 new DelegatingPasswordEncoder(encodingId, encoders)的兩個參數,也就是設定預設的id和加密方式是SM3,在encoders中加入SM3加密方式實作。(這裡需要我們新增PasswordEncoder接口的SM3加密實作)
最後,在初始化DaoAuthenticationProvider對象時(一般我們會自定義它的子類實作),需要通過setPasswordEncoder()設定自定義的DelegatingPasswordEncoder對象。到這裡我們就實作了對資料庫中密碼存在多種加密方式的相容,但隻是驗證的相容,接下來還需要将資料庫的密碼改為新加密方式存儲。
2 更新資料庫密碼
回到 DaoAuthenticationProvider來看下面的方法。
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
//令upgradeEncoding 為true就可以直接更新密碼了
boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
//用戶端傳的密碼-(這裡是明文)
String presentedPassword = authentication.getCredentials().toString();
//使用預設加密方式對密碼進行加密處理
String newPassword = this.passwordEncoder.encode(presentedPassword);
//更新密碼
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
分析這段代碼,隻要 upgradeEncoding 為 true,就會觸發密碼加密方式的更新。第一個條件需要設定自己的userDetailsPasswordService 實作;第二個條件 upgradeEncoding()方法是判斷目前資料庫中的密碼方式辨別是否為預設加密方式,如果不同則傳回true,就是判斷密碼字首是不是{SM3}
這裡的encode(presentedPassword)方法傳回值newPassword就是使用的預設加密方式SM3的加密結果。
現在,我們隻要實作UserDetailsPasswordService接口,邏輯就是操作資料庫修改密碼。同時不用忘記在初始化 DaoAuthenticationProvider對象時,調用setUserDetailsPasswordService()方法把這個service設定一下。
public interface UserDetailsPasswordService {
UserDetails updatePassword(UserDetails user, String newPassword);
}
最後總結一下密碼更新的思路,最核心的就是在資料庫密碼的前面加上一個字首,例如{MD5},{SM3}...,做密碼校驗時,先從資料庫中查詢出密碼,根據密碼的字首對使用者輸入的明文密碼采用對應的加密方式進行加密,然後對比兩個密碼是否相同。
如果相同,說明使用者密碼正确,接下來判斷資料庫中的密碼字首是不是{SM3},如果不是,就把使用者輸入的明文密碼用SM3加密一下,替換掉資料庫裡的密碼字段。
有了這個思路,是否采用Security架構就不重要了。