自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Spring Security 如何實(shí)現(xiàn)多種加密方案共存

安全 數(shù)據(jù)安全
這篇文章中,松哥給大家介紹了兩種密碼加密方案,但是兩種都是獨(dú)立使用的!能不能在同一個(gè)項(xiàng)目中同時(shí)存在多種密碼加密方案呢?答案是肯定的!

[[415547]]

這篇文章中,松哥給大家介紹了兩種密碼加密方案,但是兩種都是獨(dú)立使用的!能不能在同一個(gè)項(xiàng)目中同時(shí)存在多種密碼加密方案呢?答案是肯定的!

今天松哥就來和大家聊一聊,如何在 Spring Security 中,讓多種不同的密碼加密方案并存。

為什么要加密?常見的加密算法等等這些問題我就不再贅述了,大家可以參考之前的:Spring Boot 中密碼加密的兩種姿勢!,咱們直接來看今天的正文。

1.PasswordEncoder

在 Spring Security 中,跟密碼加密/校驗(yàn)相關(guān)的事情,都是由 PasswordEncoder 來主導(dǎo)的,PasswordEncoder 擁有眾多的實(shí)現(xiàn)類:

這些實(shí)現(xiàn)類,有的已經(jīng)過期了,有的用處不大。對于我們而言,最常用的莫過于 BCryptPasswordEncoder。

