循序渐进学spring security 第九篇,支持多种加密方案源码解读和示例代码

回顾

上一篇文章《循序渐进学spring security 第八篇,如何配置密码加密?是否支持多种加密方案?》我们介绍了配置密码的加密,顺带介绍了spring security 是支持多种加密方案的,那么,spring security是怎么支持多种加密方案的,我们通过阅读源码发现是通过DelegatingPasswordEncoder 这个实现了PasswordEncoder 的类完成的,今晚我们继续来探索DelegatingPasswordEncoder 是怎么支持多种加密方案的

在阅读本文之前,建议先看看我之前写的文章,有助于理解本文

  1. 面试不要在说不熟悉spring security了,一个demo让你使劲忽悠面试官
  2. 循序渐进学习spring security 第二篇,如何修改默认用户?
  3. 循序渐进学习spring security 第三篇,如何自定义登录页面?登录回调?
  4. 循序渐进雪spring security 第四篇,登录流程是怎样的?登录用户信息保存在哪里?
  5. 循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串?
  6. 循序渐进学spring security第六篇,手把手教你如何从数据库读取用户进行登录验证,mybatis集成
  7. 循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了
  8. 循序渐进学spring security 第八篇,如何配置密码加密?是否支持多种加密方案?

为什么要支持多密码方案?

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

DelegatingPasswordEncoder 类是怎么创建的?

上一章节我们介绍了DelegatingPasswordEncoder 是在DaoAuthenticationProvider 初始化时就默认创建了一个PasswordEncoder 的实现,也就是这个时候就创建了DelegatingPasswordEncoder
在这里插入图片描述
跟进看源码

    public static PasswordEncoder createDelegatingPasswordEncoder() {
    
    
        String encodingId = "bcrypt";
        Map<String, PasswordEncoder> encoders = new HashMap();
        encoders.put(encodingId, new BCryptPasswordEncoder());
        encoders.put("ldap", new LdapShaPasswordEncoder());
        encoders.put("MD4", new Md4PasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
        encoders.put("scrypt", new SCryptPasswordEncoder());
        encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("sha256", new StandardPasswordEncoder());
        encoders.put("argon2", new Argon2PasswordEncoder());
        return new DelegatingPasswordEncoder(encodingId, encoders);
    }

也就是说DaoAuthenticationProvider 在初始化时,就创建了DelegatingPasswordEncoder 作为默认PasswordEncoder ,如果我们在配置spring security时没有指定PasswordEncoder 的bean实例时,就会用默认的DelegatingPasswordEncoder实例,而DelegatingPasswordEncoder通过构造方法,第一个参数是encodingId,第二个参数就是一个map集合,map集合中有多个PasswordEncoder的实现,这里要注意的是encodingId 对应的PasswordEncoder 实例是BCryptPasswordEncoder 对象

好了
我们在跟进DelegatingPasswordEncoder的构造方法

public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder) {
    
    
		if (idForEncode == null) {
    
    
			throw new IllegalArgumentException("idForEncode cannot be null");
		}
		if (!idToPasswordEncoder.containsKey(idForEncode)) {
    
    
			throw new IllegalArgumentException(
					"idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
		}
		for (String id : idToPasswordEncoder.keySet()) {
    
    
			if (id == null) {
    
    
				continue;
			}
			if (id.contains(PREFIX)) {
    
    
				throw new IllegalArgumentException("id " + id + " cannot contain " + PREFIX);
			}
			if (id.contains(SUFFIX)) {
    
    
				throw new IllegalArgumentException("id " + id + " cannot contain " + SUFFIX);
			}
		}
		this.idForEncode = idForEncode;
		this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
		this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
	}

在DelegatingPasswordEncoder 构造方法中,第一个参数是idForEncode ,第二个参数是idToPasswordEncoder ,就是Map<String, PasswordEncoder>集合

  • 首选判断第一个参数:idForEncode 是否为空,在第二个参数map的集合中是否存在以idForEncode为key的数据,如果不存在,则抛出异常
  • 循环遍历idToPasswordEncoder 的key,判断key中是否包含前缀和后缀分别为"{“和”}"的,如果包含,就抛出异常
  • 给成员变量赋值,其实,成员变量idForEncode 就是默认的加密方式的key,成员变量passwordEncoderForEncode 就是默认PasswordEncoder加密的实例,而对应的实例,实际上就是BCryptPasswordEncoder对象,也就是说,DelegatingPasswordEncoder 默认的加密对象就是BCryptPasswordEncoder,idToPasswordEncoder 其实还是第二个参数传入的所有数据集合

接下来,我们来看看DelegatingPasswordEncoder 是如何对密码加密的

DelegatingPasswordEncoder 如何加密?

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

加密前,先将idForEncode 加上前缀和后缀,再拼接上passwordEncoderForEncode 加密的结果,这就是DelegatingPasswordEncoder 的加密方式。注意的是passwordEncoderForEncode 加密的字符串,才是最原始的密码的加密

DelegatingPasswordEncoder 如何校验密码?

	@Override
	public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
    
    
		if (rawPassword == null && prefixEncodedPassword == null) {
    
    
			return true;
		}
		String id = extractId(prefixEncodedPassword);
		PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
		if (delegate == null) {
    
    
			return this.defaultPasswordEncoderForMatches.matches(rawPassword, prefixEncodedPassword);
		}
		String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
		return delegate.matches(rawPassword, encodedPassword);
	}

