偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

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

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

[[415547]]

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

今天松哥就來和大家聊一聊,如何在 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 本身是一個接口,里邊只有三個方法:

  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 中配置一個 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 中配置的那個 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)建之時,就指定了 PasswordEncoder,似乎并沒有用到我們一開始配置的 Bean?其實(shí)不是的!在 DaoAuthenticationProvider 創(chuàng)建之時,會制定一個默認(rèn)的 PasswordEncoder,如果我們沒有配置任何 PasswordEncoder,將使用這個默認(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 方法獲取一個 PasswordEncoder 實(shí)例,getBeanOrNull 方法實(shí)際上就是去 Spring 容器中查找對象。
  2. 接下來直接 new 一個 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)的是什么?

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

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

我們來看下 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.  } 

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

  1. DelegatingPasswordEncoder 也是實(shí)現(xiàn)了 PasswordEncoder 接口,所以它里邊的核心方法也是兩個:encode 方法用來對密碼進(jìn)行編碼,matches 方法用來校驗(yàn)密碼。
  2. 在 DelegatingPasswordEncoder 的構(gòu)造方法中,通過 通過傳入的兩個參數(shù) encodingId 和 encoders ,獲取到默認(rèn)的編碼器賦值給 passwordEncoderForEncode,默認(rèn)的編碼器實(shí)際上就是 BCryptPasswordEncoder。
  3. 在 encode 方法中對密碼進(jìn)行編碼,但是編碼的方式加了前綴,前綴是 {編碼器名稱} ,例如如果你使用 BCryptPasswordEncoder 進(jìn)行編碼,那么生成的密碼就類似 {bcrypt}$2a$10$oE39aG10kB/rFu2vQeCJTu/V/v4n6DRR0f8WyXRiAYvBpmadoOBE.。這樣有什么用呢?每種密碼加密之后,都會加上一個前綴,這樣看到前綴,就知道該密文是使用哪個編碼器生成的了。
  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 的工作原理了。

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

4.體驗(yàn)

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

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

  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 

接下來,我們把這三個密碼拷貝到 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.     } 

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

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

5.意義何在?

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

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

本文轉(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ù)

2021-07-12 12:20:08

Spring初始化方案

2024-10-18 08:00:00

SpringBoot框架開發(fā)

2020-10-25 09:04:46

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

2010-04-09 14:47:13

Windows7Ubuntu

2022-05-19 11:29:14

計(jì)時攻擊SpringSecurity

2021-08-29 18:36:57

項(xiàng)目

2021-04-23 07:33:10

SpringSecurity單元

2024-10-15 16:41:35

2009-06-17 13:53:57

Spring.jar

2025-03-05 07:58:30

2021-05-31 07:18:46

SpringSecurity信息

2009-05-18 17:16:50

點(diǎn)贊
收藏

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