PasswordEncoder 本身是一個(gè)接口,里邊只有三個(gè)方法:

  1. public interface PasswordEncoder { 
  2.  String encode(CharSequence rawPassword); 
  3.  boolean matches(CharSequence rawPassword, String encodedPassword); 
  4.  default boolean upgradeEncoding(String encodedPassword) { 
  5.   return false
  6.  } 
  • encode 方法用來對密碼進(jìn)行加密。
  • matches 方法用來對密碼進(jìn)行比對。
  • upgradeEncoding 表示是否需要對密碼進(jìn)行再次加密以使得密碼更加安全,默認(rèn)為 false。

PasswordEncoder 的實(shí)現(xiàn)類,則具體實(shí)現(xiàn)了這些方法。

2.PasswordEncoder 在哪里起作用

對于我們開發(fā)者而言,我們通常都是在 SecurityConfig 中配置一個(gè) PasswordEncoder 的實(shí)例,類似下面這樣:

  1. @Bean 
  2. PasswordEncoder passwordEncoder() { 
  3.     return new BCryptPasswordEncoder(); 

剩下的事情,都是由系統(tǒng)調(diào)用的。今天我們就來揭開系統(tǒng)調(diào)用的神秘面紗!我們一起來看下系統(tǒng)到底是怎么調(diào)用的!

首先,松哥在前面的文章中和大家提到過,Spring Security 中,如果使用用戶名/密碼的方式登錄,密碼是在 DaoAuthenticationProvider 中進(jìn)行校驗(yàn)的,大家可以參考:SpringSecurity 自定義認(rèn)證邏輯的兩種方式(高級玩法)。

我們來看下 DaoAuthenticationProvider 中密碼是如何校驗(yàn)的:

  1. protected void additionalAuthenticationChecks(UserDetails userDetails, 
  2.   UsernamePasswordAuthenticationToken authentication) 
  3.   throws AuthenticationException { 
  4.  if (authentication.getCredentials() == null) { 
  5.   throw new BadCredentialsException(messages.getMessage( 
  6.     "AbstractUserDetailsAuthenticationProvider.badCredentials"
  7.     "Bad credentials")); 
  8.  } 
  9.  String presentedPassword = authentication.getCredentials().toString(); 
  10.  if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { 
  11.   throw new BadCredentialsException(messages.getMessage( 
  12.     "AbstractUserDetailsAuthenticationProvider.badCredentials"
  13.     "Bad credentials")); 
  14.  } 

可以看到,密碼校驗(yàn)就是通過 passwordEncoder.matches 方法來完成的。

那么 DaoAuthenticationProvider 中的 passwordEncoder 從何而來呢?是不是就是我們一開始在 SecurityConfig 中配置的那個(gè) Bean 呢?

我們來看下 DaoAuthenticationProvider 中關(guān)于 passwordEncoder 的定義,如下:

  1. public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { 
  2.  private PasswordEncoder passwordEncoder; 
  3.  public DaoAuthenticationProvider() { 
  4.   setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); 
  5.  } 
  6.  public void setPasswordEncoder(PasswordEncoder passwordEncoder) { 
  7.   this.passwordEncoder = passwordEncoder; 
  8.   this.userNotFoundEncodedPassword = null
  9.  } 
  10.  
  11.  protected PasswordEncoder getPasswordEncoder() { 
  12.   return passwordEncoder; 
  13.  } 

從這段代碼中可以看到,在 DaoAuthenticationProvider 創(chuàng)建之時(shí),就指定了 PasswordEncoder,似乎并沒有用到我們一開始配置的 Bean?其實(shí)不是的!在 DaoAuthenticationProvider 創(chuàng)建之時(shí),會制定一個(gè)默認(rèn)的 PasswordEncoder,如果我們沒有配置任何 PasswordEncoder,將使用這個(gè)默認(rèn)的 PasswordEncoder,如果我們自定義了 PasswordEncoder 實(shí)例,那么會使用我們自定義的 PasswordEncoder 實(shí)例!

從何而知呢?

我們再來看看 DaoAuthenticationProvider 是怎么初始化的。

DaoAuthenticationProvider 的初始化是在 InitializeUserDetailsManagerConfigurer#configure 方法中完成的,我們一起來看下該方法的定義:

  1. public void configure(AuthenticationManagerBuilder auth) throws Exception { 
  2.  if (auth.isConfigured()) { 
  3.   return
  4.  } 
  5.  UserDetailsService userDetailsService = getBeanOrNull( 
  6.    UserDetailsService.class); 
  7.  if (userDetailsService == null) { 
  8.   return
  9.  } 
  10.  PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class); 
  11.  UserDetailsPasswordService passwordManager = getBeanOrNull(UserDetailsPasswordService.class); 
  12.  DaoAuthenticationProvider provider = new DaoAuthenticationProvider(); 
  13.  provider.setUserDetailsService(userDetailsService); 
  14.  if (passwordEncoder != null) { 
  15.   provider.setPasswordEncoder(passwordEncoder); 
  16.  } 
  17.  if (passwordManager != null) { 
  18.   provider.setUserDetailsPasswordService(passwordManager); 
  19.  } 
  20.  provider.afterPropertiesSet(); 
  21.  auth.authenticationProvider(provider); 

從這段代碼中我們可以看到:

  1. 首先去調(diào)用 getBeanOrNull 方法獲取一個(gè) PasswordEncoder 實(shí)例,getBeanOrNull 方法實(shí)際上就是去 Spring 容器中查找對象。
  2. 接下來直接 new 一個(gè) DaoAuthenticationProvider 對象,大家知道,在 new 的過程中,DaoAuthenticationProvider 中默認(rèn)的 PasswordEncoder 已經(jīng)被創(chuàng)建出來了。
  3. 如果一開始從 Spring 容器中獲取到了 PasswordEncoder 實(shí)例,則將之賦值給 DaoAuthenticationProvider 實(shí)例,否則就是用 DaoAuthenticationProvider 自己默認(rèn)創(chuàng)建的 PasswordEncoder。

至此,就真相大白了,我們配置的 PasswordEncoder 實(shí)例確實(shí)用上了。

3.默認(rèn)的是什么?

同時(shí)大家看到,如果我們不進(jìn)行任何配置,默認(rèn)的 PasswordEncoder 也會被提供,那么默認(rèn)的 PasswordEncoder 是什么呢?我們就從這個(gè)方法看起:

  1. public DaoAuthenticationProvider() { 
  2.  setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()); 

繼續(xù):

  1. public class PasswordEncoderFactories { 
  2.  public static PasswordEncoder createDelegatingPasswordEncoder() { 
  3.   String encodingId = "bcrypt"
  4.   Map<String, PasswordEncoder> encoders = new HashMap<>(); 
  5.   encoders.put(encodingId, new BCryptPasswordEncoder()); 
  6.   encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); 
  7.   encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder()); 
  8.   encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); 
  9.   encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); 
  10.   encoders.put("pbkdf2", new Pbkdf2PasswordEncoder()); 
  11.   encoders.put("scrypt", new SCryptPasswordEncoder()); 
  12.   encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1")); 
  13.   encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256")); 
  14.   encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder()); 
  15.   encoders.put("argon2", new Argon2PasswordEncoder()); 
  16.  
  17.   return new DelegatingPasswordEncoder(encodingId, encoders); 
  18.  } 
  19.  
  20.  private PasswordEncoderFactories() {} 