看源码一目了然

  1. 首先,参数rawPassword 是用户输入的登录密码,prefixEncodedPassword 是系统中的原密码,先从prefixEncodedPassword 中解析出来id,如下是源码,也就是将前缀和后缀中的id取出来作为id
	private String extractId(String prefixEncodedPassword) {
    
    
		if (prefixEncodedPassword == null) {
    
    
			return null;
		}
		int start = prefixEncodedPassword.indexOf(PREFIX);
		if (start != 0) {
    
    
			return null;
		}
		int end = prefixEncodedPassword.indexOf(SUFFIX, start);
		if (end < 0) {
    
    
			return null;
		}
		return prefixEncodedPassword.substring(start + 1, end);
	}
  1. 根据解析出来的id,从idToPasswordEncoder map集合中取出来加密对象delegate ,如果为空,则进入到默认的密码校验方式,其实就是抛出异常,这一步大家可以自己跟进去看源码
  2. 进一步从原密码中提取出来密码,也就是将以SUFFIX作为分割符取出来原始密码
	private String extractEncodedPassword(String prefixEncodedPassword) {
    
    
		int start = prefixEncodedPassword.indexOf(SUFFIX);
		return prefixEncodedPassword.substring(start + 1);
	}
  1. 最后,再根据2中得到的delegate 对象,进行登录密码和原始密码的校验

好了,看源码就非常清晰了

示例代码

接下来,我们尝试使用DelegatingPasswordEncoder 内部的map集合中初始化的几种加密方式对密码进行加密,然后再结合DelegatingPasswordEncoder 的密码加密和密码校验方式,进行一步步校验和证实spring security支持多种加密方式共存的原则

	@Test
    public void testPasswordEncoder() {
    
    
        Map<String, PasswordEncoder> encoders = new HashMap<>();
        encoders.put("bcrypt", new BCryptPasswordEncoder());
        encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
        encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
        encoders.put("noop", NoOpPasswordEncoder.getInstance());
        DelegatingPasswordEncoder encoder1 = new DelegatingPasswordEncoder("bcrypt", encoders);
        DelegatingPasswordEncoder encoder2 = new DelegatingPasswordEncoder("MD5", encoders);
        DelegatingPasswordEncoder encoder3 = new DelegatingPasswordEncoder("SHA-256", encoders);
        DelegatingPasswordEncoder encoder4 = new DelegatingPasswordEncoder("noop", encoders);
        String e1 = encoder1.encode("123456");
        String e2 = encoder2.encode("123456");
        String e3 = encoder3.encode("123456");
        String e4 = encoder4.encode("123456");
        System.out.println("e1 = " + e1);
        System.out.println("e2 = " + e2);
        System.out.println("e3 = " + e3);
        System.out.println("e4 = " + e4);
    }

生成密码结果

e1 = {
    
    bcrypt}$2a$10$Udax6Xnpqj/3yduBANRcDOSIQN6Ty.MA2PtQgdO2lDvc9KG39q6GC
e2 = {
    
    MD5}{
    
    1nKIz3ZSAkCw1xDIPmehd5hQNtezarKrfpHsuqM11pY=}ba2563fcddbd2a96d19e4b6b7f5a3693
e3 = {
    
    SHA-256}{
    
    BaQGLNLZLKAPa0OXluSCNmd6dradJN0WAFvScNrs4Wo=}f01de9b47600d7583d6944c7d560e322eaf40dc37e779bb0f30b9e0109b74ae9
e4 = {
    
    noop}123456

有了密码,我们基于之前的文章《循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了》进行改造,如果没有看过这篇文章的建议先看下,熟悉下项目,或者下载下来自己修改测试

步骤如下:

  1. 将配置类SecurityConfig中关于PasswordEncoder bean的配置注释,不配置,这样默认就会自动使用DaoAuthenticationProvider 默认的密码代理类DelegatingPasswordEncoder 为默认密码加密类了

  2. 将用户表中,用户名为harry的密码改为e1 打印的密码,mike 密码改为e2打印的密码,再加steven 用户和james用户,密码分别是是e3和e4打印的密码
    在这里插入图片描述

  3. 启动项目,测试上述4个用户的登录情况,发现4个用户不同加密方式的都能正常登录成功

在这里插入图片描述

由此可见,想要支持多种加密方案,使用spring security 默认的即可,不需要单独配置PasswordEncoder bean

猜你喜欢

转载自blog.csdn.net/huangxuanheng/article/details/119238052