關於密碼加密的問題,鬆哥以前已經和你們聊過了,參考:java
這篇文章中,鬆哥給你們介紹了兩種密碼加密方案,可是兩種都是獨立使用的!能不能在同一個項目中同時存在多種密碼加密方案呢?答案是確定的!算法
今天鬆哥就來和你們聊一聊,如何在 Spring Security 中,讓多種不一樣的密碼加密方案並存。spring
本文是 Spring Security 系列第 31 篇,閱讀前面文章有助於更好的理解本文:數據庫
爲何要加密?常見的加密算法等等這些問題我就再也不贅述了,你們能夠參考以前的:Spring Boot 中密碼加密的兩種姿式!,我們直接來看今天的正文。後端
在 Spring Security 中,跟密碼加密/校驗相關的事情,都是由 PasswordEncoder 來主導的,PasswordEncoder 擁有衆多的實現類:跨域
這些實現類,有的已通過期了,有的用處不大。對於咱們而言,最經常使用的莫過於 BCryptPasswordEncoder。安全
PasswordEncoder 自己是一個接口,裏邊只有三個方法:session
public interface PasswordEncoder { String encode(CharSequence rawPassword); boolean matches(CharSequence rawPassword, String encodedPassword); default boolean upgradeEncoding(String encodedPassword) { return false; } }
PasswordEncoder 的實現類,則具體實現了這些方法。app
對於咱們開發者而言,咱們一般都是在 SecurityConfig 中配置一個 PasswordEncoder 的實例,相似下面這樣:框架
@Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
剩下的事情,都是由系統調用的。今天咱們就來揭開系統調用的神祕面紗!咱們一塊兒來看下系統究竟是怎麼調用的!
首先,鬆哥在前面的文章中和你們提到過,Spring Security 中,若是使用用戶名/密碼的方式登陸,密碼是在 DaoAuthenticationProvider 中進行校驗的,你們能夠參考:SpringSecurity 自定義認證邏輯的兩種方式(高級玩法)。
咱們來看下 DaoAuthenticationProvider 中密碼是如何校驗的:
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { 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 建立之時,就指定了 PasswordEncoder,彷佛並無用到咱們一開始配置的 Bean?其實不是的!在 DaoAuthenticationProvider 建立之時,會制定一個默認的 PasswordEncoder,若是咱們沒有配置任何 PasswordEncoder,將使用這個默認的 PasswordEncoder,若是咱們自定義了 PasswordEncoder 實例,那麼會使用咱們自定義的 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); }
從這段代碼中咱們能夠看到:
至此,就真相大白了,咱們配置的 PasswordEncoder 實例確實用上了。
同時你們看到,若是咱們不進行任何配置,默認的 PasswordEncoder 也會被提供,那麼默認的 PasswordEncoder 是什麼呢?咱們就從這個方法看起:
public DaoAuthenticationProvider() { setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); }
繼續:
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() {} }
能夠看到:
咱們來看下 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 + "\""); } } }
這段代碼比較長,我來和你們挨個解釋下:
{編碼器名稱}
,例如若是你使用 BCryptPasswordEncoder 進行編碼,那麼生成的密碼就相似 {bcrypt}$2a$10$oE39aG10kB/rFu2vQeCJTu/V/v4n6DRR0f8WyXRiAYvBpmadoOBE.
。這樣有什麼用呢?每種密碼加密以後,都會加上一個前綴,這樣看到前綴,就知道該密文是使用哪一個編碼器生成的了。OK,至此,相信你們都明白了 DelegatingPasswordEncoder 的工做原理了。
若是咱們想同時使用多個密碼加密方案,看來使用 DelegatingPasswordEncoder 就能夠了,而 DelegatingPasswordEncoder 默認還不用配置。
接下來咱們稍微體驗一下 DelegatingPasswordEncoder 的用法。
首先咱們來生成三個密碼做爲測試密碼:
@Test void contextLoads() { Map<String, PasswordEncoder> encoders = new HashMap<>(); encoders.put("bcrypt", new BCryptPasswordEncoder()); encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); DelegatingPasswordEncoder encoder1 = new DelegatingPasswordEncoder("bcrypt", encoders); DelegatingPasswordEncoder encoder2 = new DelegatingPasswordEncoder("MD5", encoders); DelegatingPasswordEncoder encoder3 = new DelegatingPasswordEncoder("noop", encoders); String e1 = encoder1.encode("123"); String e2 = encoder2.encode("123"); String e3 = encoder3.encode("123"); System.out.println("e1 = " + e1); System.out.println("e2 = " + e2); System.out.println("e3 = " + e3); }
生成結果以下:
e1 = {bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi e2 = {MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2 e3 = {noop}123
接下來,咱們把這三個密碼拷貝到 SecurityConfig 中去:
@Configuration("aaa") public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override @Bean protected UserDetailsService userDetailsService() { InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); manager.createUser(User.withUsername("javaboy").password("{bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi").roles("admin").build()); manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build()); manager.createUser(User.withUsername("江南一點雨").password("{MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2").roles("user").build()); return manager; } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") ... } }
這裏三個用戶使用三種不一樣的密碼加密方式。
配置完成後,重啓項目,分別使用 javaboy/12三、sang/123 以及 江南一點雨/123 進行登陸,發現都能登陸成功。
爲何咱們會有這種需求?想在項目種同時存在多種密碼加密方案?其實這個主要是針對老舊項目改造用的,密碼加密方式一旦肯定,基本上無法再改了(你總不能讓用戶從新註冊一次吧),可是咱們又想使用最新的框架來作密碼加密,那麼無疑,DelegatingPasswordEncoder 是最佳選擇。
好啦,這就是今天和小夥伴們分享的多種密碼加密方案問題,感興趣的小夥伴記得點個在看鼓勵下鬆哥哦~