可以看到:

  1. 在 PasswordEncoderFactories 中,首先構(gòu)建了一個(gè) encoders,然后給所有的編碼方式都取了一個(gè)名字,再把名字做 key,編碼方式做 value,統(tǒng)統(tǒng)存入 encoders 中。
  2. 最后返回了一個(gè) DelegatingPasswordEncoder 實(shí)例,同時(shí)傳入默認(rèn)的 encodingId 就是 bcrypt,以及 encoders 實(shí)例,DelegatingPasswordEncoder 看名字應(yīng)該是一個(gè)代理對象。

我們來看下 DelegatingPasswordEncoder 的定義:

  1. public class DelegatingPasswordEncoder implements PasswordEncoder { 
  2.  private static final String PREFIX = "{"
  3.  private static final String SUFFIX = "}"
  4.  private final String idForEncode; 
  5.  private final PasswordEncoder passwordEncoderForEncode; 
  6.  private final Map<String, PasswordEncoder> idToPasswordEncoder; 
  7.  private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder(); 
  8.  public DelegatingPasswordEncoder(String idForEncode, 
  9.   Map<String, PasswordEncoder> idToPasswordEncoder) { 
  10.   if (idForEncode == null) { 
  11.    throw new IllegalArgumentException("idForEncode cannot be null"); 
  12.   } 
  13.   if (!idToPasswordEncoder.containsKey(idForEncode)) { 
  14.    throw new IllegalArgumentException("idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder); 
  15.   } 
  16.   for (String id : idToPasswordEncoder.keySet()) { 
  17.    if (id == null) { 
  18.     continue
  19.    } 
  20.    if (id.contains(PREFIX)) { 
  21.     throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX); 
  22.    } 
  23.    if (id.contains(SUFFIX)) { 
  24.     throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX); 
  25.    } 
  26.   } 
  27.   this.idForEncode = idForEncode; 
  28.   this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode); 
  29.   this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder); 
  30.  } 
  31.  public void setDefaultPasswordEncoderForMatches( 
  32.   PasswordEncoder defaultPasswordEncoderForMatches) { 
  33.   if (defaultPasswordEncoderForMatches == null) { 
  34.    throw new IllegalArgumentException("defaultPasswordEncoderForMatches cannot be null"); 
  35.   } 
  36.   this.defaultPasswordEncoderForMatches = defaultPasswordEncoderForMatches; 
  37.  } 
  38.  
  39.  @Override 
  40.  public String encode(CharSequence rawPassword) { 
  41.   return PREFIX + this.idForEncode + SUFFIX + this.passwordEncoderForEncode.encode(rawPassword); 
  42.  } 
  43.  
  44.  @Override 
  45.  public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) { 
  46.   if (rawPassword == null && prefixEncodedPassword == null) { 
  47.    return true
  48.   } 
  49.   String id = extractId(prefixEncodedPassword); 
  50.   PasswordEncoder delegate = this.idToPasswordEncoder.get(id); 
  51.   if (delegate == null) { 
  52.    return this.defaultPasswordEncoderForMatches 
  53.     .matches(rawPassword, prefixEncodedPassword); 
  54.   } 
  55.   String encodedPassword = extractEncodedPassword(prefixEncodedPassword); 
  56.   return delegate.matches(rawPassword, encodedPassword); 
  57.  } 
  58.  
  59.  private String extractId(String prefixEncodedPassword) { 
  60.   if (prefixEncodedPassword == null) { 
  61.    return null
  62.   } 
  63.   int start = prefixEncodedPassword.indexOf(PREFIX); 
  64.   if (start != 0) { 
  65.    return null
  66.   } 
  67.   int end = prefixEncodedPassword.indexOf(SUFFIX, start); 
  68.   if (end < 0) { 
  69.    return null
  70.   } 
  71.   return prefixEncodedPassword.substring(start + 1, end); 
  72.  } 
  73.  
  74.  @Override 
  75.  public boolean upgradeEncoding(String prefixEncodedPassword) { 
  76.   String id = extractId(prefixEncodedPassword); 
  77.   if (!this.idForEncode.equalsIgnoreCase(id)) { 
  78.    return true
  79.   } 
  80.   else { 
  81.    String encodedPassword = extractEncodedPassword(prefixEncodedPassword); 
  82.    return this.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword); 
  83.   } 
  84.  } 
  85.  
  86.  private String extractEncodedPassword(String prefixEncodedPassword) { 
  87.   int start = prefixEncodedPassword.indexOf(SUFFIX); 
  88.   return prefixEncodedPassword.substring(start + 1); 
  89.  } 
  90.  private class UnmappedIdPasswordEncoder implements PasswordEncoder { 
  91.  
  92.   @Override 
  93.   public String encode(CharSequence rawPassword) { 
  94.    throw new UnsupportedOperationException("encode is not supported"); 
  95.   } 
  96.  
  97.   @Override 
  98.   public boolean matches(CharSequence rawPassword, 
  99.    String prefixEncodedPassword) { 
  100.    String id = extractId(prefixEncodedPassword); 
  101.    throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\""); 
  102.   } 
  103.  } 

