Spring Security如何实现升级密码加密方式详解

软件发布|下载排行|最新软件

当前位置:首页IT学院IT技术

Spring Security如何实现升级密码加密方式详解

bangiao   2023-02-02 我要评论

本章内容

  • 密码加密方式怎么升级?
  • spring security底层怎么实现的密码加密方式升级?

密码加密方式怎么升级?

前面我们学过DelegatingPasswordEncoder类,但是不清楚他到底是做什么的,我也没讲的很清楚。所以呢,我们就重新再讲一讲它的另一个实际应用。

小明呢,有一天在刷新闻。突然收到了一篇关于MD5加密存在重大漏洞的报告, 而最佳的代替加密方案是BCrypt。此时小明慌了。

因为他项目里面就是用着MD5加密。那现在怎么办呢?小明的用户体量比较大,你不可能叫客户/程序员一个个去改是吧?

spring security就提供了一种这种情况的解决方案。

在用户登录你的账户时,自动的升级您的密码加密方式。比如说从MD5加密方式变成BCrypt

但是呢,这种方式有一个前提。您数据库的用户密码必须要有ID,也就是花括号的那一部分{noop}123456

当然如果花括号没有,然后数据体量就比较大,你只能重写DelegatingPasswordEncoder

抄代码的地方就在PasswordEncoderFactories#createDelegatingPasswordEncoder, 也就是你数据库中的密码没有花括号部分(拿不到ID)的情况下, 使用BCryptPasswordEncoder

小白: "那在spring security中哪一部分定义了这项功能?"

升级方案源码

首先我们得思考。什么情况下才会进行密码升级?

按照常理来说,应该是在用户登录成功之后进行密码升级。所以我们在找源码的时候,应该先去找认证成功的那部分源码,绝对能找到这部分功能。

我第一反应找UsernamePasswordAuthenticationFilterAbstractAuthenticationProcessingFilter抽象类的doFilter方法

但是不幸的是这里找不到我们想要的功能。所以我立即反应起来这项功能应该是在认证器这边。

DaoAuthenticationProviderAbstractUserDetailsAuthenticationProvider

找到的认证成功之后,他执行的一段函数。可以明显的看出有更新密码的过程。

这里只要保证upgradeEncoding == true,那么就可以进入更新密码的过程。

这里我们看到了一段代码this.userDetailsPasswordService, 可以百分百确定,我们的功能就在这个接口里面。

至于if的另一个函数upgradeEncoding, 你只要知道用户输入密码和数据库密码ID不同就为 true, 相同就为 false, 当然还有ID相同不同长度的解决方案, 这里就不细谈了

public interface UserDetailsPasswordService {
   UserDetails updatePassword(UserDetails user, String newPassword);
}

如果你英文能力比较强的话,可以直接去查看这个接口上面就会有注释,内容就是修改用户名的密码就这么简单。

既然已经知道这个接口的存在了,那现在的问题是怎么让spring security调用我们所实现的这个接口呢?

我现在罗列出三张图片。就可以从这三张图片中总结出三种加载我们实现类的方法。

实战

第一种方式: Spring Bean

public class UserService1 implements UserDetailsService {
	@Resource
	private UsersMapper usersMapper;
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		Optional<Users> optionalUsers = Optional.ofNullable(usersMapper.loadUserByUsername(username));
		return optionalUsers.orElseThrow(() -> new UsernameNotFoundException("找不到用户名"));
	}
}
@Bean
public UserService1 userService1() throws Exception {
    return new UserService1();
}

这种方式对应着上面第3张图。

那现在就会有人问的。我并没有写出从MD5加密方式升级到BCrypt加密方式。他是怎么自动升级到BCrypt加密方式的?

带着问题看源码

他是怎么自动升级到BCrypt加密方式的?

我们知道spring security里面默认使用的PasswordEncoder是这样的。

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

不知道当做知道哈

他们内部的源码是这样的。

public static PasswordEncoder createDelegatingPasswordEncoder() {
    // 省略了一堆代码
   String encodingId = "bcrypt";
   Map<String, PasswordEncoder> encoders = new HashMap<>();
   encoders.put(encodingId, new BCryptPasswordEncoder());
   encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
   encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
   return new DelegatingPasswordEncoder(encodingId, encoders);
}

