這篇文章中,松哥給大家介紹了兩種密碼加密方案,但是兩種都是獨(dú)立使用的!能不能在同一個(gè)項(xiàng)目中同時(shí)存在多種密碼加密方案呢?答案是肯定的!
今天松哥就來(lái)和大家聊一聊,如何在 Spring Security 中,讓多種不同的密碼加密方案并存。
為什么要加密?常見(jiàn)的加密算法等等這些問(wèn)題我就不再贅述了,大家可以參考之前的:Spring Boot 中密碼加密的兩種姿勢(shì)!,咱們直接來(lái)看今天的正文。
1.PasswordEncoder
在 Spring Security 中,跟密碼加密/校驗(yàn)相關(guān)的事情,都是由 PasswordEncoder 來(lái)主導(dǎo)的,PasswordEncoder 擁有眾多的實(shí)現(xiàn)類:
這些實(shí)現(xiàn)類,有的已經(jīng)過(guò)期了,有的用處不大。對(duì)于我們而言,最常用的莫過(guò)于 BCryptPasswordEncoder。
PasswordEncoder 本身是一個(gè)接口,里邊只有三個(gè)方法:
- public interface PasswordEncoder {
- String encode(CharSequence rawPassword);
- boolean matches(CharSequence rawPassword, String encodedPassword);
- default boolean upgradeEncoding(String encodedPassword) {
- return false;
- }
- }
- encode 方法用來(lái)對(duì)密碼進(jìn)行加密。
- matches 方法用來(lái)對(duì)密碼進(jìn)行比對(duì)。
- upgradeEncoding 表示是否需要對(duì)密碼進(jìn)行再次加密以使得密碼更加安全,默認(rèn)為 false。
PasswordEncoder 的實(shí)現(xiàn)類,則具體實(shí)現(xiàn)了這些方法。
2.PasswordEncoder 在哪里起作用
對(duì)于我們開(kāi)發(fā)者而言,我們通常都是在 SecurityConfig 中配置一個(gè) PasswordEncoder 的實(shí)例,類似下面這樣:
- @Bean
- PasswordEncoder passwordEncoder() {
- return new BCryptPasswordEncoder();
- }
剩下的事情,都是由系統(tǒng)調(diào)用的。今天我們就來(lái)揭開(kāi)系統(tǒng)調(diào)用的神秘面紗!我們一起來(lái)看下系統(tǒng)到底是怎么調(diào)用的!
首先,松哥在前面的文章中和大家提到過(guò),Spring Security 中,如果使用用戶名/密碼的方式登錄,密碼是在 DaoAuthenticationProvider 中進(jìn)行校驗(yàn)的,大家可以參考:SpringSecurity 自定義認(rèn)證邏輯的兩種方式(高級(jí)玩法)。
我們來(lái)看下 DaoAuthenticationProvider 中密碼是如何校驗(yàn)的:
- 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"));
- }
- }
可以看到,密碼校驗(yàn)就是通過(guò) passwordEncoder.matches 方法來(lái)完成的。
那么 DaoAuthenticationProvider 中的 passwordEncoder 從何而來(lái)呢?是不是就是我們一開(kāi)始在 SecurityConfig 中配置的那個(gè) Bean 呢?
我們來(lái)看下 DaoAuthenticationProvider 中關(guān)于 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 創(chuàng)建之時(shí),就指定了 PasswordEncoder,似乎并沒(méi)有用到我們一開(kāi)始配置的 Bean?其實(shí)不是的!在 DaoAuthenticationProvider 創(chuàng)建之時(shí),會(huì)制定一個(gè)默認(rèn)的 PasswordEncoder,如果我們沒(méi)有配置任何 PasswordEncoder,將使用這個(gè)默認(rèn)的 PasswordEncoder,如果我們自定義了 PasswordEncoder 實(shí)例,那么會(huì)使用我們自定義的 PasswordEncoder 實(shí)例!
從何而知呢?
我們?cè)賮?lái)看看 DaoAuthenticationProvider 是怎么初始化的。
DaoAuthenticationProvider 的初始化是在 InitializeUserDetailsManagerConfigurer#configure 方法中完成的,我們一起來(lái)看下該方法的定義:
- 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);
- }
從這段代碼中我們可以看到:
- 首先去調(diào)用 getBeanOrNull 方法獲取一個(gè) PasswordEncoder 實(shí)例,getBeanOrNull 方法實(shí)際上就是去 Spring 容器中查找對(duì)象。
- 接下來(lái)直接 new 一個(gè) DaoAuthenticationProvider 對(duì)象,大家知道,在 new 的過(guò)程中,DaoAuthenticationProvider 中默認(rèn)的 PasswordEncoder 已經(jīng)被創(chuàng)建出來(lái)了。
- 如果一開(kāi)始從 Spring 容器中獲取到了 PasswordEncoder 實(shí)例,則將之賦值給 DaoAuthenticationProvider 實(shí)例,否則就是用 DaoAuthenticationProvider 自己默認(rèn)創(chuàng)建的 PasswordEncoder。
至此,就真相大白了,我們配置的 PasswordEncoder 實(shí)例確實(shí)用上了。
3.默認(rèn)的是什么?
同時(shí)大家看到,如果我們不進(jìn)行任何配置,默認(rèn)的 PasswordEncoder 也會(huì)被提供,那么默認(rèn)的 PasswordEncoder 是什么呢?我們就從這個(gè)方法看起:
- public DaoAuthenticationProvider() {
- setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
- }
繼續(xù):
- 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() {}
- }
可以看到:
- 在 PasswordEncoderFactories 中,首先構(gòu)建了一個(gè) encoders,然后給所有的編碼方式都取了一個(gè)名字,再把名字做 key,編碼方式做 value,統(tǒng)統(tǒng)存入 encoders 中。
- 最后返回了一個(gè) DelegatingPasswordEncoder 實(shí)例,同時(shí)傳入默認(rèn)的 encodingId 就是 bcrypt,以及 encoders 實(shí)例,DelegatingPasswordEncoder 看名字應(yīng)該是一個(gè)代理對(duì)象。
我們來(lái)看下 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 + "\"");
- }
- }
- }
這段代碼比較長(zhǎng),我來(lái)和大家挨個(gè)解釋下:
- DelegatingPasswordEncoder 也是實(shí)現(xiàn)了 PasswordEncoder 接口,所以它里邊的核心方法也是兩個(gè):encode 方法用來(lái)對(duì)密碼進(jìn)行編碼,matches 方法用來(lái)校驗(yàn)密碼。
- 在 DelegatingPasswordEncoder 的構(gòu)造方法中,通過(guò) 通過(guò)傳入的兩個(gè)參數(shù) encodingId 和 encoders ,獲取到默認(rèn)的編碼器賦值給 passwordEncoderForEncode,默認(rèn)的編碼器實(shí)際上就是 BCryptPasswordEncoder。
- 在 encode 方法中對(duì)密碼進(jìn)行編碼,但是編碼的方式加了前綴,前綴是 {編碼器名稱} ,例如如果你使用 BCryptPasswordEncoder 進(jìn)行編碼,那么生成的密碼就類似 {bcrypt}$2a$10$oE39aG10kB/rFu2vQeCJTu/V/v4n6DRR0f8WyXRiAYvBpmadoOBE.。這樣有什么用呢?每種密碼加密之后,都會(huì)加上一個(gè)前綴,這樣看到前綴,就知道該密文是使用哪個(gè)編碼器生成的了。
- 最后 matches 方法的邏輯就很清晰了,先從密文中提取出來(lái)前綴,再根據(jù)前綴找到對(duì)應(yīng)的 PasswordEncoder,然后再調(diào)用 PasswordEncoder 的 matches 方法進(jìn)行密碼比對(duì)。
- 如果根據(jù)提取出來(lái)的前綴,找不到對(duì)應(yīng)的 PasswordEncoder,那么就會(huì)調(diào)用 UnmappedIdPasswordEncoder#matches 方法,進(jìn)行密碼比對(duì),該方法實(shí)際上并不會(huì)進(jìn)行密碼比對(duì),而是直接拋出異常。
OK,至此,相信大家都明白了 DelegatingPasswordEncoder 的工作原理了。
如果我們想同時(shí)使用多個(gè)密碼加密方案,看來(lái)使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默認(rèn)還不用配置。
4.體驗(yàn)
接下來(lái)我們稍微體驗(yàn)一下 DelegatingPasswordEncoder 的用法。
首先我們來(lái)生成三個(gè)密碼作為測(cè)試密碼:
- @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);
- }
生成結(jié)果如下:
- e1 = {bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi
- e2 = {MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2
- e3 = {noop}123
接下來(lái),我們把這三個(gè)密碼拷貝到 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("江南一點(diǎn)雨").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")
- ...
- }
- }
這里三個(gè)用戶使用三種不同的密碼加密方式。
配置完成后,重啟項(xiàng)目,分別使用 javaboy/123、sang/123 以及 江南一點(diǎn)雨/123 進(jìn)行登錄,發(fā)現(xiàn)都能登錄成功。
5.意義何在?
為什么我們會(huì)有這種需求?想在項(xiàng)目種同時(shí)存在多種密碼加密方案?其實(shí)這個(gè)主要是針對(duì)老舊項(xiàng)目改造用的,密碼加密方式一旦確定,基本上沒(méi)法再改了(你總不能讓用戶重新注冊(cè)一次吧),但是我們又想使用最新的框架來(lái)做密碼加密,那么無(wú)疑,DelegatingPasswordEncoder 是最佳選擇。
好啦,這就是今天和小伙伴們分享的多種密碼加密方案問(wèn)題,感興趣的小伙伴記得點(diǎn)個(gè)在看鼓勵(lì)下松哥哦~
原文鏈接:原文地址:https://mp.weixin.qq.com/s/8GJCqcEYW7ZGKlKXRTwGMg