這段代碼比較長,我來和大家挨個(gè)解釋下:

  1. DelegatingPasswordEncoder 也是實(shí)現(xiàn)了 PasswordEncoder 接口,所以它里邊的核心方法也是兩個(gè):encode 方法用來對密碼進(jìn)行編碼,matches 方法用來校驗(yàn)密碼。
  2. 在 DelegatingPasswordEncoder 的構(gòu)造方法中,通過 通過傳入的兩個(gè)參數(shù) encodingId 和 encoders ,獲取到默認(rèn)的編碼器賦值給 passwordEncoderForEncode,默認(rèn)的編碼器實(shí)際上就是 BCryptPasswordEncoder。
  3. 在 encode 方法中對密碼進(jìn)行編碼,但是編碼的方式加了前綴,前綴是 {編碼器名稱} ,例如如果你使用 BCryptPasswordEncoder 進(jìn)行編碼,那么生成的密碼就類似 {bcrypt}$2a$10$oE39aG10kB/rFu2vQeCJTu/V/v4n6DRR0f8WyXRiAYvBpmadoOBE.。這樣有什么用呢?每種密碼加密之后,都會加上一個(gè)前綴,這樣看到前綴,就知道該密文是使用哪個(gè)編碼器生成的了。
  4. 最后 matches 方法的邏輯就很清晰了,先從密文中提取出來前綴,再根據(jù)前綴找到對應(yīng)的 PasswordEncoder,然后再調(diào)用 PasswordEncoder 的 matches 方法進(jìn)行密碼比對。
  5. 如果根據(jù)提取出來的前綴,找不到對應(yīng)的 PasswordEncoder,那么就會調(diào)用 UnmappedIdPasswordEncoder#matches 方法,進(jìn)行密碼比對,該方法實(shí)際上并不會進(jìn)行密碼比對,而是直接拋出異常。

OK,至此,相信大家都明白了 DelegatingPasswordEncoder 的工作原理了。

如果我們想同時(shí)使用多個(gè)密碼加密方案,看來使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默認(rèn)還不用配置。

4.體驗(yàn)

接下來我們稍微體驗(yàn)一下 DelegatingPasswordEncoder 的用法。

首先我們來生成三個(gè)密碼作為測試密碼:

  1. @Test 
  2. void contextLoads() { 
  3.     Map<String, PasswordEncoder> encoders = new HashMap<>(); 
  4.     encoders.put("bcrypt", new BCryptPasswordEncoder()); 
  5.     encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5")); 
  6.     encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); 
  7.     DelegatingPasswordEncoder encoder1 = new DelegatingPasswordEncoder("bcrypt", encoders); 
  8.     DelegatingPasswordEncoder encoder2 = new DelegatingPasswordEncoder("MD5", encoders); 
  9.     DelegatingPasswordEncoder encoder3 = new DelegatingPasswordEncoder("noop", encoders); 
  10.     String e1 = encoder1.encode("123"); 
  11.     String e2 = encoder2.encode("123"); 
  12.     String e3 = encoder3.encode("123"); 
  13.     System.out.println("e1 = " + e1); 
  14.     System.out.println("e2 = " + e2); 
  15.     System.out.println("e3 = " + e3); 