嗯,你要注意这几行代码。

String encodingId = "bcrypt";
encoders.put(encodingId, new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);

别的什么都不看,只看encodingId变量。我们现在进入DelegatingPasswordEncoder的内部看看他的构造函数。

public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder,
      String idPrefix, String idSuffix) {
    // 省略一堆代码
   this.idForEncode = idForEncode;
   this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
   this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
   this.idPrefix = idPrefix;
   this.idSuffix = idSuffix;
}

encodingId在这个类中被叫做idForEncode

了解了这个之后,再关注这几行代码。

this.idForEncode = idForEncode;
this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);

是不是相当于

this.idForEncode = "bcrypt";
this.passwordEncoderForEncode = new BCryptPasswordEncoder();

我们再回到这里看红框框的这行代码。

@Override
public String encode(CharSequence rawPassword) {
   return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}

现在你对比一下这个函数跟前面构造函数的名字看看。

构造函数的变量叫 idForEncode , encode函数也叫 idForEncode , 前面的构造函数,我们发现这个变量其实已经被保存在DelegatingPasswordEncoder类里面了。而且值还是"bcrypt"

而构造函数里面this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode)

idToPasswordEncoder就是个Map, k是每种加密对象的id, v是每种加密算法

比如: key = "bcrypt", 那么 value = "BCryptPasswordEncoder"

所以 idToPasswordEncoderencode 函数时, 是BCryptPasswordEncoder

小白: "什么玩意儿, 乱七八糟的, 看不懂"

小黑: "抱歉表达能力不行, 我简单点说"

小黑: "因为PasswordEncoderFactories.createDelegatingPasswordEncoder()函数使用bcrypt作为默认加密方式, 所以在调用PasswordEncoder.encode时默认也使用bcrypt"

小黑: "还不懂就配合下面的图片看看"

造成它默认是BCryptPasswordEncoder的原因是什么?

就上面这一行代码

搞懂这个有什么作用呢?

Spring security默认全部加密方式升级方案全部都是bcrypt,那如果我们要自定义升级到我们需要的加密方式呢?

重写PasswordEncoderFactories类, 把上面的变量修改成你需要修改的加密类型, 并且往Map中添加加密类型的对象

public static PasswordEncoder createDelegatingPasswordEncoder() {
   String encodingId = "无敌加密";
   Map<String, PasswordEncoder> encoders = new HashMap<>();
   encoders.put(encodingId, new 无敌加密PasswordEncoder());
    // 省略一堆代码
   return new DelegatingPasswordEncoder(encodingId, encoders);
}

我去跑题了, 回归正题

第二种方式: 多继承接口方式

public class UserService implements UserDetailsService, UserDetailsPasswordService {
   @Resource
   private UsersMapper usersMapper;
   /**
    * 升级用户密码为当前加密方式
    *
    * @param user        要修改的用户, 这个用户必须有 id
    * @param newPassword 新的密码, 该密码已经被 passwordEncoder 加密
    * @return
    */
   @Override
   public UserDetails updatePassword(UserDetails user, String newPassword) {
      if (user instanceof Users users) {
         users.setPassword(newPassword);
         usersMapper.updateByPrimaryKeySelective(users);
      }
      return user;
   }
   @Override
   public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
      Optional<Users> optionalUsers = Optional.ofNullable(usersMapper.loadUserByUsername(username));
      return optionalUsers.orElseThrow(() -> new UsernameNotFoundException("找不到用户"));
   }
}

这种方式对应着上面三张图片的第1张图片给出的方案

第三种方式: HttpSecurity直接添加

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
   AuthenticationManagerBuilder authenticationManagerBuilder = http.getSharedObject(AuthenticationManagerBuilder.class);
   authenticationManagerBuilder.authenticationProvider(/* 你的认证七 */)
         .userDetailsService(/* 加载用户方式 */)
         .passwordEncoder(/* 密码加密方式 */)
         .userDetailsPasswordManager(/* 第三种更新加密的方式 */);
   return authenticationManagerBuilder.build();
}

这种方式比较麻烦, 只有你需要重写某个Provider的时候才会用到

一般我们使用第二种方式就行

Copyright 2022 版权所有 软件发布 访问手机版

声明:所有软件和文章来自软件开发商或者作者 如有异议 请与本站联系 联系我们