(3)spring security - 认识PasswordEncoder

目录

目标:

  1. 简单了解认证的流程
  2. 简单认识spring security中的Password Encoder

1.简介

还是以这幅图为基础,认识Password Encoder到底是什么?

1.1.简单了解认证流程

当我们在登录表单上输入用户名和密码后,将会被UsernamePasswordAuthenticationFilter过滤器拦截,在public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)函数中,尝试从请求中获取用户名和密码。

java 复制代码
//从请求中获取用户名
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
//从请求中获取密码
String password = obtainPassword(request);
password = (password != null) ? password : "";

接着封装UsernamePasswordAuthenticationToken对象:

java 复制代码
//将用户名和密码封装为未认证的UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,password);

设置details:

java 复制代码
//设置authRequest对象中的details
setDetails(request, authRequest);

认证authRequest封装的信息:

java 复制代码
this.getAuthenticationManager().authenticate(authRequest);

this.getAuthenticationManager()实际是获取一个AuthenticationManager类型的对象,返回的是ProviderManager类型的对象,并调用authenticate函数。当认证成功后会返回一个Authentication对象。

ProviderManager在Spring Security中扮演着认证提供者管理器的角色,其主要作用是管理和协调多个AuthenticationProvider的认证过程。ProviderManager是AuthenticationManager的一个实现,它通过使用一组AuthenticationProvider来处理认证请求。

其主要功能和作用包括:

  1. 认证过程管理‌:ProviderManager会遍历所有支持的AuthenticationProvider,找到第一个能够成功认证的AuthenticationProvider并返回填充更多信息的Authentication对象。如果某个AuthenticationProvider认证失败,ProviderManager会尝试下一个AuthenticationProvider,直到找到一个成功的认证结果或者所有Provider都尝试完毕‌。
  2. 异常处理‌:在认证过程中,如果某个AuthenticationProvider在认证过程中抛出AuthenticationException,ProviderManager会继续尝试下一个Provider。但如果抛出AccountStatusException或InternalAuthenticationServiceException,则会停止认证过程并抛出异常‌。
  3. 扩展性和灵活性‌:ProviderManager支持多种类型的AuthenticationProvider,包括数据库认证、LDAP认证等,这使得Spring Security能够处理不同类型的认证请求,增强了系统的灵活性和扩展性‌。

由于我们现在使用的是数据库认证,所以实际使用的认证程序是DaoAuthenticationProvider:

实际调用的authenticate函数是AbstractUserDetailsAuthenticationProvider抽象类中的authenticate函数:

java 复制代码
	@Override
	public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		//......
		//获取你在登录表单中输入的用户名
		String username = determineUsername(authentication);
		//......
		//查询缓存中是否有匹配的用户信息,实际user为null
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
			cacheWasUsed = false;
			try {
				//使用retrieveUser函数,在数据库中获取用户信息
				//实际是使用我们自己在EmployeeDetailsService.java中定义的loadUserByUsername函数,
				//来获取数据库中的用户信息
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
				//异常处理
			}
			//......
		}
		try {
			//检查账户是否锁定、账户是否可用、账户是否过期
			this.preAuthenticationChecks.check(user);
			//检查密码是否为null,如果密码不为null,获取密码并验证密码
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
			//异常处理
		}
		//密码验证通过后,检查密码是否过期
		this.postAuthenticationChecks.check(user);
		//缓存用户信息
		if (!cacheWasUsed) {
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		//......
		//重新生成一个已经通过认证的UsernamePasswordAuthenticationToken(实际上它也是Authentication类型的),并返回它
		//最终的Authentication对象被返回给UsernamePasswordAuthenticationFilter过滤器的attemptAuthentication函数
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

接着执行AbstractAuthenticationProcessingFilterdoFilter函数。

doFilter函数中将会:

  1. 组合多个SessionAuthenticationStrategy实例,以提供更灵活和强大的会话管理功能。
  2. 在securitycontexholder上设置认证成功的身份验证对象
  3. 通知配置的RememberMeServices登录成功(当前没做任何事情)
  4. 通过配置的ApplicationEventPublisher触发一个InteractiveAuthenticationSuccessEvent
  5. 将其他行为委托给AuthenticationSuccessHandler。

最后回到FilterChainProxy,执行其它过滤器。

2.密码验证

上一节说到,密码的验证是由一下代码实现的:

java 复制代码
//检查密码是否为null,如果密码不为null,获取密码并验证密码
//其中user保存的数据库中的用户信息数据
//authentication保存的是登录表单中输入的用户名和密码
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);