生成結(jié)果如下:

  1. e1 = {bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi 
  2. e2 = {MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2 
  3. e3 = {noop}123 

接下來,我們把這三個(gè)密碼拷貝到 SecurityConfig 中去:

  1. @Configuration("aaa"
  2. public class SecurityConfig extends WebSecurityConfigurerAdapter { 
  3.  
  4.     @Override 
  5.     @Bean 
  6.     protected UserDetailsService userDetailsService() { 
  7.  
  8.         InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager(); 
  9.         manager.createUser(User.withUsername("javaboy").password("{bcrypt}$2a$10$Sb1gAUH4wwazfNiqflKZve4Ubh.spJcxgHG8Cp29DeGya5zsHENqi").roles("admin").build()); 
  10.         manager.createUser(User.withUsername("sang").password("{noop}123").roles("admin").build()); 
  11.         manager.createUser(User.withUsername("江南一點(diǎn)雨").password("{MD5}{Wucj/L8wMTMzFi3oBKWsETNeXbMFaHZW9vCK9mahMHc=}4d43db282b36d7f0421498fdc693f2a2").roles("user").build()); 
  12.         return manager; 
  13.     } 
  14.  
  15.     @Override 
  16.     protected void configure(HttpSecurity http) throws Exception { 
  17.         http.authorizeRequests() 
  18.                 .antMatchers("/admin/**").hasRole("admin"
  19.                 .antMatchers("/user/**").hasRole("user"
  20.                 ... 
  21.     } 

這里三個(gè)用戶使用三種不同的密碼加密方式。

配置完成后,重啟項(xiàng)目,分別使用 javaboy/123、sang/123 以及 江南一點(diǎn)雨/123 進(jìn)行登錄,發(fā)現(xiàn)都能登錄成功。

5.意義何在?

為什么我們會有這種需求?想在項(xiàng)目種同時(shí)存在多種密碼加密方案?其實(shí)這個(gè)主要是針對老舊項(xiàng)目改造用的,密碼加密方式一旦確定,基本上沒法再改了(你總不能讓用戶重新注冊一次吧),但是我們又想使用最新的框架來做密碼加密,那么無疑,DelegatingPasswordEncoder 是最佳選擇。

好啦,這就是今天和小伙伴們分享的多種密碼加密方案問題,感興趣的小伙伴記得點(diǎn)個(gè)在看鼓勵下松哥哦~

本文轉(zhuǎn)載自微信公眾號「江南一點(diǎn)雨」,可以通過以下二維碼關(guān)注。轉(zhuǎn)載本文請聯(lián)系江南一點(diǎn)雨公眾號。

 

責(zé)任編輯:武曉燕 來源: 江南一點(diǎn)雨
相關(guān)推薦

2022-06-16 10:38:24

URL權(quán)限源代碼

2021-12-28 11:13:05

安全認(rèn)證 Spring Boot

2009-11-03 14:19:53

2021-04-28 06:26:11

Spring Secu功能實(shí)現(xiàn)源碼分析

2020-09-16 08:07:54

權(quán)限粒度Spring Secu

2021-05-31 10:47:17

SpringSecuritySession

2022-06-04 12:25:10

解密加密過濾器

2021-03-09 13:18:53

加密解密參數(shù)

2024-10-18 08:00:00

SpringBoot框架開發(fā)

2021-07-12 12:20:08

Spring初始化方案

2010-04-09 14:47:13

Windows7Ubuntu

2022-05-19 11:29:14

計(jì)時(shí)攻擊SpringSecurity

2020-10-25 09:04:46

數(shù)據(jù)加密數(shù)據(jù)泄露攻擊

2021-04-23 07:33:10

SpringSecurity單元

2021-08-29 18:36:57

項(xiàng)目

2024-10-15 16:41:35

2009-06-17 13:53:57

Spring.jar

2021-05-31 07:18:46

SpringSecurity信息

2025-03-05 07:58:30

2009-05-18 17:16:50

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號