前言
本篇是Spring Security 源码之 UsernamePasswordAuthenticationFilter中AuthenticationProvider
部分的专项分析。
AuthenticationProvider
顾名思义AuthenticationProvider
即认证方式提供者。Spring支持多种认证方式,AuthenticationProvider
是这些认证方式统一实现的接口。
常用的AuthenticationProvider
子类如下:
- DaoAuthenticationProvider:从
UserDetailsService
中获取用户信息进行认证操作。 - RememberMeAuthenticationProvider:提供RemeberMe方式认证。
- JwtAuthenticationProvider:JWT方式认证。
- AnonymousAuthenticationProvider:匿名方式认证
AbstractUserDetailsAuthenticationProvider
在分析DaoAuthenticationProvider
之前我们需要先分析下他的父类AbstractUserDetailsAuthenticationProvider
。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 要求authentication必须是UsernamePasswordAuthenticationToken类型
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// 获取authentication的principal
String username = determineUsername(authentication);
// 标记使用了cache
boolean cacheWasUsed = true;
// 从userCache中读取user详细信息
UserDetails user = this.userCache.getUserFromCache(username);
// 如果cache中没有user详细信息
if (user == null) {
// 标记未使用cache
cacheWasUsed = false;
try {
// 获取user详情,retrieveUser方法在子类中实现
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
}
// 如果报错,认证失败
catch (UsernameNotFoundException ex) {
this.logger.debug("Failed to find user '" + username + "'");
// 这里处理需要隐藏用户名不存在的错误提示这个逻辑
// 如果不需要隐藏,直接抛出UsernameNotFoundException
if (!this.hideUserNotFoundExceptions) {
throw ex;
}
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
// 执行认证之前检查
// 例如检查用户账户是否过期等
this.preAuthenticationChecks.check(user);
// 执行额外检查,additionalAuthenticationChecks方法在子类中实现
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 如果认证发生异常
catch (AuthenticationException ex) {
// 如果没有使用cache,直接抛出异常
if (!cacheWasUsed) {
throw ex;
}
// There was a problem, so try again after checking
// we're using latest data (i.e. not from the cache)
// 如果使用到了cache,尝试不使用cache读取最新数据
cacheWasUsed = false;
user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
this.preAuthenticationChecks.check(user);
additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
}
// 进行认证后额外检查
this.postAuthenticationChecks.check(user);
// 如果没有使用cache,把用户信息存入cache
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
// 如果强制要求返回string类型,将用户名(String类型)返回
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
// 创建认证成功的UsernamePasswordAuthenticationToken
// 使用authoritiesMapper重写用户权限
// 例如SimpleAuthorityMapper,将authority统一转换为大写或小写,再添加上"ROLE_"前缀
return createSuccessAuthentication(principalToReturn, authentication, user);
}
DaoAuthenticationProvider
这里我们分析它实现或重写父类的方法。
获取用户的方法retrieveUser
:
@Override
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
// 如果用户不存在的密码为null,设置为passwordEncoder编码"userNotFoundPassword"后的字符串
// 用来避免出现安全问题
// 因为有些passwordEncoder对于空密码会短路处理,执行消耗时间要小于正常密码
// 用户存在或者不存在执行耗时会有较大差别,为攻击者猜测用户是否存在创造条件
prepareTimingAttackProtection();
try {
// 使用UserDetailsService,获取用户详情
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException(
"UserDetailsService returned null, which is an interface contract violation");
}
return loadedUser;
}
catch (UsernameNotFoundException ex) {
// 抵消上面提到的时间差攻击
// 用passwordEncoder去编码 userNotFoundEncodedPassword
// 耗时和编码已存在用户的密码基本一样
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
这段逻辑中出现了2个重要的类:
- UserDetailsService:负责加载用户数据,接口只有一个方法,从用户名获取用户信息。具有多个实现类。
- PasswordEncoder:密码编码器。依照安全要求,用户密码是不能明文存放在系统中的,PasswordEncoder正是用于将密码编码处理的工具。具有多种密码编码方式的子类。
这两块儿在后面章节中单独介绍。
额外认证检查additionalAuthenticationChecks
,主要校验用户密码是否有误。
@Override
@SuppressWarnings("deprecation")
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
// 如果没有密码,抛出异常
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
// 获取密码
String presentedPassword = authentication.getCredentials().toString();
// 使用passwordEncoder对比encode之后的密码,如果不匹配,抛出异常
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages
.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
最后是createSuccessAuthentication
方法。DaoAuthenticationProvider
重写了父类的方法,加入了密码重新encode的逻辑。
@Override
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication,
UserDetails user) {
// 为了安全起见,判断是否需要再次encoding
boolean upgradeEncoding = this.userDetailsPasswordService != null
&& this.passwordEncoder.upgradeEncoding(user.getPassword());
// 如果需要,再次encode用户密码
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
// 再执行父类的createSuccessAuthentication方法,上面我们已经分析过了
return super.createSuccessAuthentication(principal, authentication, user);
}
UserDetailsService
负责加载用户详细信息,接口只有一个方法loadUserByUsername
,即通过username获取UserDetails
。
它常用的子类如下:
- CachingUserDetailsService:带缓存的UserDetailsService,先从cache中读取用户详情,如果没读取到,再使用被代理的UserDetailsService读取。
- InMemoryUserDetailsManager:在内存中维护一系列用户信息,无法持久化,多用于测试环境。
- JdbcUserDetailsManager:在数据库维护用户信息,可以持久化
- LdapUserDetailsManager:LDAP协议读取用户信息
PasswordEncoder
PasswordEncoder
拥有如下编码算法的子类
- Argon2PasswordEncoder
- BCryptPasswordEncoder
- Pbkdf2PasswordEncoder
- SCryptPasswordEncoder
- MessageDigestPasswordEncoder:MD5方式,由于MD5碰撞安全性不高,不推荐使用
- StandardPasswordEncoder:使用SHA-256方式加上随机的8字节salt,不推荐使用
除此之外,还有2个特殊的实现类:
- DelegatingPasswordEncoder:代理的
PasswordEncoder
,在encode之后的密码字符串添加encoder标识前缀,这样在匹配密码的时候,就可以知道编码之后的密码是哪个PasswordEncoder编码的。 - NoOpPasswordEncoder:不进行任何encode操作
RememberMeAuthenticationProvider
用户使用RememberMe方式进行认证,RememberMe即“记住我”,用户关闭会话后,他的登录状态仍然能够保存一段时间,在此期间不用重新登录。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 只支持RememberMeAuthenticationToken
if (!supports(authentication.getClass())) {
return null;
}
// 判断配置的key的hashcode和token携带的keyHash是否相同
// 如果不相同则认证失败
if (this.key.hashCode() != ((RememberMeAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage("RememberMeAuthenticationProvider.incorrectKey",
"The presented RememberMeAuthenticationToken does not contain the expected key"));
}
return authentication;
}
设置key和创建RememberMeAuthenticationToken
的过程在RememberMeAuthenticationFilter
中。本人在后续博客中会专门介绍。
JwtAuthenticationProvider
使用JSON Web Token方式进行认证。具体生成和检验逻辑较为复杂。请大家参考JSON Web Token 入门教程
这里仅说明下Spring Security的认证逻辑:
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 转换成为bearer token
BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
// 使用JwtDecoder解析bearer token
// 如果解析失败会抛出异常
Jwt jwt = getJwt(bearer);
// 将JWT转换为AbstractAuthenticationToken
AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
// 设置token detail
token.setDetails(bearer.getDetails());
this.logger.debug("Authenticated token");
return token;
}
AnonymousAuthenticationProvider
用于匿名认证。如果某URL允许匿名访问,需要进行此认证过程。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 只支持AnonymousAuthenticationToken
if (!supports(authentication.getClass())) {
return null;
}
// 判断配置的key的hashcode和token携带的keyHash是否相同
// 如果不相同则认证失败
if (this.key.hashCode() != ((AnonymousAuthenticationToken) authentication).getKeyHash()) {
throw new BadCredentialsException(this.messages.getMessage("AnonymousAuthenticationProvider.incorrectKey",
"The presented AnonymousAuthenticationToken does not contain the expected key"));
}
return authentication;
}
AnonymousAuthenticationProvider
的逻辑和RememberMeAuthenticationProvider
非常相似,都是在provider对比key的hash值是否相同。AnonymousAuthenticationToken
的生成过程在AnonymousAuthenticationFilter
中,以后单独开篇介绍。
本文为原创内容,欢迎大家讨论、批评指正与转载。转载时请注明出处。
网友评论