additionalAuthenticationChecks的源代码是:

java 复制代码
	@Override
	@SuppressWarnings("deprecation")
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		//判断密码是否为null
		if (authentication.getCredentials() == null) {
			//打印日志
			//抛出异常
		}
		//获取明文密码
		String presentedPassword = authentication.getCredentials().toString();
		//验证密码
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			//密码验证失败
			//打印日志
			//抛出异常
		}
	}

其中的matches函数源码如下:

java 复制代码
	@Override
	public boolean matches(CharSequence rawPassword, String encodedPassword) {
		if (rawPassword == null) {
			throw new IllegalArgumentException("rawPassword cannot be null");
		}
		if (encodedPassword == null || encodedPassword.length() == 0) {
			this.logger.warn("Empty encoded password");
			return false;
		}
		//判断是否是BCrypt
		if (!this.BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
			this.logger.warn("Encoded password does not look like BCrypt");
			return false;
		}
		//检查明文密码(登陆表单中输入的密码)是否与先前散列的密码(数据库中的密码)匹配
		return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
	}

checkpw实际就是比较两个字符串:

  1. 首先将明文密码编码成与数据库中的密码一样的格式
  2. 然后使用MessageDigest.isEqual方法,比较连个已编码的密码是否具有相同的长度并且对应位置的所有字节都相等

存储在数据库中的已编码的密码本身永远不会被解码!!!

3.PasswordEncoder的内置实现

我们可以在PasswordEncoderFactories类中找到spring security中支持的编码器的完整列表。如果其中一个符合我们的要求,我们就不需要自定义自己的编码器。

  • bcrypt - BCryptPasswordEncoder
  • ldap - org. springframework. security. crypto. password. LdapShaPasswordEncoder
  • MD4 - org. springframework. security. crypto. password. Md4PasswordEncoder
  • MD5 - new MessageDigestPasswordEncoder("MD5")
  • noop - org. springframework. security. crypto. password. NoOpPasswordEncoder:不编码密码,保持密码为明文状态。它只能用于单元测试。
  • pbkdf2 - Pbkdf2PasswordEncoder. defaultsForSpringSecurity_v5_5()
  • pbkdf2@SpringSecurity_v5_8 - Pbkdf2PasswordEncoder. defaultsForSpringSecurity_v5_8()
  • scrypt - SCryptPasswordEncoder. defaultsForSpringSecurity_v4_1()
  • scrypt@SpringSecurity_v5_8 - SCryptPasswordEncoder. defaultsForSpringSecurity_v5_8()
  • SHA-1 - new MessageDigestPasswordEncoder("SHA-1")
  • SHA-256 - new MessageDigestPasswordEncoder("SHA-256")
  • sha256 - org. springframework. security. crypto. password. StandardPasswordEncoder
  • argon2 - Argon2PasswordEncoder. defaultsForSpringSecurity_v5_2()
  • argon2@SpringSecurity_v5_8 - Argon2PasswordEncoder. defaultsForSpringSecurity_v5_8()

在上述的编码器中,建议使用bcrypt , pbkdf2 或scrypt 。

4.小结

本章以(2)Spring Security - 了解UserDetailsService的代码为基础,通过调试代码,简单学习了spring security的认证流程和密码的验证流程。

以后如果需要用到自定义的密码编码器,将另外写一篇学习笔记。

相关推荐
2301_813599551 小时前
Go语言怎么做秒杀系统_Go语言秒杀系统实战教程【实用】
jvm·数据库·python
NCIN EXPE6 小时前
redis 使用
数据库·redis·缓存
MongoDB 数据平台6 小时前
为编码代理引入 MongoDB 代理技能和插件
数据库·mongodb
lUie INGA6 小时前
在2023idea中如何创建SpringBoot
java·spring boot·后端
极客on之路6 小时前
mysql explain type 各个字段解释
数据库·mysql
代码雕刻家6 小时前
MySQL与SQL Server的基本指令
数据库·mysql·sqlserver
lThE ANDE6 小时前
开启mysql的binlog日志
数据库·mysql
yejqvow126 小时前
CSS如何控制placeholder文字的颜色_使用--placeholder伪元素
jvm·数据库·python
oLLI PILO6 小时前
nacos2.3.0 接入pgsql或其他数据库
数据库
geBR OTTE6 小时前
SpringBoot中整合ONLYOFFICE在线编辑
java·spring boot·后端