成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Spring Security 如何實現多種加密方案共存

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

[[415547]]

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

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

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

1.PasswordEncoder

在 Spring Security 中,跟密碼加密/校驗相關的事情,都是由 PasswordEncoder 來主導的,PasswordEncoder 擁有眾多的實現類:

這些實現類,有的已經過期了,有的用處不大。對于我們而言,最常用的莫過于 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 方法用來對密碼進行加密。
  • matches 方法用來對密碼進行比對。
  • upgradeEncoding 表示是否需要對密碼進行再次加密以使得密碼更加安全,默認為 false。

PasswordEncoder 的實現類,則具體實現了這些方法。

2.PasswordEncoder 在哪里起作用

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

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

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

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

我們來看下 DaoAuthenticationProvider 中密碼是如何校驗的:

  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.  } 

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

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

我們來看下 DaoAuthenticationProvider 中關于 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 創建之時,就指定了 PasswordEncoder,似乎并沒有用到我們一開始配置的 Bean?其實不是的!在 DaoAuthenticationProvider 創建之時,會制定一個默認的 PasswordEncoder,如果我們沒有配置任何 PasswordEncoder,將使用這個默認的 PasswordEncoder,如果我們自定義了 PasswordEncoder 實例,那么會使用我們自定義的 PasswordEncoder 實例!

從何而知呢?

我們再來看看 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. 首先去調用 getBeanOrNull 方法獲取一個 PasswordEncoder 實例,getBeanOrNull 方法實際上就是去 Spring 容器中查找對象。
  2. 接下來直接 new 一個 DaoAuthenticationProvider 對象,大家知道,在 new 的過程中,DaoAuthenticationProvider 中默認的 PasswordEncoder 已經被創建出來了。
  3. 如果一開始從 Spring 容器中獲取到了 PasswordEncoder 實例,則將之賦值給 DaoAuthenticationProvider 實例,否則就是用 DaoAuthenticationProvider 自己默認創建的 PasswordEncoder。

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

3.默認的是什么?

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

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

繼續:

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

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

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

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

4.體驗

接下來我們稍微體驗一下 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); 

生成結果如下:

  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("江南一點雨").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.     } 

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

配置完成后,重啟項目,分別使用 javaboy/123、sang/123 以及 江南一點雨/123 進行登錄,發現都能登錄成功。

5.意義何在?

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

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

本文轉載自微信公眾號「江南一點雨」,可以通過以下二維碼關注。轉載本文請聯系江南一點雨公眾號。

 

責任編輯:武曉燕 來源: 江南一點雨
相關推薦

2022-06-16 10:38:24

URL權限源代碼

2021-12-28 11:13:05

安全認證 Spring Boot

2009-11-03 14:19:53

2021-04-28 06:26:11

Spring Secu功能實現源碼分析

2020-09-16 08:07:54

權限粒度Spring Secu

2021-05-31 10:47:17

SpringSecuritySession

2022-06-04 12:25:10

解密加密過濾器

2021-03-09 13:18:53

加密解密參數

2021-07-12 12:20:08

Spring初始化方案

2024-10-18 08:00:00

SpringBoot框架開發

2020-10-25 09:04:46

數據加密數據泄露攻擊

2010-04-09 14:47:13

Windows7Ubuntu

2022-05-19 11:29:14

計時攻擊SpringSecurity

2021-04-23 07:33:10

SpringSecurity單元

2021-08-29 18:36:57

項目

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

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 欧美一级视频在线观看 | 亚洲成av人片在线观看 | 久色一区 | 日本高清aⅴ毛片免费 | 国产探花在线精品一区二区 | 亚洲电影免费 | 国产一区二区影院 | 综合色在线 | 欧美性一区二区三区 | 中文字幕国产视频 | 欧美日本韩国一区二区三区 | 91久久久久久久久久久久久 | 欧美精品a∨在线观看不卡 欧美日韩中文字幕在线播放 | 激情视频一区 | 一区二区在线免费观看 | 天天精品综合 | 91视频在线 | 日韩一区二区免费视频 | 国产成人免费视频网站视频社区 | av香蕉 | 国产精品日韩一区 | 久国久产久精永久网页 | 欧美涩涩网| 天天综合成人网 | 亚洲欧美日韩在线 | 在线观看h视频 | 九九热免费在线观看 | www.国产 | 国产一级视频 | 欧美综合一区二区三区 | 岛国av免费在线观看 | 国产精品一区二区三区在线 | 日韩三区在线 | 中文字幕黄色大片 | 视频一二三区 | 精品国产精品国产偷麻豆 | 国产在线一区二区三区 | 国产性生活一级片 | 成人在线中文字幕 | 一级在线观看 | 成人在线观看免费视频 |