目录
- 1.简介
- 1.1.简单了解认证流程
- 2.密码验证
- 3.PasswordEncoder的内置实现
- 4.小结
目标:
- 简单了解认证的流程
- 简单认识spring security中的Password Encoder
1.简介
还是以这幅图为基础,认识Password Encoder到底是什么?
1.1.简单了解认证流程
当我们在登录表单上输入用户名和密码后,将会被UsernamePasswordAuthenticationFilter
过滤器拦截,在public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
函数中,尝试从请求中获取用户名和密码。
//从请求中获取用户名
String username = obtainUsername(request);
username = (username != null) ? username.trim() : "";
//从请求中获取密码
String password = obtainPassword(request);
password = (password != null) ? password : "";
接着封装UsernamePasswordAuthenticationToken
对象:
//将用户名和密码封装为未认证的UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,password);
设置details:
//设置authRequest对象中的details
setDetails(request, authRequest);
认证authRequest封装的信息:
this.getAuthenticationManager().authenticate(authRequest);
this.getAuthenticationManager()
实际是获取一个AuthenticationManager
类型的对象,返回的是ProviderManager
类型的对象,并调用authenticate
函数。当认证成功后会返回一个Authentication对象。
ProviderManager
在Spring Security中扮演着认证提供者管理器的角色,其主要作用是管理和协调多个AuthenticationProvider的认证过程。ProviderManager是AuthenticationManager的一个实现,它通过使用一组AuthenticationProvider来处理认证请求。
其主要功能和作用包括:
- 认证过程管理:ProviderManager会遍历所有支持的AuthenticationProvider,找到第一个能够成功认证的AuthenticationProvider并返回填充更多信息的Authentication对象。如果某个AuthenticationProvider认证失败,ProviderManager会尝试下一个AuthenticationProvider,直到找到一个成功的认证结果或者所有Provider都尝试完毕。
- 异常处理:在认证过程中,如果某个AuthenticationProvider在认证过程中抛出AuthenticationException,ProviderManager会继续尝试下一个Provider。但如果抛出AccountStatusException或InternalAuthenticationServiceException,则会停止认证过程并抛出异常。
- 扩展性和灵活性:ProviderManager支持多种类型的AuthenticationProvider,包括数据库认证、LDAP认证等,这使得Spring Security能够处理不同类型的认证请求,增强了系统的灵活性和扩展性。
由于我们现在使用的是数据库认证,所以实际使用的认证程序是DaoAuthenticationProvider
:
实际调用的authenticate
函数是AbstractUserDetailsAuthenticationProvider
抽象类中的authenticate
函数:
@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);
}
接着执行AbstractAuthenticationProcessingFilter
的doFilter
函数。
在doFilter
函数中将会:
- 组合多个SessionAuthenticationStrategy实例,以提供更灵活和强大的会话管理功能。
- 在securitycontexholder上设置认证成功的身份验证对象
- 通知配置的RememberMeServices登录成功(当前没做任何事情)
- 通过配置的ApplicationEventPublisher触发一个InteractiveAuthenticationSuccessEvent
- 将其他行为委托给AuthenticationSuccessHandler。
最后回到FilterChainProxy
,执行其它过滤器。
2.密码验证
上一节说到,密码的验证是由一下代码实现的:
//检查密码是否为null,如果密码不为null,获取密码并验证密码
//其中user保存的数据库中的用户信息数据
//authentication保存的是登录表单中输入的用户名和密码
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
additionalAuthenticationChecks
的源代码是:
@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
函数源码如下:
@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
实际就是比较两个字符串:
- 首先将明文密码编码成与数据库中的密码一样的格式
- 然后使用
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的认证流程和密码的验证流程。
以后如果需要用到自定义的密码编码器,将另外写一篇学习笔记。