黑客24小时在线接单的网站

黑客24小时在线接单的网站

Spring Security 如何实现多种加密方案共存

这篇文章中,松哥给大家介绍了两种密码加密方案,但是两种都是独立使用的!能不能在同一个项目中同时存在多种密码加密方案呢?答案是肯定的!

今天松哥就来和大家聊一聊,如何在 Spring Security 中,让多种不同的密码加密方案并存。

为什么要加密?常见的加密算法等等这些问题我就不再赘述了,大家可以参考之前的:Spring Boot 中密码加密的两种姿势!,咱们直接来看今天的正文。

1.PasswordEncoder

在 Spring Security 中,跟密码加密/校验相关的事情,都是由 PasswordEncoder 来主导的,PasswordEncoder 拥有众多的实现类:

这些实现类,有的已经过期了,有的用处不大。对于我们而言,最常用的莫过于 BCryptPasswordEncoder。

PasswordEncoder 本身是一个接口,里边只有三个方法:

  • publicinterfacePasswordEncoder{
  • Stringencode(CharSequencerawPassword);
  • booleanmatches(CharSequencerawPassword,StringencodedPassword);
  • defaultbooleanupgradeEncoding(StringencodedPassword){
  • returnfalse;
  • }
  • }
    • encode 方法用来对密码进行加密。
    • matches 方法用来对密码进行比对。
    • upgradeEncoding 表示是否需要对密码进行再次加密以使得密码更加安全,默认为 false。

    PasswordEncoder 的实现类,则具体实现了这些方法。

    2.PasswordEncoder 在哪里起作用

    对于我们开发者而言,我们通常都是在 SecurityConfig 中配置一个 PasswordEncoder 的实例,类似下面这样:

  • @Bean
  • PasswordEncoderpasswordEncoder(){
  • returnnewBCryptPasswordEncoder();
  • }
  • 剩下的事情,都是由系统调用的。今天我们就来揭开系统调用的神秘面纱!我们一起来看下系统到底是怎么调用的!

    首先,松哥在前面的文章中和大家提到过,Spring Security 中,如果使用用户名/密码的方式登录,密码是在 DaoAuthenticationProvider 中进行校验的,大家可以参考:SpringSecurity 自定义认证逻辑的两种方式(高级玩法)。

    我们来看下 DaoAuthenticationProvider 中密码是如何校验的:

  • protectedvoidadditionalAuthenticationChecks(UserDetailsuserDetails,
  • UsernamePasswordAuthenticationTokenauthentication)
  • throwsAuthenticationException{
  • if(authentication.getCredentials()==null){
  • thrownewBadCredentialsException(messages.getMessage(
  • "AbstractUserDetailsAuthenticationProvider.badCredentials",
  • "Badcredentials"));
  • }
  • StringpresentedPassword=authentication.getCredentials().toString();
  • if(!passwordEncoder.matches(presentedPassword,userDetails.getPassword())){
  • thrownewBadCredentialsException(messages.getMessage(
  • "AbstractUserDetailsAuthenticationProvider.badCredentials",
  • "Badcredentials"));
  • }
  • }
  • 可以看到,密码校验就是通过 passwordEncoder.matches 方法来完成的。

    那么 DaoAuthenticationProvider 中的 passwordEncoder 从何而来呢?是不是就是我们一开始在 SecurityConfig 中配置的那个 Bean 呢?

    我们来看下 DaoAuthenticationProvider 中关于 passwordEncoder 的定义,如下:

  • publicclassDaoAuthenticationProviderextendsAbstractUserDetailsAuthenticationProvider{
  • privatePasswordEncoderpasswordEncoder;
  • publicDaoAuthenticationProvider(){
  • setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
  • }
  • publicvoidsetPasswordEncoder(PasswordEncoderpasswordEncoder){
  • this.passwordEncoder=passwordEncoder;
  • this.userNotFoundEncodedPassword=null;
  • }
  • protectedPasswordEncodergetPasswordEncoder(){
  • returnpasswordEncoder;
  • }
  • }
  • 从这段代码中可以看到,在 DaoAuthenticationProvider 创建之时,就指定了 PasswordEncoder,似乎并没有用到我们一开始配置的 Bean?其实不是的!在 DaoAuthenticationProvider 创建之时,会制定一个默认的 PasswordEncoder,如果我们没有配置任何 PasswordEncoder,将使用这个默认的 PasswordEncoder,如果我们自定义了 PasswordEncoder 实例,那么会使用我们自定义的 PasswordEncoder 实例!

    从何而知呢?

    我们再来看看 DaoAuthenticationProvider 是怎么初始化的。

    DaoAuthenticationProvider 的初始化是在 InitializeUserDetailsManagerConfigurer#configure 方法中完成的,我们一起来看下该方法的定义:

  • publicvoidconfigure(AuthenticationManagerBuilderauth)throwsException{
  • if(auth.isConfigured()){
  • return;
  • }
  • UserDetailsServiceuserDetailsService=getBeanOrNull(
  • UserDetailsService.class);
  • if(userDetailsService==null){
  • return;
  • }
  • PasswordEncoderpasswordEncoder=getBeanOrNull(PasswordEncoder.class);
  • UserDetailsPasswordServicepasswordManager=getBeanOrNull(UserDetailsPasswordService.class);
  • DaoAuthenticationProviderprovider=newDaoAuthenticationProvider();
  • provider.setUserDetailsService(userDetailsService);
  • if(passwordEncoder!=null){
  • provider.setPasswordEncoder(passwordEncoder);
  • }
  • if(passwordManager!=null){
  • provider.setUserDetailsPasswordService(passwordManager);
  • }
  • provider.afterPropertiesSet();
  • auth.authenticationProvider(provider);
  • }
  • 从这段代码中我们可以看到:

  • 首先去调用 getBeanOrNull 方法获取一个 PasswordEncoder 实例,getBeanOrNull 方法实际上就是去 Spring 容器中查找对象。
  • 接下来直接 new 一个 DaoAuthenticationProvider 对象,大家知道,在 new 的过程中,DaoAuthenticationProvider 中默认的 PasswordEncoder 已经被创建出来了。
  • 如果一开始从 Spring 容器中获取到了 PasswordEncoder 实例,则将之赋值给 DaoAuthenticationProvider 实例,否则就是用 DaoAuthenticationProvider 自己默认创建的 PasswordEncoder。
  • 至此,就真相大白了,我们配置的 PasswordEncoder 实例确实用上了。

    3.默认的是什么?

    同时大家看到,如果我们不进行任何配置,默认的 PasswordEncoder 也会被提供,那么默认的 PasswordEncoder 是什么呢?我们就从这个方法看起:

  • publicDaoAuthenticationProvider(){
  • setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
  • }
  • 继续:

  • publicclassPasswordEncoderFactories{
  • publicstaticPasswordEncodercreateDelegatingPasswordEncoder(){
  • StringencodingId="bcrypt";
  • Map<String,PasswordEncoder>encoders=newHashMap<>();
  • encoders.put(encodingId,newBCryptPasswordEncoder());
  • encoders.put("ldap",neworg.springframework.security.crypto.password.LdapShaPasswordEncoder());
  • encoders.put("MD4",neworg.springframework.security.crypto.password.Md4PasswordEncoder());
  • encoders.put("MD5",neworg.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
  • encoders.put("noop",org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
  • encoders.put("pbkdf2",newPbkdf2PasswordEncoder());
  • encoders.put("scrypt",newSCryptPasswordEncoder());
  • encoders.put("SHA-1",neworg.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
  • encoders.put("SHA-256",neworg.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
  • encoders.put("sha256",neworg.springframework.security.crypto.password.StandardPasswordEncoder());
  • encoders.put("argon2",newArgon2PasswordEncoder());
  • returnnewDelegatingPasswordEncoder(encodingId,encoders);
  • }
  • privatePasswordEncoderFactories(){}
  • }
  • 可以看到:

  • 在 PasswordEncoderFactories 中,首先构建了一个 encoders,然后给所有的编码方式都取了一个名字,再把名字做 key,编码方式做 value,统统存入 encoders 中。
  • 最后返回了一个 DelegatingPasswordEncoder 实例,同时传入默认的 encodingId 就是 bcrypt,以及 encoders 实例,DelegatingPasswordEncoder 看名字应该是一个代理对象。
  • 我们来看下 DelegatingPasswordEncoder 的定义:

  • publicclassDelegatingPasswordEncoderimplementsPasswordEncoder{
  • privatestaticfinalStringPREFIX="{";
  • privatestaticfinalStringSUFFIX="}";
  • privatefinalStringidForEncode;
  • privatefinalPasswordEncoderpasswordEncoderForEncode;
  • privatefinalMap<String,PasswordEncoder>idToPasswordEncoder;
  • privatePasswordEncoderdefaultPasswordEncoderForMatches=newUnmappedIdPasswordEncoder();
  • publicDelegatingPasswordEncoder(StringidForEncode,
  • Map<String,PasswordEncoder>idToPasswordEncoder){
  • if(idForEncode==null){
  • thrownewIllegalArgumentException("idForEncodecannotbenull");
  • }
  • if(!idToPasswordEncoder.containsKey(idForEncode)){
  • thrownewIllegalArgumentException("idForEncode"+idForEncode+"isnotfoundinidToPasswordEncoder"+idToPasswordEncoder);
  • }
  • for(Stringid:idToPasswordEncoder.keySet()){
  • if(id==null){
  • continue;
  • }
  • if(id.contains(PREFIX)){
  • thrownewIllegalArgumentException("id"+id+"cannotcontain"+PREFIX);
  • }
  • if(id.contains(SUFFIX)){
  • thrownewIllegalArgumentException("id"+id+"cannotcontain"+SUFFIX);
  • }
  • }
  • this.idForEncode=idForEncode;
  • this.passwordEncoderForEncode=idToPasswordEncoder.get(idForEncode);
  • this.idToPasswordEncoder=newHashMap<>(idToPasswordEncoder);
  • }
  • publicvoidsetDefaultPasswordEncoderForMatches(
  • PasswordEncoderdefaultPasswordEncoderForMatches){
  • if(defaultPasswordEncoderForMatches==null){
  • thrownewIllegalArgumentException("defaultPasswordEncoderForMatchescannotbenull");
  • }
  • this.defaultPasswordEncoderForMatches=defaultPasswordEncoderForMatches;
  • }
  • @Override
  • publicStringencode(CharSequencerawPassword){
  • returnPREFIX+this.idForEncode+SUFFIX+this.passwordEncoderForEncode.encode(rawPassword);
  • }
  • @Override
  • publicbooleanmatches(CharSequencerawPassword,StringprefixEncodedPassword){
  • if(rawPassword==null&&prefixEncodedPassword==null){
  • returntrue;
  • }
  • Stringid=extractId(prefixEncodedPassword);
  • PasswordEncoderdelegate=this.idToPasswordEncoder.get(id);
  • if(delegate==null){
  • returnthis.defaultPasswordEncoderForMatches
  • .matches(rawPassword,prefixEncodedPassword);
  • }
  • StringencodedPassword=extractEncodedPassword(prefixEncodedPassword);
  • returndelegate.matches(rawPassword,encodedPassword);
  • }
  • privateStringextractId(StringprefixEncodedPassword){
  • if(prefixEncodedPassword==null){
  • returnnull;
  • }
  • intstart=prefixEncodedPassword.indexOf(PREFIX);
  • if(start!=0){
  • returnnull;
  • }
  • intend=prefixEncodedPassword.indexOf(SUFFIX,start);
  • if(end<0){
  • returnnull;
  • }
  • returnprefixEncodedPassword.substring(start+1,end);
  • }
  • @Override
  • publicbooleanupgradeEncoding(StringprefixEncodedPassword){
  • Stringid=extractId(prefixEncodedPassword);
  • if(!this.idForEncode.equalsIgnoreCase(id)){
  • returntrue;
  • }
  • else{
  • StringencodedPassword=extractEncodedPassword(prefixEncodedPassword);
  • returnthis.idToPasswordEncoder.get(id).upgradeEncoding(encodedPassword);
  • }
  • }
  • privateStringextractEncodedPassword(StringprefixEncodedPassword){
  • intstart=prefixEncodedPassword.indexOf(SUFFIX);
  • returnprefixEncodedPassword.substring(start+1);
  • }
  • privateclassUnmappedIdPasswordEncoderimplementsPasswordEncoder{
  • @Override
  • publicStringencode(CharSequencerawPassword){
  • thrownewUnsupportedOperationException("encodeisnotsupported");
  • }
  • @Override
  • publicbooleanmatches(CharSequencerawPassword,
  • StringprefixEncodedPassword){
  • Stringid=extractId(prefixEncodedPassword);
  • thrownewIllegalArgumentException("ThereisnoPasswordEncodermappedfortheid\""+id+"\"");
  • }
  • }
  • }
  • 这段代码比较长,我来和大家挨个解释下:

  • DelegatingPasswordEncoder 也是实现了 PasswordEncoder 接口,所以它里边的核心方法也是两个:encode 方法用来对密码进行编码,matches 方法用来校验密码。
  • 在 DelegatingPasswordEncoder 的构造方法中,通过 通过传入的两个参数 encodingId 和 encoders ,获取到默认的编码器赋值给 passwordEncoderForEncode,默认的编码器实际上就是 BCryptPasswordEncoder。
  • 在 encode 方法中对密码进行编码,但是编码的方式加了前缀,前缀是 {编码器名称} ,例如如果你使用 BCryptPasswordEncoder 进行编码,那么生成的密码就类似 {bcrypt}$2a$10$oE39aG10kB/rFu2vQeCJTu/V/v4n6DRR0f8WyXRiAYvBpmadoOBE.。这样有什么用呢?每种密码加密之后,都会加上一个前缀,这样看到前缀,就知道该密文是使用哪个编码器生成的了。
  • 最后 matches 方法的逻辑就很清晰了,先从密文中提取出来前缀,再根据前缀找到对应的 PasswordEncoder,然后再调用 PasswordEncoder 的 matches 方法进行密码比对。
  • 如果根据提取出来的前缀,找不到对应的 PasswordEncoder,那么就会调用 UnmappedIdPasswordEncoder#matches 方法,进行密码比对,该方法实际上并不会进行密码比对,而是直接抛出异常。
  • OK,至此,相信大家都明白了 DelegatingPasswordEncoder 的工作原理了。

    如果我们想同时使用多个密码加密方案,看来使用 DelegatingPasswordEncoder 就可以了,而 DelegatingPasswordEncoder 默认还不用配置。

    4.体验

    接下来我们稍微体验一下 DelegatingPasswordEncoder 的用法。

    首先我们来生成三个密码作为测试密码:

  • @Test
  • voidcontextLoads(){
  • Map<String,PasswordEncoder>encoders=newHashMap<>();
  • encoders.put("bcrypt",newBCryptPasswordEncoder());
  • encoders.put("MD5",neworg.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
  • encoders.put("noop",org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
  • DelegatingPasswordEncoderencoder1=newDelegatingPasswordEncoder("bcrypt",encoders);
  • DelegatingPasswordEncoderencoder2=newDelegatingPasswordEncoder("MD5",encoders);
  • DelegatingPasswordEncoderencoder3=newDelegatingPasswordEncoder("noop",encoders);
  • Stringe1=encoder1.encode("123");
  • Stringe2=encoder2.encode("123");
  • Stringe3=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")
  • publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{
  • @Override
  • @Bean
  • protectedUserDetailsServiceuserDetailsService(){
  • InMemoryUserDetailsManagermanager=newInMemoryUserDetailsManager();
  • 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());
  • returnmanager;
  • }
  • @Override
  • protectedvoidconfigure(HttpSecurityhttp)throwsException{
  • http.authorizeRequests()
  • .antMatchers("/admin/**").hasRole("admin")
  • .antMatchers("/user/**").hasRole("user")
  • ...
  • }
  • }
  • 这里三个用户使用三种不同的密码加密方式。

    配置完成后,重启项目,分别使用 javaboy/123、sang/123 以及 江南一点雨/123 进行登录,发现都能登录成功。

    5.意义何在?

    为什么我们会有这种需求?想在项目种同时存在多种密码加密方案?其实这个主要是针对老旧项目改造用的,密码加密方式一旦确定,基本上没法再改了(你总不能让用户重新注册一次吧),但是我们又想使用最新的框架来做密码加密,那么无疑,DelegatingPasswordEncoder 是最佳选择。

    好啦,这就是今天和小伙伴们分享的多种密码加密方案问题,感兴趣的小伙伴记得点个在看鼓励下松哥哦~

    本文转载自微信公众号「江南一点雨」,可以通过以下二维码关注。转载本文请联系江南一点雨公众号。

    • 评论列表:
    •  森槿雾敛
       发布于 2022-06-17 04:57:36  回复该评论
    • enticationProvider 自己默认创建的 PasswordEncoder。至此,就真相大白了,我们配置的 PasswordEncoder 实例确实用上了。3.默认的是什
    •  北槐謓念
       发布于 2022-06-17 01:44:22  回复该评论
    • eisnoPasswordEncodermappedfortheid\""+id+"\"");}}}这段代码比较长,我来和大家挨个解释下: DelegatingPasswordEncoder 也是实现了 PasswordEncoder 接口,所以它里边的核心方法也是两个:e

    发表评论:

    Powered By

    Copyright Your WebSite.Some Rights Reserved.